System.gc()和-XX:+DisableExplicitGC啟動參數,以及DirectByteBuffer的內存釋放


首先我們修改下JVM的啟動參數,重新運行之前博客中的代碼。JVM啟動參數和測試代碼如下:


-verbose:gc -XX:+PrintGCDetails -XX:+DisableExplicitGC -XX:MaxDirectMemorySize=40M
import java.nio.ByteBuffer;

public class TestDirectByteBuffer
{
// -verbose:gc -XX:+PrintGCDetails -XX:MaxDirectMemorySize=40M
// 加上-XX:+DisableExplicitGC,也會報OOM(Direct buffer memory)
public static void main(String[] args) throws Exception
{
while (true)
{
ByteBuffer.allocateDirect(10 * 1024 * 1024);
}
}
}
與之前的JVM啟動參數相比,增加了-XX:+DisableExplicitGC,這個參數作用是禁止代碼中顯示調用GC。代碼如何顯示調用GC呢,通過System.gc()函數調用。如果加上了這個JVM啟動參數,那么代碼中調用System.gc()沒有任何效果,相當於是沒有這行代碼一樣。

Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:632)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:97)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:288)
at direct.TestDirectByteBuffer.main(TestDirectByteBuffer.java:13)
Heap
PSYoungGen total 9536K, used 507K [0x1cf90000, 0x1da30000, 0x27a30000)
eden space 8192K, 6% used [0x1cf90000,0x1d00ef30,0x1d790000)
from space 1344K, 0% used [0x1d8e0000,0x1d8e0000,0x1da30000)
to space 1344K, 0% used [0x1d790000,0x1d790000,0x1d8e0000)
PSOldGen total 21888K, used 0K [0x07a30000, 0x08f90000, 0x1cf90000)
object space 21888K, 0% used [0x07a30000,0x07a30000,0x08f90000)
PSPermGen total 16384K, used 2292K [0x03a30000, 0x04a30000, 0x07a30000)
object space 16384K, 13% used [0x03a30000,0x03c6d380,0x04a30000)
顯然堆內存(包括新生代和老年代)內存很充足,但是堆外內存溢出了。也就是說NIO直接內存的回收,需要依賴於System.gc()。如果我們的應用中使用了java nio中的direct memory,那么使用-XX:+DisableExplicitGC一定要小心,存在潛在的內存泄露風險。


我們知道java代碼無法強制JVM何時進行垃圾回收,也就是說垃圾回收這個動作的觸發,完全由JVM自己控制,它會挑選合適的時機回收堆內存中的無用java對象。代碼中顯示調用System.gc(),只是建議JVM進行垃圾回收,但是到底會不會執行垃圾回收是不確定的,可能會進行垃圾回收,也可能不會。什么時候才是合適的時機呢?一般來說是,系統比較空閑的時候(比如JVM中活動的線程很少的時候),還有就是內存不足,不得不進行垃圾回收。我們例子中的根本矛盾在於:堆內存由JVM自己管理,堆外內存必須要由我們自己釋放;堆內存的消耗速度遠遠小於堆外內存的消耗,但要命的是必須先釋放堆內存中的對象,才能釋放堆外內存,但是我們又不能強制JVM釋放堆內存。

 

下面我們看下new DirectByteBuffer的源碼


DirectByteBuffer(int cap)
{

super(-1, 0, cap, cap, false);
Bits.reserveMemory(cap);
int ps = Bits.pageSize();
long base = 0;
try {
base = unsafe.allocateMemory(cap + ps);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(cap);
throw x;
}
unsafe.setMemory(base, cap + ps, (byte) 0);
if (base % ps != 0) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
cleaner = Cleaner.create(this, new Deallocator(base, cap));
viewedBuffer = null;
}
static void reserveMemory(long size)
{

synchronized (Bits.class) {
if (!memoryLimitSet && VM.isBooted()) {
maxMemory = VM.maxDirectMemory();
memoryLimitSet = true;
}
if (size <= maxMemory - reservedMemory) {
reservedMemory += size;
return;
}
}

// 顯示調用垃圾回收
System.gc();
try {
Thread.sleep(100);
} catch (InterruptedException x) {
// Restore interrupt status
Thread.currentThread().interrupt();
}
synchronized (Bits.class) {
if (reservedMemory + size > maxMemory)
throw new OutOfMemoryError("Direct buffer memory");
reservedMemory += size;
}

}
可以看到:每次執行代碼ByteBuffer.allocateDirect(10 * 1024 * 1024);的時候,都會調用一次System.gc()。目的很簡單,就是希望JVM趕緊把堆中的無用對象回收掉。雖然System.gc()只是建議JVM進行垃圾回收,不能強制。可以這里理解:如此頻繁的建議JVM進行垃圾回收,就算堆內存還很充足,JVM也不能對我們顯示的GC視而不見啊。所以顯示的使用System.gc(),還是有用的。也就是說direct memory的釋放,依賴於System.gc()觸發JVM的垃圾回收動作,只有回收了堆內存中的DirectByteBuffer對象,才有可能回收DirectByteBuffer對象中占用的堆外內存空間。


回想下java中使用堆外內存,關於內存回收需要注意的事和沒有解決的遺留問題 這篇博客中的第4節 正確釋放Unsafe分配的堆外內存

我們在RevisedObjectInHeap類中


// 讓對象占用堆內存,觸發[Full GC
private byte[] bytes = null;

public RevisedObjectInHeap()
{
address = unsafe.allocateMemory(2 * 1024 * 1024);

// 占用堆內存
bytes = new byte[1024 * 1024];
}
定義了1M的字節數組,就是為了讓JVM趕緊進行垃圾回收,這樣當堆內存中的垃圾對象被回收的時候,JVM就能夠調用finalize()方法,就能夠釋放堆外內存。這跟NIO類庫中,顯示調用System.gc()目的是一樣的。至此我們可以得出:堆內存和非堆內存資源(文件句柄、socket句柄,堆外內存、數據庫連接等)的同步釋放,的確是一個很棘手的問題。雖然通過System.gc()能夠避免內存泄露,但是嚴重影響系統的運行效率,因為垃圾回收會減慢系統的運行。最佳編程實踐是:暴露出釋放資源的接口,程序員使用完成后,顯示釋放,這樣就能夠避免堆內存和非堆內存資源的同步釋放的難題。


RevisedObjectInHeap類中通過finalize()方法來釋放堆外內存的,閱讀源碼可以發現,NIO中direct memory的釋放並不是通過finalize(),而是通過sun.misc.Cleaner實現的

cleaner = Cleaner.create(this, new Deallocator(base, cap));


為什么不用finalize呢?因為finalize不安全,也非常影響性能。什么是sun.misc.Cleaner?這是個幽靈引用PhantomReference。后續博客將繼續分析finalize和Cleaner等垃圾回收相關的知識,歡迎關注。
---------------------
作者:aitangyong
來源:CSDN
原文:https://blog.csdn.net/aitangyong/article/details/39403031
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM