1 什么是堆外内存
Java虚拟机的堆以外的内存叫堆外内存(DirectBuffer),也叫直接内存。
堆外内存与堆内内存(HeapByteBuffer)相对应,对于整个机器内存而言,除堆内内存以外部分即为堆外内存。堆外内存虽然不受JVM管控,但是堆外内存还是在java进程里面的,而不是由系统内核直接管理,所以它还是在java进程里面的。
直接内存大小可以通过MaxDirectMemorySize设置;如果不指定,默认与堆的最大值-Xmx参数值一致
2 堆外内存特点
2.1 使用堆内内存的好处
- 减少内存在Native堆和JVM堆拷贝过程,避免拷贝损耗,降低内存使用
- 改善堆过大时垃圾回收效率,减少停顿。(Full GC时会扫描堆内存,回收效率和堆大小成正比)
直接内存的速度会优于Java堆,即读写性能高。因此处于性能考虑,读写频繁的场合可能会考虑使用直接内存。
Java的NIO库允许Java程序使用直接内存,用于数据缓冲区。
使用NIO时,如图所示,操作系统划出的直接缓存区可以被java代码直接访问,只有一份。NIO适合对大文件的读写操作。
2.2 堆外内存的缺点
- 不受JVM内存回收管理,所以垃圾回收机制不会回收直接内存的空间,需要用户自己释放内存空间
- 堆外内存一旦发生泄漏,比较难排查
- 分配和取消分配的成本通常高于非直接缓冲区
3 堆外内存分配
3.1 NIO类中的ByteBuffer#allocateDirect
DirectByteBuffer用于创建Native缓存区。通过调用ByteBuffer的静态方法allocateDirect方法创建DirectByteBuffer
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
真正分配堆外内存的逻辑还是通过 UNSAFE.allocateMemory(size),allocateMemory 方法返回的是内存地址;
Unsafe#allocateMemory 所分配的内存必须自己手动释放,否则会造成内存泄漏
DirectByteBuffer(int cap) {
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (long)(pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0L;
try {
base = UNSAFE.allocateMemory(size);
} catch (OutOfMemoryError var9) {
Bits.unreserveMemory(size, cap);
throw var9;
}
UNSAFE.setMemory(base, size, (byte)0);
if (pa && base % (long)ps != 0L) {
this.address = base + (long)ps - (base & (long)(ps - 1));
} else {
this.address = base;
}
this.cleaner = Cleaner.create(this, new DirectByteBuffer.Deallocator(base, size, cap));
this.att = null;
}
3.2 FileChannelde#map
通过FileChannel的map方法,可以把文件映射为内存对象,该方法返回MappedByteBuffer对象。
MappedByteBuffer是NIO提供的文件内存映射的实现方案,可以把整个文件或文件段映射到Native堆内存。
# class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer
public abstract MappedByteBuffer map(
MapMode mode,
long position,
long size) throws IOException;
4 堆外内存回收
在DirectByteBuffer创建时,同时创建了一个Cleaner对象。Cleaner继承了PhantomReference,PhantomReference是Reference的子类,所以Cleaner是一个虚引用对象。
4.1 Reference
在分析堆外内存回收之前 ,先了解下Reference。
4.1.1 Reference内部主要的成员
- referent: 表示其引用的对象,即我们在构造的时候需要被包装在其中的对象
- next:即描述当前引用节点所存储的下一个即将被处理的节点。但next仅在放到queue中才会有意义( 因为,只有在enqueue的时候,会将next设置为下一个要处理的Reference对象 )。为了描述相应的状态值,在放到队列当中后,其queue就不会再引用这个队列了。而是引用一个特殊的ENQUEUED。因为已经放到队列当中,并且不会再次放到队列当中.
- queue Reference内部提供2个构造函数,一个带queue,一个不带queue。其中queue的意义在于,我们可以在外部对这个queue进行监控。即如果有对象即将被回收,那么相应的reference对象就会被放到这个queue里.
private T referent; // 引用到的对象
Reference next; //指向下一个;
ReferenceQueue<? super T> queue; //引用队列
4.1.2 Reference有4种状态
- Active
新创建的引用对象都是这个状态。GC 会根据引用对象是否在创建时制定ReferenceQueue参数进行状态转移,如果指定了,那么转移到Pending,如果没指定,转移到Inactive。在这个状态中
//如果构造参数中没指定queue,那么queue为ReferenceQueue.NULL,否则为构造参数中传递过来的queue
queue = ReferenceQueue || ReferenceQueue.NULL
next = null
- Pending
由JVM来赋值的,当Reference内部的referent对象的可达状态改变时,JVM会将Reference对象放入pending链表
//构造参数参数中传递过来的queue
queue = ReferenceQueue
next = 该queue中的下一个引用,如果是该队列中的最后一个,那么为this
- Enqueued
调用ReferenceQueue.enqueued方法后的引用处于这个状态中
queue = ReferenceQueue.ENQUEUED
next = 该queue中的下一个引用,如果是该队列中的最后一个,那么为this
- Inactive
最终状态,通过引用队列的poll方法可以从引用队列中获取引用对象,同时引用对象会从队列中移除,此时引用对象处于Inactive状态,之后会被GC回收。
queue = ReferenceQueue.NULL
next = this
4.1.3 pending列表
pending和 discovered成员是Reference类中重要的两个成员
public abstract class Reference<T> {
// 指向pending列表中的下一个节点
transient private Reference<T> discovered;
// 静态变量pending列表,可以看做是一个链表,pending指向链表的头结点
private static Reference<Object> pending = null;
pending整个jvm中唯一,与discovered结合使用,pending 与discovered构成了一个链表pending列表。
Garbage Collector 回收了referent对象后,会把相应的Reference对象放入pending列表中。
4.1.4 ReferenceHandler线程
Reference-handler线程 把 pending列表中的元素入队到 ReferenceQueue
public abstract class Reference<T> {
// 指向pending列表中的下一个节点
transient private Reference<T> discovered;
// 静态变量pending列表,可以看做是一个链表,pending指向链表的头结点
private static Reference<Object> pending = null;
static boolean tryHandlePending(boolean waitForNotify) {
Reference<Object> r;
Cleaner c;
try {
synchronized (lock) {
// 如果pending不为空
if (pending != null) {
// 获取pending执行的对象
r = pending;
// 如果是Cleaner类型
c = r instanceof Cleaner ? (Cleaner) r : null;
// 将pending指向下一个节点
pending = r.discovered;
// 将discovered置为空
r.discovered = null;
} else {
// 等待
if (waitForNotify) {
lock.wait();
}
return waitForNotify;
}
}
} catch (OutOfMemoryError x) {
Thread.yield();
// retry
return true;
} catch (InterruptedException x) {
// retry
return true;
}
if (c != null) {
// 调用clean方法进行清理
c.clean();
return true;
}
// 获取引用队列
ReferenceQueue<? super Object> q = r.queue;
// 如果队列不为空,将对象加入到引用队列中
if (q != ReferenceQueue.NULL) q.enqueue(r);
// 返回true
return true;
}
}
4.2 Cleaner对象
JDK中使用DirectByteBuffer对象来表示堆外内存,每个DirectByteBuffer对象在初始化时,都会创建一个对应的Cleaner对象。
public class Cleaner extends PhantomReference<Object> {
// ReferenceQueue队列
private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue();
// 静态变量,链表的头结点,创建的Cleaner都会加入到这个链表中
private static Cleaner first = null;
private Cleaner next = null;
private Cleaner prev = null;
private final Runnable thunk;
public static Cleaner create(Object ob, Runnable thunk) {
return thunk == null ? null : add(new Cleaner(ob, thunk));
}
private Cleaner(Object referent, Runnable thunk) {
super(referent, dummyQueue); // 调用父类构造函数,传入引用对象和引用队列
this.thunk = thunk; // thunk指向传入的Deallocator
}
private static synchronized Cleaner add(Cleaner cl) {
if (first != null) {
cl.next = first;
first.prev = cl;
}
first = cl;
return cl;
}
public void clean() {
if (remove(this)) {
try {
this.thunk.run();
} catch (final Throwable var2) {
// ...
}
}
}
}
Cleaner的构造
Cleaner继承了PhantomReference,PhantomReference是Reference的子类,所以Cleaner是一个虚引用对象。
虚引用需要与引用队列结合使用,所以在Cleaner中可以看到有一个ReferenceQueue,它是一个静态的变量(dummyQueue),所以创建的所有Cleaner对象都会共同使用这个引用队列。
在创建Cleaner的create方法中:
- 通过构造函数创建了一个Cleaner对象,构造函数中的referent参数为DirectByteBuffer,thunk参数为Deallocator对象
- 调用add方法将创建的Cleaner对象加入到链表中,新加入的节点放在链表的头部,first成员变量是一个静态变量,它指向链表的头结点,创建的Cleaner都会加入到这个链表中。
Cleaner父类Reference的构造
Cleaner调用父类构造函数时,最终会进入到父类Reference中的构造函数中:
- referent:指向实际的引用对象,上面创建的是DirectByteBuffer,所以这里指向的是DirectByteBuffer。
- queue:引用队列,指向Cleaner中的引用队列dummyQueue。
public class PhantomReference<T> extends Reference<T> {
// ...
public PhantomReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q); // 调用父类构造函数
}
}
public abstract class Reference<T> {
/* 引用对象 */
private T referent;
// 引用队列
volatile ReferenceQueue<? super T> queue;
Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
// 设置引用队列
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
}
4.3 DirectByteBuffer堆外内存回收
在DirectByteBuffer的构造函数中,在申请内存之前,先调用了Bits的reserveMemory方法回收内存,申请内存之后,调用Cleaner的create方法创建了一个Cleaner对象,并传入了当前对象(DirectByteBuffer)和一个Deallocator类型的对象:
DirectByteBuffer(int cap) {
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (long)(pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0L;
try {
// 分配内存
base = UNSAFE.allocateMemory(size);
} catch (OutOfMemoryError var9) {
Bits.unreserveMemory(size, cap);
throw var9;
}
UNSAFE.setMemory(base, size, (byte)0);
if (pa && base % (long)ps != 0L) {
this.address = base + (long)ps - (base & (long)(ps - 1));
} else {
this.address = base;
}
// 创建Cleader,传入了当前对象和Deallocator
this.cleaner = Cleaner.create(this, new DirectByteBuffer.Deallocator(base, size, cap));
this.att = null;
}
Deallocator是DirectByteBuffer的一个内部类,并且实现了Runnable接口,调用unsafe.freeMemory(address),从而回收这块堆外内存:
class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer {
private static class Deallocator implements Runnable {
// ...
private Deallocator(long address, long size, int capacity) {
assert (address != 0);
this.address = address; // 设置内存地址
this.size = size;
this.capacity = capacity;
}
public void run() {
if (address == 0) {
return;
}
// 释放内存
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
}
}
4.4 回收过程
Cleaner对象在初始化时会被添加到Cleaner链表中,和Cleaner类的静态变量(first)形成引用关系。
如果该DirectByteBuffer对象在一次GC中被回收了,此时,只有Cleaner对象唯一保存了堆外内存的数据,在下一次FGC时,把该Cleaner对象放入到pengding列表中。ReferenceHandler线程从pending列表读取元素,并触发clean方法。
Cleaner对象的clean方法主要有两个作用:
- 把自身从Clener链表删除,从而在下次GC时能够被回收
- 释放堆外内存