java 堆外内存 gc_Java 堆外内存

Java中的DirectByteBuffer用于管理堆外内存,通过 Cleaner 实现基于GC的回收。当DirectByteBuffer对象被GC回收,Cleaner对象会触发内存释放。若DirectByteBuffer在老生代长时间存活,可能导致堆外内存无法及时释放,此时可通过手动调用Cleaner的clean方法进行主动回收。Cleaner继承自PhantomReference,GC检测到其引用的对象失效后,会执行clean方法。
摘要由CSDN通过智能技术生成

入口ByteBuffer.allocateDirect

public static ByteBuffer allocateDirect(intcapacity) {return newDirectByteBuffer(capacity);

}

DirectByteBuffer构造函数

DirectByteBuffer(int cap) { //package-private

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{

base=unsafe.allocateMemory(size);

}catch(OutOfMemoryError x) {

Bits.unreserveMemory(size, cap);throwx;

}

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;

}

cleaner= Cleaner.create(this, newDeallocator(base, size, cap));

att= null;

}

c5842c81f9cd5c7c25d3cd06549ce4b9.png

JDK中使用DirectByteBuffer对象来表示堆外内存,每个DirectByteBuffer对象在初始化时,都会创建一个对用的Cleaner对象,这个Cleaner对象会在合适的时候执行

unsafe.freeMemory(address),从而回收这块堆外内存。当初始化一块堆外内存时,对象的引用关系如下:

其中first是Cleaner类的静态变量,Cleaner对象在初始化时会被添加到Clener链表中,和first形成引用关系,ReferenceQueue是用来保存需要回收的Cleaner对象。

如果该DirectByteBuffer对象在一次GC中被回收了

21cfc339b0c5f318321f96376c22d573.png

此时,只有Cleaner对象唯一保存了堆外内存的数据(开始地址、大小和容量),在下一次Full GC时,把该Cleaner对象放入到ReferenceQueue中,并触发clean方法。

1. 堆外内存的创建

在DirectByteBuffer中,首先向Bits类申请额度,Bits类有一个全局的 totalCapacity变量,记录着全部DirectByteBuffer的总大小,每次申请,都先看看是否超限,

堆外内存的限额默认与堆内内存(由-Xmx 设定)相仿,可用 -XX:MaxDirectMemorySize 重新设定。如果已经超限,会主动执行Sytem.gc(),期待能主动回收一点堆外内存。

然后休眠一百毫秒,看看totalCapacity降下来没有,如果内存还是不足,就抛出OOM异常。如果额度被批准,就调用大名鼎鼎的sun.misc.Unsafe去分配内存。

2. 堆外内存基于GC的回收

存在于堆内的DirectByteBuffer对象很小,只存着基地址和大小等几个属性,和一个Cleaner,但它代表着后面所分配的一大段内存,是所谓的冰山对象。

快速回顾一下堆内的GC机制,当新生代满了,就会发生young gc;如果此时对象还没失效,就不会被回收;撑过几次young gc后,对象被迁移到老生代;

当老生代也满了,就会发生full gc。这里可以看到一种尴尬的情况,因为DirectByteBuffer本身的个头很小,只要熬过了young gc,即使已经失效了也能

在老生代里舒服的呆着,不容易把老生代撑爆触发full gc,如果没有别的大块头进入老生代触发full gc,就一直在那耗着,占着一大片堆外内存不释放。

这时,就只能靠前面提到的申请额度超限时触发的system.gc()来救场了。但这道最后的保险其实也不很好,首先它会中断整个进程,然后它让当前线程

睡了整整一百毫秒,而且如果gc没在一百毫秒内完成,它仍然会无情的抛出OOM异常。还有,万一,万一大家迷信某个调优指南设置了-DisableExplicitGC

禁止了system.gc(),那就不好玩了。所以,堆外内存还是自己主动点回收更好,比如Netty就是这么做的。

3. 堆外内存的主动回收

对于Sun的JDK这其实很简单,只要从DirectByteBuffer里取出那个sun.misc.Cleaner,然后调用它的clean()就行。

在Netty里,因为不确定跑在Sun的JDK里(比如安卓),所以多废了些功夫来确定Cleaner的存在。

4. Cleaner如何与GC相关联?

Cleaner就是PhantomReference的子类。

当GC时发现它除了PhantomReference外已不可达(持有Cleaner的DirectByteBuffer失效了),就会把它放进 Reference类pending list静态变量里。

然后另有一条ReferenceHandler线程,关注着这个pending list,如果看到有对象类型是Cleaner,就会执行它的clean(); 如果是其他类型就放入ReferenceQueue中,

这样应用的代码可以从Queue里拖出这些理论上已死的对象,这是一种比finalizer更轻量更好的机制。

参考:

江南白衣: 堆外内存

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值