前些日子遇到了一個堆外內存泄漏的問題,由於對Java的堆外內存不熟悉,排查了好久,因而這兩天特地研究了一下Java的堆外內存的分配與釋放原理。java
在我目前的接觸到的Java中分配直接內存主要有以下三種方式,這篇文章也主要就這三種方式作一些分析:c++
Java提供了Unsafe類用來進行直接內存的分配與釋放數據結構
public native long allocateMemory(long var1);
public native void freeMemory(long var1);
複製代碼
示例ide
public class DirectMemoryMain {
public static void main(String[] args) throws InterruptedException {
Unsafe unsafe = getUnsafe();
while (true) {
for (int i = 0; i < 10000; i++) {
long address = unsafe.allocateMemory(10000);
// System.out.println(address);
// unsafe.freeMemory(address);
}
Thread.sleep(1);
}
}
// Unsafe沒法直接使用,須要經過反射來獲取
private static Unsafe getUnsafe() {
try {
Class clazz = Unsafe.class;
Field field = clazz.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
} catch (IllegalAccessException | NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
}
複製代碼
下面爲這段代碼的演示效果,其中JVM最大內存設爲64M,而真實內存則能夠無限增加。ui
雖然Unsafe能夠經過反射調用來進行內存分配,可是按照其設計方式,它並非給開發者來使用的,並且Unsafe裏面的方法也十分原始,更像是一個底層設施。而其上層的封裝則是DirectByteBuffer,這個纔是最終留給開發者使用的。DirectByteBuffer的分配是經過ByteBuffer.allocateDirect(int capacity)
方法來實現的。this
DirectByteBuffer申請內存的源碼以下:spa
DirectByteBuffer(int cap) {
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)) {
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
// 建立Cleaner
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
複製代碼
整個DirectByteBuffer分配過程當中,比較須要關注的Bits.reserveMemory()和Cleaner,Deallocator,其中Bits.reserveMemory()與分配相關,Cleaner、Deallocator則與內存釋放相關。設計
static void reserveMemory(long size, int cap) {
// 初始化maxMemory,就是使用-XX:MaxDirectMemorySize指定的最大直接內存大小
if (!memoryLimitSet && VM.isBooted()) {
maxMemory = VM.maxDirectMemory();
memoryLimitSet = true;
}
// 第一次先採起最樂觀的方式直接嘗試告訴Bits要分配內存
if (tryReserveMemory(size, cap)) {
return;
}
final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
// 嘗試執行Cleaner來釋放直接內存,直到內存空間足夠
while (jlra.tryHandlePendingReference()) {
if (tryReserveMemory(size, cap)) {
return;
}
}
// GC
System.gc();
// 按照1ms,2ms,4ms,...,256ms的等待間隔嘗試9次分配內存
boolean interrupted = false;
try {
long sleepTime = 1;
int sleeps = 0;
while (true) {
if (tryReserveMemory(size, cap)) {
return;
}
if (sleeps >= MAX_SLEEPS) {
break;
}
if (!jlra.tryHandlePendingReference()) {
try {
Thread.sleep(sleepTime);
sleepTime <<= 1;
sleeps++;
} catch (InterruptedException e) {
interrupted = true;
}
}
}
throw new OutOfMemoryError("Direct buffer memory");
} finally {
if (interrupted) {
Thread.currentThread().interrupt();
}
}
}
複製代碼
// -XX:MaxDirectMemorySize限制的是總cap,而不是真實的內存使用量,(在頁對齊的狀況下,真實內存使用量和總cap是不一樣的)
private static boolean tryReserveMemory(long size, int cap) {
long totalCap;
while (cap <= maxMemory - (totalCap = totalCapacity.get())) {
if (totalCapacity.compareAndSet(totalCap, totalCap + cap)) {
reservedMemory.addAndGet(size);
count.incrementAndGet();
return true;
}
}
return false;
}
複製代碼
內存釋放是經過Cleaner和Deallocator來實現的。指針
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);
}
}
複製代碼
這個類中主要方法爲run(),裏面的步驟也很簡單,包含兩步code
每一個DirectByteBuffer都有一個相對應的Deallocator,而Deallocator則是由Cleaner來進行調度。
Cleaner的數據結構爲一個雙向鏈表,以下
private static Cleaner first = null; // 鏈表的頭節點
private Cleaner next = null; // 下一個節點
private Cleaner prev = null; // 上一個節點
private final Runnable thunk; // 存放Deallocator
複製代碼
Cleaner中主要包含以下操做,add, remove,clean
1. add
private static synchronized Cleaner add(Cleaner var0) {
if (first != null) {
var0.next = first;
first.prev = var0;
}
first = var0;
return var0;
}
複製代碼
add操做就是不斷地將新的Cleaner節點添加在鏈表頭部,以後將頭節點指針指向新的Cleaner
2. remove
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;
}
}
複製代碼
remove操做就是將Cleaner節點從鏈表中刪除
3. clean
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;
}
});
}
}
}
複製代碼
clean操做則是移除Cleaner節點並調用Deallocator的run()方法
疑問 Cleaner.clean()又是由誰在什麼時候調用的呢?
仔細觀察能夠發現,Cleaner繼承了PhantomReference,其referent爲DirectByteBuffer
在Reference初次加載的過程當中會調用一段靜態代碼
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread handler = new ReferenceHandler(tg, "Reference Handler");
handler.setPriority(Thread.MAX_PRIORITY);
handler.setDaemon(true);
handler.start();
// provide access in SharedSecrets
SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
@Override
public boolean tryHandlePendingReference() {
return tryHandlePending(false);
}
});
}
複製代碼
這段代碼中包含了兩種能夠調用Cleaner的方式:
事實上直接內存的回收過程也的確是由這兩種方式混合組成,這兩種方式有一個共同點,他們都會調用Reference.tryHandlePending()方法。
static boolean tryHandlePending(boolean waitForNotify) {
Reference<Object> r;
Cleaner c;
try {
synchronized (lock) {
if (pending != null) {
r = pending;
c = r instanceof Cleaner ? (Cleaner) r : null;
pending = r.discovered;
r.discovered = null;
} else {
if (waitForNotify) {
lock.wait();
}
return waitForNotify;
}
}
} catch (OutOfMemoryError x) {
Thread.yield();
return true;
} catch (InterruptedException x) {
return true;
}
if (c != null) {
c.clean();
return true;
}
ReferenceQueue<? super Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}
複製代碼
其中pending和discovered由JVM來操做,兩個共同組成一個等待隊列鏈表,對於PhantomReference的狀況,當對象不存在其餘引用,便會直接加入等待隊列。每當等待隊列中出現Cleaner,就會執行其clean()方法。
1. 整個DirectByteBuffer的分配與釋放流程以下
2. -XX:MaxDirectMemorySize參數只對由DirectByteBuffer分配的內存有效,對Unsafe直接分配的內存無效
疑問 native方法中分配的內存是不是屬於DirectByteBuffer對象呢?
這個疑問來自於一次內存泄漏問題的排查,一直沒有機會去研究,正好藉此次機會尋找一下該問題的答案。
寫了一個簡單的demo程序以下
// java部分
public class NativeMain {
public native void allocateMemory();
static {
System.setProperty("java.library.path", ".");
System.loadLibrary("nativemain");
}
public static void main(String[] args) throws Exception {
NativeMain nativeMain = new NativeMain();
while (true) {
for (int i = 0; i < 10000; i++) {
nativeMain.allocateMemory();
}
Thread.sleep(1);
}
}
}
複製代碼
// c++實現部分
#include "jni.h"
#include "NativeMain.h"
#include <stdlib.h>
JNIEXPORT void JNICALL Java_NativeMain_allocateMemory(JNIEnv *, jobject) {
char *ptr = (char*)malloc(1000);
}
複製代碼
運行發現native方法分配的內存並不會產生DirectByteBuffer對象,一樣的也不受-XX:MaxDirectMemorySize影響。