NIO中如何使用虚引用管理堆外内存原理

wuchangjian2021-11-11 11:41:14编程学习

虚引用是最弱的引用,弱到什么地步呢?也就是你定义了虚引用根本无法通过虚引用获取到这个对象,更别谈影响这个对象的生命周期了。在虚引用中唯一的作用就是用队列接收对象即将死亡的通知。

1、虚引用的特点

  • 虚引用必须与ReferenceQueue一起使用,当GC准备回收一个对象,如果发现它还有虚引用,就会在回收之前,把这个虚引用加入到与之关联的ReferenceQueue中。

  • 无法通过虚引用来获取被虚引用对象引用的真实对象!!!!

    ReferenceQueue queue = new ReferenceQueue();
    PhantomReference<byte[]> reference = new PhantomReference<byte[]>(new byte[1], queue);
    // 调用reference.get(),试图获取  【被虚引用对象引用的真实对象】  这里会返回null
    System.out.println(reference.get());
    

    原因是因为引用类重写了get方法

    public class PhantomReference<T> extends Reference<T> {
    
      	// 重写get方法,直接返回空
        public T get() {
            return null;
        }
    }
    

2、虚引用的作用

利用虚引用管理堆外内存是虚引用的典型用法,比如JDK的NIO分配堆外内存的时候就是使用的虚引用的特性来管理的堆外内存。

a)、前景引入

问题1:为什么Java不用自己手动释放内存

我们知道在Java里我们不用像C++那样自己手动去释放内存,因为我们的内存是分配在JVM堆空间的,会由GC垃圾回收器帮我们自动管理并回收垃圾内存。但是如果我们能不能操作堆外内存呢?

问题2:Java中有没有办法操作堆外内存

答案是:有的

  • 方法一:比如在JDK的NIO中我们就可以通过ByteBuffer.allocateDirect(size)去手动的分配一块堆外内存
  • 方法二:比如我们可以通过反射拿到Unsafe对象,然后通过调用unsafe.allocateMemory(size)方法去开辟一块堆外内存

问题3:Java中如何释放堆外内存

从上面我们已经看到我们可以通过ByteBuffer.allocateDirectunsafe.allocateMemory两种方式分配堆外内存。但是当分配的堆外内存使用完了以后我们该怎么释放呢?

  • 堆外内存不受JVM垃圾收集器管理,所以垃圾收集器无法回收堆外内存。所以堆外内存需要我们手动释放。我们可以通过unsafe对象的unsafe.freeMemory(address)方法手动释放指定位置的堆外内存。

b)、通过unsafe来操作堆外内存示例

  • 通过unsafe对象来操作堆外内存,需要自己手动释放
/**
 * 直接内存分配的底层原理:Unsafe
 */
public class Demo1_27 {

    static int _1Gb = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        Unsafe unsafe = getUnsafe();
        // 分配内存
        long base = unsafe.allocateMemory(_1Gb);
        unsafe.setMemory(base, _1Gb, (byte) 0);
        System.in.read();

        // 释放内存
        unsafe.freeMemory(base);
        System.in.read();
    }

    /**
     * 通过反射获取Unsafe对象
     */
    public static Unsafe getUnsafe() {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            Unsafe unsafe = (Unsafe) f.get(null);
            return unsafe;
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}

c)、通过ByteBuffer来使用堆外内存示例

/**
 * 添加JVM运行时参数:-XX:+DisableExplicitGC,禁用显式回收对直接内存的影响
 */
public class Demo1_26 {
  
    static int _1Gb = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
      	// ①、分配1G内存
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
        System.out.println("分配完毕...");
        System.in.read();
        System.out.println("开始释放...");
      	// ②、被虚引用引用的对象  的  强引用被释放掉
        byteBuffer = null;
        System.gc(); // 显式的垃圾回收,Full GC
      	// ③、在这里阻塞住,通过内存分析工具分析当前JVM进程的内存消耗。观察上面分配的1G内存是否被回收
        System.in.read();
    }
}

通过内存分析工具可以看到这块1gb的内存已经被回收了。可是我们并没有手动释放堆外内存呀。这是什么情况?

内存释放前

内存释放后

