生活
这是我们这个行业的一件咄咄怪事:我们不仅不从错误中学习,我们也不从成功中学习。
今天来研究下DirectByteBuffer,本来要在IO/NIO的部分才会研究到,不过昨天讲到了虚引用,所以觉得还是有必要先行了解一下,加深印象。
虚引用
先来复习下昨天的虚引用:
被虚引用引用到的对象对其生命周期没有任何作用,与没有虚引用的对象是一样的,随时可能被JVM回收掉。
只是虚引用必须与一个引用队列相关联,当这个虚引用要被JVM回收时,就会进入引用队列,当在引用队列发现这个虚引用,也就知道这个引用相关联的对象要被回收了,在此之前会做一些操作。
堆外内存
堆外内存:
与堆外内存相反的就是堆内存中,我们创建的对象都是在堆内内存中的,也就是受JVM的管控的,在GC的过程中可能会被JVM回收。But堆外内存,指的时JVM堆以外的内存,这部分内存并不受JVM管控,也就不能被JVM直接回收了。
什么是DirectByteBuffer?
DirectByteBuffer顾名思义直接缓冲,我们可以使用它进行堆外内存的分配/使用/回收。
DirectByteBuffer 申请
//使用如下代码申请
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
//调用到 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 {
//调用native方法分配堆外内存,并返回基地址
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;
}
//清理器创建,细节下面说
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
DirectByteBuffer 回收
当直接内存使用完毕,DirectByteBuffer是可以被JVM回收的,但是通过它分配到的堆外内存却不能被JVM回收。那怎么办呢,如果有大量的堆外内存回收不了,就会造成内存泄漏。
那么这块直接内存是如何被回收的呢?
从清理器Cleaner入手:
1、从Cleaner的实现得知它是一个虚引用,在从虚引用的定义得知,虚引用必须与一个引用队列相关联,在jvm回收一个对象,发现它有虚引用时,就会把这个对象放入到引用队列中,做一系列操作。
private Cleaner(Object var1, Runnable var2) {
super(var1, dummyQueue);
this.thunk = var2;
}
public static Cleaner create(Object var0, Runnable var1) {
return var1 == null ? null : add(new Cleaner(var0, var1));
}
2、虚引用的基类启动了一个守护线程,当发现引用队列里有Cleaner对象时,执行clean方法
//清理时 执行trunk的run方法
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;
}
});
}
}
}
//执行到 Deallocator run方法来释放堆外内存
public void run() {
if (address == 0) {
// Paranoia
return;
}
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
什么时候用到它?
进行文件复制时
使用它的优点?
1、减少了GC [GC会暂停其他操作]
2、加快了复制的速度
DirectByteBuffer 可能会出现什么问题?
1、在6u32前的版本里,CMS GC有bug会导致可能回收不掉
2、DirectByteBuffer晋升到年老代,必须要等到full gc 才有可能被回收。当DirectByteBuffer使用较多且存活时间过长的情况时,很有可能造成堆外内存OOM
如何判断是 DirectByteBuffer ?
当堆外内存占用较多时,可以尝试强制执行Full Gc看堆外内存是否有下降,如果有下降就很有可能时DirectByteBuffer的原因了。
最好是在启动参数上增加-XX:MaxDirectMemorySize=x[m|g],例如-XX:MaxDirectMemorySize=500m
最大分配的堆外内存500M,超过这个范围就是OOM