谈谈JDK堆外内存的创建和回收

堆外内存的优势在于IO操作,相比堆内存可以减少一次copy和gc的次数。下面通过源码去了解堆外内存的分配和回收。一般分配堆外内存通过ByteBuffer allocateDirect(int capacity)方法,其内部是通过如下构造函数来实现。

DirectByteBuffer(int cap) {               
    super(-1, 0, cap, cap);// mark, pos, lim, 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 {
        base = unsafe.allocateMemory(size);
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    unsafe.setMemory(base, size, (byte) 0);
    if (pa && (base % ps != 0)) { // 修改内存起始地址
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;
}

首先调用父类的构造方法初始化ByteBuffer的四个基本属性,接下来reserveMemory方法是判断堆外剩余内存是否满足。这里的剩余并不是系统真是的剩余内存,参数-XX:MaxDirectMemorySize指定JVM最多可用的堆外内存。如果堆外内存不足,则触发System.gc,这里有些难已理解,明明是堆外内存不足,System.gc的作用是建议VM进行full gc,再怎么说也是堆内存的回收。这里先保留这个疑问,继续往下看。

根据VM参数判断是否内存页对齐计算真实分配内存的大小,由-XX:+PageAlignDirectMemory控制,默认为false。allocateMemory是真正分配内存如果失败则回收内存。setMemory为填充内存。

接下来根据是否内存页对齐来计算内存的起始地址。我们知道HeapByteBuffer是基于byte数组来实现,不需要我们去考虑回收由JVM去处理。但是堆外内存JVM无法像堆内存那样回收,因此就有了Cleaner和Deallocator的存在。

每一个DirectBytebuffer都对应一个DeallocatorCleaner对象,而DeallocatorCleaner的一个属性。Deallocator继承了Runnable接口,当然run方法内部是释放内存的逻辑。

public void run() {
    // 释放
    unsafe.freeMemory(address);
    address = 0;
    // 修改堆外内存的占用大小
    Bits.unreserveMemory(size, capacity);
}

在分析Cleaner之前我们先复习下PhantomReference(虚引用)

虚引用,正如其名,对一个对象而言,这个引用形同虚设,有和没有一样。如果一个对象与GC Roots之间仅存在虚引用,则称这个对象为虚可达(phantom reachable)对象。
当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在垃圾回收后,将这个虚引用加入引用队列,可以通过检查引用队列中是否有相应的虚引用来判断对象是否已经被回收了。

Cleaner继承自PhantomReference,在谈谈Java Reference的原理中介绍了Reference框架的大体逻辑,在PendingHandlerThread会把Pending list的引用对象移入Reference Queue,这个过程中如果Reference是Cleaner类型,那么会执行clean方法

public void clean() {
    if (remove(this)) {
        try {
            this.thunk.run();
        } catch (final Throwable var2) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    if (System.err != null) {
                        (new Error("Cleaner terminated abnormally", var2)).printStackTrace();
                    }
                    System.exit(1);
                    return null;
                }
            });
        }
    }
}

thunk是Deallocator类型,也就是说它run方法最终是由PendingHandlerThread线程执行的。这就是JDK的自动回收堆外内存。

总结一下:DirectByteBuffer对象指向堆外的内存,它保存了一块内存的基本属性和Cleaner和Deallocator对象等。占用的空间相比堆外内存只是冰山一角,当DirectByteBuffer对象被回收,Cleaner对象也就是他的虚引用被加入到Pending list,PendingHandlerThread线程执行Cleaner的clean方法,最终释放堆外内存。这也就解释了为什么执行gc可以回收堆外内存了。也可以手动释放,首先拿到DirectByteBuffer的Cleaner对象,执行它的clean方法。

  1. 由于cleaner是private访问权限,所以自然想到使用反射来实现。
  2. DirectByteBuffer实现了DirectBuffer接口,这个接口有cleaner方法可以获取cleaner对象
DirectByteBuffer 是 Java NIO 中提供的一种用于操作堆外内存的缓冲区类型。DirectByteBuffer 持有一个堆外内存的引用,当 DirectByteBuffer 对象被垃圾回收时,它所持有的堆外内存并不会自动被回收,这可能会导致内存泄露。 为了避免内存泄露,需要手动释放 DirectByteBuffer 所持有的堆外内存。一种常见的方法是通过调用 DirectByteBuffer 的 clean() 方法来释放堆外内存。但是,clean() 方法并不能保证立即释放堆外内存,而且它只能在 JDK 9 之前的版本中使用。 另外一种更可靠的方法是使用反射来调用 DirectByteBuffer 内部的 Cleaner 对象的 clean() 方法,这样可以保证及时释放堆外内存。示例代码如下: ``` import sun.misc.Cleaner; import java.lang.reflect.Field; import java.nio.ByteBuffer; public class DirectByteBufferUtil { private static final sun.misc.Unsafe UNSAFE; private static final long ADDRESS_OFFSET; private static final long CLEANER_OFFSET; static { try { Field field = sun.misc.Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); UNSAFE = (sun.misc.Unsafe) field.get(null); ADDRESS_OFFSET = UNSAFE.objectFieldOffset(Buffer.class.getDeclaredField("address")); CLEANER_OFFSET = UNSAFE.objectFieldOffset(ByteBuffer.class.getDeclaredField("cleaner")); } catch (Exception ex) { throw new RuntimeException(ex); } } public static void free(ByteBuffer buffer) { long address = UNSAFE.getLong(buffer, ADDRESS_OFFSET); if (address != 0) { UNSAFE.putLong(buffer, ADDRESS_OFFSET, 0); Cleaner cleaner = (Cleaner) UNSAFE.getObject(buffer, CLEANER_OFFSET); if (cleaner != null) { cleaner.clean(); } } } } ``` 使用上述代码中的 free() 方法来释放 DirectByteBuffer 所持有的堆外内存,例如: ``` ByteBuffer buffer = ByteBuffer.allocateDirect(1024); // 使用 buffer 操作堆外内存 DirectByteBufferUtil.free(buffer); ``` 这样就可以释放 DirectByteBuffer 所持有的堆外内存了。需要注意的是,由于上述代码使用了 JDK 内部的类和方法,所以可能不具有可移植性,应该根据具体情况谨慎使用。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值