占小狼
轉載請注明原創出處,謝謝!
堆外記憶體
JVM啟動時配置設定的記憶體,稱為堆記憶體,與之相對的,在代碼中還可以使用堆外記憶體,比如Netty,廣泛使用了堆外記憶體,但是這部分的記憶體并不歸JVM管理,GC算法并不會對它們進行回收,是以在使用堆外記憶體時,要格外小心,防止記憶體一直得不到釋放,造成線上故障。
堆外記憶體的申請和釋放
JDK的ByteBuffer類提供了一個接口allocateDirect(int capacity)進行堆外記憶體的申請,底層通過unsafe.allocateMemory(size)實作,接下去看看在JVM層面是如何實作的。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI1cjM4QzMmBjZjVzMvwFcvwVbvNmL1h2cuFWaq5yd3d3Lc9CX6MHc0RHaiojIsJye.jpg)
可以發現,最底層是通過malloc方法申請的,但是這塊記憶體需要進行手動釋放,JVM并不會進行回收,幸好Unsafe提供了另一個接口freeMemory可以對申請的堆外記憶體進行釋放。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI1cjM4QzMmBjZjVzMvwFcvwVbvNmL1h2cuFWaq5yd3d3Lc9CX6MHc0RHaiojIsJye.jpg)
堆外記憶體的回收機制
如果每次申請堆外記憶體,都需要在代碼中顯示的釋放,對于Java這門語言的設計來說,顯然不夠合理,既然JVM不會管理這些堆外記憶體,它們是如何回收的呢?
DirectByteBuffer
JDK中使用DirectByteBuffer對象來表示堆外記憶體,每個DirectByteBuffer對象在初始化時,都會建立一個對用的Cleaner對象,這個Cleaner對象會在合适的時候執行unsafe.freeMemory(address),進而回收這塊堆外記憶體。
當初始化一塊堆外記憶體時,對象的引用關系如下:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI1cjM4QzMmBjZjVzMvwFcvwVbvNmL1h2cuFWaq5yd3d3Lc9CX6MHc0RHaiojIsJye.jpg)
其中first是Cleaner類的靜态變量,Cleaner對象在初始化時會被添加到Clener連結清單中,和first形成引用關系,ReferenceQueue是用來儲存需要回收的Cleaner對象。
如果該DirectByteBuffer對象在一次GC中被回收了
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI1cjM4QzMmBjZjVzMvwFcvwVbvNmL1h2cuFWaq5yd3d3Lc9CX6MHc0RHaiojIsJye.jpg)
此時,隻有Cleaner對象唯一儲存了堆外記憶體的資料(開始位址、大小和容量),在下一次FGC時,把該Cleaner對象放入到ReferenceQueue中,并觸發clean方法。
Cleaner對象的clean方法主要有兩個作用:
1、把自身從Clener連結清單删除,進而在下次GC時能夠被回收
2、釋放堆外記憶體
public void run() {
if (address == 0) {
// Paranoia
return;
}
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
如果JVM一直沒有執行FGC的話,無效的Cleaner對象就無法放入到ReferenceQueue中,進而堆外記憶體也一直得不到釋放,記憶體豈不是會爆?
其實在初始化DirectByteBuffer對象時,如果目前堆外記憶體的條件很苛刻時,會主動調用System.gc()強制執行FGC。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI1cjM4QzMmBjZjVzMvwFcvwVbvNmL1h2cuFWaq5yd3d3Lc9CX6MHc0RHaiojIsJye.jpg)
不過很多線上環境的JVM參數有-XX:+DisableExplicitGC,導緻了System.gc()等于一個空函數,根本不會觸發FGC,這一點在使用Netty架構時需要注意是否會出問題。