3、使用虚引用管理堆外内存分析

  • 上面介绍了两种使用堆外内存的方法,一种是依靠unsafe对象,另一种是NIO中的ByteBuffer,直接使用unsafe对象来操作内存,对于一般开发者来说难度很大,并且如果内存管理不当,容易造成内存泄漏。所以不推荐。推荐使用的是ByteBuffer来操作堆外内存。
  • 在上面的ByteBuffer案例中,我们并没有手动释放内存,但是最终当byteBuffer对象被垃圾回收时,堆外内存仍然被释放掉了,这是什么原因?是垃圾回收器回收的堆外内存吗?显然不是,因为堆外内存不受JVM垃圾收集器管理。

对于ByteBuffer这块的源码,我开始的时候尝试把它贴出来讲,但是发现这么讲的话需要的篇幅太长了。这里就以将流程的方式来聊聊ByteBuffer中是如何释放的堆外内存的吧!!!

①、测试代码回顾

/**
 * 添加JVM运行时参数:-XX:+DisableExplicitGC,禁用显式回收对直接内存的影响
 */
public class Demo1_26 {
  
    static int _1Gb = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
      	// ①、分配1G内存
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
        System.out.println("分配完毕...");
        System.in.read();
        System.out.println("开始释放...");
      	// ②、被虚引用引用的对象  的  强引用被释放掉
        byteBuffer = null;
        System.gc(); // 显式的垃圾回收,Full GC
      	// ③、在这里阻塞住,通过内存分析工具分析当前JVM进程的内存消耗。观察上面分配的1G内存是否被回收
        System.in.read();
    }
}

②、流程分析

1、byteBuffer未被回收前内存图示

即在源码中的②还没有被执行的时候,内存图示如下

  • 可以看到被虚引用引用的对象其实就是这个byteBuffer对象。所以说需要重点关注的是这个byteBuffer对象被回收了以后会触发什么操作。

2、byteBuffer被回收后内存图示

  • 当byteBuffer被回收后,在进行GC垃圾回收的时候,发现虚引用对象CleanerPhantomReference类型的对象,并且被该对象引用的对象(ByteBuffer对象)已经被回收了
  • 那么他就将将这个对象放入到(ReferenceQueue)队列中
  • JVM中会有一个优先级很低的线程会去将该队列中的虚引用对象取出来,然后回调clean()方法
  • clean()方法里做的工作其实就是根据内存地址去释放这块内存(内部还是通过unsafe对象去释放的内存)。

3、部分源码展示

a)、ByteBuffer创建源码
// 创建堆外内存 
public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
 }

 // 这就是一个虚引用对象
 private final Cleaner cleaner;

// 具体创建堆外内存逻辑
DirectByteBuffer(int cap) {

        super(-1, 0, cap, cap);
        boolean pa = VM.isDirectMemoryPageAligned();
        int ps = Bits.pageSize();
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
        Bits.reserveMemory(size, cap);

        long base = 0;
        try {
          	// 使用unsafe对象分配一块堆外内存
            base = unsafe.allocateMemory(size);
        } catch (OutOfMemoryError x) {
            Bits.unreserveMemory(size, cap);
            throw x;
        }
        unsafe.setMemory(base, size, (byte) 0);
        if (pa && (base % ps != 0)) {
            // Round up to page boundary
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
  			// 【重点】创建一个虚引用对象,这个虚引用对象指向this对象(也就是指向创建的byteBuffer对象)
  			// 这里的 Deallocator 可以重点探究。
  			// 其实这里就是将Deallocator保存到了虚引用对象cleaner上。当虚引用对象被放入队列后,就会执行Deallocator对象的clean() 方法来清除堆外内存。看下面源码体现
  			// 可以看到这里将分配的堆外内存地址传递给了Deallocator对象保存。到时候释放堆外内存的时候也就要依靠这个地址
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }
b)、Deallocator如何释放内存源码
private static class Deallocator implements Runnable{

    private static Unsafe unsafe = Unsafe.getUnsafe();

    private long address;
    private long size;
    private int capacity;

    private Deallocator(long address, long size, int capacity) {
        assert (address != 0);
        this.address = address;
        this.size = size;
        this.capacity = capacity;
    }

  	// 当虚引用对象被放入队列后,JVM中会有一个线程去取这个队列中的元素。然后就会执行这个方法释放内存
    public void run() {
        if (address == 0) {
            // Paranoia
            return;
        }
      	// 通过分配堆外内存时保存的地址来释放堆外内存
        unsafe.freeMemory(address);
        address = 0;
        Bits.unreserveMemory(size, capacity);
    }

}

相关文章

6.npm

...

链表——笔记理解

众所周知,在一个数组中插入或是删除一个元素,都会需要移动后面...

发表评论    

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。