堆外内存
简介
堆外内存也称为非堆内存(
Non-Heap Memory
)或者直接内存(Direct Memory
),位于Java虚拟机管控之外,受操作系统管理。Netty和NIO中广泛的使用了堆外内存,这部分内存如果得不到释放,容易造成堆外内存溢出,导致系统故障。
申请和释放
堆外内存申请
ByteBuffer
提供了静态方法allocateDirect(int capacity)
来分配堆外内存,其底层使用Unsafe.allocateMemory(long var1)
。
ByteBuffer.allocateDirect(int capacity)
方法源码:
1
2
3
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
DirectByteBuffer
构造函数
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
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 = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
Bits.reserveMemory()
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
class Bits {
static void reserveMemory(long size, int cap) {
if (!MEMORY_LIMIT_SET && VM.initLevel() >= 1) {
//获取JVM设置的最大直接内存
MAX_MEMORY = VM.maxDirectMemory();
MEMORY_LIMIT_SET = true;
}
// optimist!,乐观主义,尝试直接分配
if (tryReserveMemory(size, cap)) {
return;
}
final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
boolean interrupted = false;
try {
// Retry allocation until success or there are no more
// references (including Cleaners that might free direct
// buffer memory) to process and allocation still fails.
//重试分配直到成功或者没有更多引用需要处理,分配仍然失败
boolean refprocActive;
do {
try {
refprocActive = jlra.waitForReferenceProcessing();
} catch (InterruptedException e) {
// Defer interrupts and keep trying.
interrupted = true;
refprocActive = true;
}
if (tryReserveMemory(size, cap)) {
return;
}
} while (refprocActive);
// trigger VM's Reference processing
//显示调用使JVM进行full GC
System.gc();
// A retry loop with exponential back-off delays.
// Sometimes it would suffice to give up once reference
// processing is complete. But if there are many threads
// competing for memory, this gives more opportunities for
// any given thread to make progress. In particular, this
// seems to be enough for a stress test like
// DirectBufferAllocTest to (usually) succeed, while
// without it that test likely fails. Since failure here
// ends in OOME, there's no need to hurry.
long sleepTime = 1;
int sleeps = 0;
//重试9次然后还是不能分配直接内存抛出直接内存溢出
while (true) {
if (tryReserveMemory(size, cap)) {
return;
}
if (sleeps >= MAX_SLEEPS) {
break;
}
try {
if (!jlra.waitForReferenceProcessing()) {
Thread.sleep(sleepTime);
sleepTime <<= 1;
sleeps++;
}
} catch (InterruptedException e) {
interrupted = true;
}
}
// no luck
throw new OutOfMemoryError("Direct buffer memory");
//中断线程
} finally {
if (interrupted) {
// don't swallow interrupts
Thread.currentThread().interrupt();
}
}
}
}
Bits.tryReserveMemory()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static boolean tryReserveMemory(long size, int cap) {
// -XX:MaxDirectMemorySize limits the total capacity rather than the
// actual memory usage, which will differ when buffers are page
// aligned.
long totalCap;
//如果直接内存容量还要剩余通过CAS方式分配并返回
while (cap <= MAX_MEMORY - (totalCap = TOTAL_CAPACITY.get())) {
if (TOTAL_CAPACITY.compareAndSet(totalCap, totalCap + cap)) {
RESERVED_MEMORY.addAndGet(size);
COUNT.incrementAndGet();
return true;
}
}
return false;
}
Unsafe.allocateMemory(long var1)
方法源码:
1
public native long allocateMemory(long var1);
JVM的unsafe.cpp#Unsafe_AllocateMemory
方法里,通过os::malloc
来实现堆外内存申请。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
UNSAFE_ENTRY(jlong, Unsafe_AllocateMemory(JNIEnv *env, jobject unsafe, jlong size))
UnsafeWrapper("Unsafe_AllocateMemory");
size_t sz = (size_t)size;
if (sz != (julong)size || size < 0) {
THROW_0(vmSymbols::java_lang_IllegalArgumentException());
}
if (sz == 0) {
return 0;
}
sz = round_to(sz, HeapWordSize);
void* x = os::malloc(sz, mtInternal);
if (x == NULL) {
THROW_0(vmSymbols::java_lang_OutOfMemoryError());
}
//Copy::fill_to_words((HeapWord*)x, sz / HeapWordSize);
return addr_to_java(x);
UNSAFE_END
堆外内存释放
Unsafe.freeMemory(long var1)
方法源码:
1
public native void freeMemory(long var1);
JVM的unsafe.cpp#Unsafe_FreeMemory
方法里,通过os::free
来实现堆外内存释放。
1
2
3
4
5
6
7
8
UNSAFE_ENTRY(void, Unsafe_FreeMemory(JNIEnv *env, jobject unsafe, jlong addr))
UnsafeWrapper("Unsafe_FreeMemory");
void* p = addr_from_java(addr);
if (p == NULL) {
return;
}
os::free(p);
UNSAFE_END
Bits.unreserveMemory()
1
2
3
4
5
6
static void unreserveMemory(long size, int cap) {
long cnt = COUNT.decrementAndGet();
long reservedMem = RESERVED_MEMORY.addAndGet(-size);
long totalCap = TOTAL_CAPACITY.addAndGet(-cap);
assert cnt >= 0 && reservedMem >= 0 && totalCap >= 0;
}
回收机制
JDK中使用DirectByteBuffer
对象来表示堆外内存,每个DirectByteBuffer
对象在初始化时,都会创建一个对象的Cleaner
对象,这个Cleaner
对象会在合适的时候执行unsafe.freeMemory(address)
,从而回收这块堆外内存。
其中first
是Cleaner
类的静态变量,Cleaner
对象在初始化时会被添加到Clener
链表中,和first
形成引用关系,ReferenceQueue
是用来保存需要回收的Cleaner
对象。
如果该DirectByteBuffer
对象在一次GC中被回收了。只有Cleaner
对象唯一保存了堆外内存的数据(开始地址、大小和容量),在下一次GC时,把该Cleaner
对象放入到ReferenceQueue
中,并触发clean
方法。clean
方法把自身从链表删除,等候GC回收;释放堆外内存。
DirectByteBuffer
内部类
Deallocator
是DirectByteBuffer
的内部类,实现了Runnable
,复写run方法,调用Unsafe.freeMemory
实现释放堆外内存。
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);
}
}
构造方法
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
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 {
//调用Unsafe.allocateMemory方法分配堆外内存
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.clean()执行时会调用Deallocator.run()方法
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
Cleaner
Cleaner是PhantomReference(虚引用)的子类。
当GC时发现虚引用包装的DirectByteBuffer
已经被清除掉,就会把Cleaner
放进 Reference类pending list
静态变量里。然后另有一条ReferenceHandler
线程,名字叫 "Reference Handler"
的,关注着这个pending list
,如果看到有对象类型是Cleaner
,就会执行它的clean()
。
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public class Cleaner extends PhantomReference<Object> {
//用于保存虚引用的队列
private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue();
private static Cleaner first = null;
private Cleaner next = null;
private Cleaner prev = null;
private final Runnable thunk;
//链表新增节点
private static synchronized Cleaner add(Cleaner var0) {
if (first != null) {
var0.next = first;
first.prev = var0;
}
first = var0;
return var0;
}
//链表删除节点
private static synchronized boolean remove(Cleaner var0) {
if (var0.next == var0) {
return false;
} else {
if (first == var0) {
if (var0.next != null) {
first = var0.next;
} else {
first = var0.prev;
}
}
if (var0.next != null) {
var0.next.prev = var0.prev;
}
if (var0.prev != null) {
var0.prev.next = var0.next;
}
var0.next = var0;
var0.prev = var0;
return true;
}
}
//构造方法
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));
}
//清除方法
public void clean() {
//将自身从链表清除
if (remove(this)) {
try {
//调用Deallocator.run方法,释放堆外内存
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static class Deallocator implements Runnable{
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);
}
}
堆外内存GC
-XX:MaxDirectMemorySize=40M
:设置堆外内存大小- 在使用Netty等会产生堆外内存的框架时需要禁止使用
-XX:+DisableExplicitGC
参数,它导致了System.gc()等于一个空函数,根本不会触发FGC。 - 使用CMS垃圾回收器时,可以设置
-XX:ExplicitGCInvokesConcurrent
参数,使单线程FGC,变为CMS GC。