DirectByteBuffer

堆外内存
  堆外内存是相对于堆内内存的一个概念。堆内内存是由JVM所管控的Java进程内存,我们平时在Java中创建的对象都处于堆内内存中,并且它们遵循JVM的内存管理机制,JVM会采用垃圾回收机制统一管理它们的内存。那么堆外内存就是存在于JVM管控之外的一块内存区域,因此它是不受JVM的管控。
  可以使用ByteBuffer等类来操纵堆外内存了,使用ByteBuffer分配本地内存则非常简单,直接:

1
ByteBuffer buffer = ByteBuffer.allocateDirect(10 * 1024 * 1024);

  可以通过指定JVM参数来确定堆外内存大小限制:-XX:MaxDirectMemorySize=512m
  对于这种direct buffer内存不够的时候会抛出错误: java.lang.OutOfMemoryError: Direct buffer memory
  堆外内存泄露的问题定位通常比较麻烦,可以借助google-perftools这个工具,它可以输出不同方法申请堆外内存的数量。

 

  使用ByteBuffer堆外内存时,必须要由我们自己释放;必须先释放堆内存中的对象(引用),才能释放堆外内存,但是我们又不能强制JVM释放堆内存。例如:ByteBuffer bb = ByteBuffer.allocateDirect(1024),这段代码的执行会在堆外占用1k的内存,Java堆内只会占用一个对象的指针引用的大小,堆外的这1k的空间只有当bb对象被回收时,才会被回收,这里会发现一个明显的不对称现象,就是堆外可能占用了很多,而堆内没占用多少,导致还没触发GC,那就很容易出现Direct Memory造成物理内存耗光。
  再者,假设堆内 ByteBuffer对象的引用升级到了老年代,导致这个引用会长期存在无法回收,这时堆外的内存将长期无法得到回收。

堆外内存分配
  堆外内存申请:ByteBuffer.allocateDirect(cap);
  进行内存申请的时候,会调用:DirectByteBuffer(int cap)构造函数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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);
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对象用于跟踪DirectByteBuffer对象的垃圾回收
//以实现当DirectByteBuffer被垃圾回收时,堆外内存也会被释放
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// These methods should be called whenever direct memory is allocated or
// freed. They allow the user to control the amount of direct memory
// which a process may access. All sizes are specified in bytes.
static void reserveMemory(long size, int cap) {
synchronized (Bits.class) {
// 获取最大可以申请的对外内存大小,默认值是64MB
// 可以通过参数-XX:MaxDirectMemorySize=<size>设置这个大小
if (!memoryLimitSet && VM.isBooted()) {
maxMemory = VM.maxDirectMemory();
memoryLimitSet = true;
}

// -XX:MaxDirectMemorySize限制的是用户申请的大小,而不考虑对齐情况
// 所以使用两个变量来统计:
// reservedMemory:真实的目前保留的空间
// totalCapacity:目前用户申请的空间
if (cap <= maxMemory - totalCapacity) {
reservedMemory += size;
totalCapacity += cap;
count++;
return; // 如果空间足够,更新统计变量后直接返回
}
}
// 如果已经没有足够空间,则尝试GC
System.gc();
try {
Thread.sleep(100);
} catch (InterruptedException x) {
// Restore interrupt status
Thread.currentThread().interrupt();
}
synchronized (Bits.class) {
// GC后再次判断,如果还是没有足够空间,则抛出OOME
if (totalCapacity + cap > maxMemory)
throw new OutOfMemoryError("Direct buffer memory");
reservedMemory += size;
totalCapacity += cap;
count++;
}
}

  Bits.reserveMemory用于在系统中保存总分配内存(按页分配)的大小和实际内存的大小。该方法不管是直接内存的分配还是释放都会被调用,会检查直接内存的大小。如果空间不足,会调用System.gc()尝试释放内存,然后再进行判断,如果还是没有足够的空间,抛出OOME。System.gc()会触发一个FULL GC,当然前提是没有显示的设置-XX:+DisableExplicitGC来禁用显式GC。
  最后,DirectByteBuffer使用Cleaner机制进行空间回收。

  注意,这里之所以用使用FULL GC的很重要的一个原因是:System.gc()会对新生代的老生代都会进行内存回收,这样会比较彻底地回收DirectByteBuffer对象以及它们关联的堆外内存。
  sun.misc.Unsafe.allocateMemory这个函数是通过JNI调用C的malloc来申请内存;
申请内存时,可以通过-XX:+PageAlignDirectMemory指定申请的内存是否需要按页对齐,默认不对其;

堆外内存回收
  Cleaner是PhantomReference的子类,并通过自身的next和prev字段维护的一个双向链表。PhantomReference的作用在于跟踪垃圾回收过程,并不会对对象的垃圾回收过程造成任何的影响。
  所以cleaner = Cleaner.create(this, new Deallocator(base, size, cap))用于对当前构造的DirectByteBuffer对象的垃圾回收过程进行跟踪。
  当DirectByteBuffer对象从pending状态变为 enqueue状态时,会触发Cleaner的clean(),而Cleaner的clean()的方法会实现通过unsafe对堆外内存的释放。也可以直接调用directByteBuffer.cleaner().clean()来主动释放:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Cleaner.java
public void clean() {
if(remove(this)) {
try {
this.thunk.run();
} catch (final Throwable var2) {
AccessController.doPrivileged(new PrivilegedAction() {
public Void run() {
if(System.err != null) {
(new Error("Cleaner terminated abnormally", var2)).printStackTrace();
}

System.exit(1);
return null;
}
});
}


}
}

这里的thunk即DirectByteBuffer构造函数中指定的Deallocator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private static class Deallocator
implements Runnable
{
private static Unsafe unsafe = Unsafe.getUnsafe();

private long address;
private long size;
private int capacity;

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) {
// Paranoia
return;
}
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
}

说明:sun.misc.Unsafe.freeMemory方法使用C标准库的free函数释放内存空间。

默认情况下,可以申请的DirectByteBuffer大小为Runtime.getRuntime().maxMemory(),而这个值等于可用的最大Java堆大小,也就是我们-Xmx参数指定的值。

参考:http://www.importnew.com/29817.html

https://blog.csdn.net/liuxiao723846/article/details/89814005

http://www.jianshu.com/p/007052ee3773

主动分配和回收堆外内存:

ByteBuffer byteBuffer = ByteBuffer.allocateDirect();

((DirectBuffer)byteBuffer).cleaner().clean();