java的堆內存是如何被回收的

堆外內存

JVM啟動時分配的內存,稱為堆內存,與之相對的,在代碼中還可以使用堆外內存,比如Netty,廣泛使用了堆外內存,但是這部分的內存並不歸JVM管理,GC算法並不會對它們進行回收,所以在使用堆外內存時,要格外小心,防止內存一直得不到釋放,造成線上故障。

堆外內存的申請和釋放

JDK的 <code>ByteBuffer/<code>類提供了一個接口 <code>allocateDirect(intcapacity)/<code>進行堆外內存的申請,底層通過 <code>unsafe.allocateMemory(size)/<code>實現,接下去看看在JVM層面是如何實現的。

可以發現,最底層是通過 <code>malloc/<code>方法申請的,但是這塊內存需要進行手動釋放,JVM並不會進行回收,幸好 <code>Unsafe/<code>提供了另一個接口 <code>freeMemory/<code>可以對申請的堆外內存進行釋放。

堆外內存的回收機制

如果每次申請堆外內存,都需要在代碼中顯示的釋放,對於Java這門語言的設計來說,顯然不夠合理,既然JVM不會管理這些堆外內存,它們是如何回收的呢?

DirectByteBuffer

JDK中使用 <code>DirectByteBuffer/<code>對象來表示堆外內存,每個 <code>DirectByteBuffer/<code>對象在初始化時,都會創建一個對應的 <code>Cleaner/<code>對象,這個 <code>Cleaner/<code>對象會在合適的時候執行 <code>unsafe.freeMemory(address)/<code>,從而回收這塊堆外內存。

當初始化一塊堆外內存時,對象的引用關係如下:

其中 <code>first/<code>是 <code>Cleaner/<code>類的靜態變量, <code>Cleaner/<code>對象在初始化時會被添加到 <code>Clener/<code>鏈表中,和<code>first/<code>形成引用關係, <code>ReferenceQueue/<code>是用來保存需要回收的 <code>Cleaner/<code>對象。

如果該 <code>DirectByteBuffer/<code>對象在一次GC中被回收了

此時,只有 <code>Cleaner/<code>對象唯一保存了堆外內存的數據(開始地址、大小和容量),在下一次FGC時,把該 <code>Cleaner/<code>對象放入到 <code>ReferenceQueue/<code>中,並觸發 <code>clean/<code>方法。

<code>Cleaner/<code>對象的 <code>clean/<code>方法主要有兩個作用:

1、把自身從 <code>Clener/<code>鏈表刪除,從而在下次GC時能夠被回收

2、釋放堆外內存

public void run() { if (address == 0) { // Paranoia return; } unsafe.freeMemory(address); address = 0; Bits.unreserveMemory(size, capacity);}

如果JVM一直沒有執行FGC的話,無效的 <code>Cleaner/<code>對象就無法放入到ReferenceQueue中,從而堆外內存也一直得不到釋放,內存豈不是會爆?

其實在初始化 <code>DirectByteBuffer/<code>對象時,如果當前堆外內存的條件很苛刻時,會主動調用 <code>System.gc()/<code>強制執行FGC。

不過很多線上環境的JVM參數有 <code>-XX:+DisableExplicitGC/<code>,導致了 <code>System.gc()/<code>等於一個空函數,根本不會觸發FGC,這一點在使用Netty框架時需要注意是否會出問題。