热爱美好的事物,是一种追求
介绍
____堆外内存意味着把内存对象分配在 Java 虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机),
作用:能缩短垃圾回收时间
适合生命期中等或较长的对象
特点:
- 对于大内存有良好的伸缩性,可以自行扩展
- 对垃圾回收停顿的改善可以明显感觉到,不影响用户线程
- 在进程间可以共享,减少虚拟机间的复制
如果使用的多,那么你可能会考虑硬盘的速度会有影响
堆外内存的回收
堆外内存的分配很简单,比如如下
ByteBuffer buffer = ByteBuffer.allocateDirect(1 * 1024 * 1024);
或者
Unsafe unsafe = Unsafe.getUnsafe()
unsafe.allocateMemory(2 * 1024 * 1024);
分配知道了,我们知道堆内内存是基于JVM的自动垃圾回收,那么堆外内存呢?
先抛出来,一般FullGC时会回收
不过注意不要使用**-XX:+DisableExplicitGC**,打开则让System.gc()无效,内存无法有效回收,导致OOM
问题:堆内内存充足,堆外内存造成物理内存耗光,仍然没有触发FullGC
NIO方式分配堆外内存的回收
先看第一种方式,
我们知道,JVM垃圾回收要么会在内存不足时回收,要么在空闲时也会回收,JVM自己管理
矛盾点在于:堆内内存还没有达到可以FullGC的点,然而堆外内存已经消耗完毕了,可能会造成OOM,这就很尴尬
所以需要我们手动释放,那怎么手动释放,看下源码 ByteBuffer.allocateDirect()
可以看到都做了mark,position,cap的初始化,
那么其中Bits.reserveMemory(size, cap);:用于在系统中保存总分配内存(按页分配)的大小和实际内存的大小,具体执行中需要首先用 tryReserveMemory 方法来判断系统内存(堆外内存)是否足够;分为多种情况这里暂不细说
分配内存后大家看
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
类中维护了这个定义:private final Cleaner cleaner;,就是它来维护清理相关,Deallocator是一个内部类,是用来执行清理动作的,如下
可以看到实现了Runnable接口,调用了unsafe.freeMemory(address),释放内存,那是如何触发的呢?先往下看 Cleaner.create
private Cleaner(Object var1, Runnable var2) {
super(var1, dummyQueue);
this.thunk = var2;
}
这时候再贴一下刚才的入口
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
this为当前内存块,var1为new Deallocator,里面是清理动作,new Cleaner(var0,var1);
private Cleaner(Object var1, Runnable var2) {
super(var1, dummyQueue);
this.thunk = var2;
}
可以看到 super(var1, dummyQueue); 这个queue给了Reference抽象类的volatile ReferenceQueue<? super T> queue;这个后面再说
var2赋值给了thunk,它是什么,如下
那thunk是怎么用的呢?看下面有个clean方法中 this.thunk.run();
执行了,执行什么???
执行的就是刚才DirectByteBuffer中的Deallocator这个内部类的unsafe.freeMemory(address);清理动作
也就是说我们如果主动调用这个clean就可以了
比如说我们创建了byteBuffer,可以判断调用
if (byteBuffer instanceof DirectBuffer) {
((DirectBuffer)byteBuffer).cleaner().clean();
}
那么话再说Cleaner这个类,queue给了Reference抽象类的volatile ReferenceQueue<? super T> queue;
Cleaner 类继承了 PhantomReference 类,并且在自己的 clean() 方法中启动了清理线程,当 DirectByteBuffer 被 GC 之前 cleaner 对象会被放入一个引用队列(ReferenceQueue),JVM 会启动一个低优先级线程扫描这个队列,并且执行 Cleaner 的 clean 方法来做清理工作。看下源码,
上面初始化Clean的时候会调用父类 Reference
Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
Cleaner的clean方法被Reference的tryHandlePending调用
ReferenceHandler是Reference的内部类
它又在Reference的static块中new了,并且设置后台线程执行
那么现在有个问题,这个static在调用构造器时并没有被触发???也就没有自动清理,这个问题大家看下怎么回事,可以留言
unsafe方式分配内存的手动回收
private long address = 0;
private Unsafe unsafe = Unsafe.getUnsafe();
// 让对象占用堆内存,触发[Full GC
private byte[] bytes = null;
public TestMain() {
address = unsafe.allocateMemory(2 * 1024 * 1024);
bytes = new byte[1024 * 1024];
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize." + bytes.length);
unsafe.freeMemory(address);
}
覆盖了finalize方法,手动释放分配的堆外内存,如果堆中的对象被回收,那么相应的也会释放占用的堆外内存
private byte[] bytes = null;
这行代码主要目的是为了触发堆内存的垃圾回收行为,顺带执行对象的finalize释放堆外内存。如果没有这行代码或者是分配的字节数组比较小,程序运行一段时间后还是会报OutOfMemoryError。这是因为每当创建1个RevisedObjectInHeap对象的时候,占用的堆内存很小(就几十个字节左右),但是却需要占用2M的堆外内存。这样堆内存还很充足(这种情况下不会执行堆内存的垃圾回收),但是堆外内存已经不足,所以就不会报OutOfMemoryError。
此处参考:https://blog.csdn.net/hellozhxy/article/details/102728206