JVM源碼分析之堆外內存(直接內存)

一、堆外內存定義

  內存對象分配在JVM中堆之外的內存,也能夠稱爲直接內存,這些內存直接受操做系統管理(而不是JVM),這樣作的好處是可以在必定程度上減小垃圾回收對應用程序形成的影響。通常咱們使用Unsafe和NIO包下ByteBuffer來建立堆外內存。java

二、爲何使用堆外內存

  一、減小了垃圾回收算法

  使用堆外內存的話,堆外內存是直接受操做系統管理( 而不是虛擬機 )。這樣作的結果就是能保持一個較小的堆內內存,以減小垃圾收集對應用的影響。框架

  二、提高複製速度(io效率)源碼分析

  堆內內存由JVM管理,屬於「用戶態」;而堆外內存由OS管理,屬於「內核態」。若是從堆內向磁盤寫數據時,數據會被先複製到堆外內存,即內核緩衝區,而後再由OS寫入磁盤,使用堆外內存避免了這個操做。this

三、堆外內存申請

 JDK的ByteBuffer類提供了一個接口allocateDirect(int capacity)進行堆外內存的申請,底層經過unsafe.allocateMemory(size)實現。Netty、Mina等框架提供的接口也是基於ByteBuffer封裝的。spa

import java.nio.ByteBuffer;
public class DirectOom {    
    public static void main(String[] args) {        
        //直接分配128M的直接內存(100M) 
        ByteBuffer bb = ByteBuffer.allocateDirect(128*1024*1204);    
    }
}
複製代碼

 源碼分析以下:操作系統

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類保存總分配內存(按頁分配)的大小和實際內存的大小
        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;
    }
複製代碼

四、堆外內存釋放

雖然堆外內存直接受操做系統管理,可是不表明JVM不進行內存回收,要了解堆外內存釋放,必須瞭解如下內容:code

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

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

源碼分析以下(和對象的引用關係對照):

cleaner對象在如下代碼中建立

first對象和ReferenceQueue對象是在如下代碼中,

2)若是該DirectByteBuffer對象在一次GC中被回收了,會發生什麼?

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

Cleaner對象的clean方法主要有兩個做用: 一、把自身從Clener鏈表刪除,從而在下次GC時可以被回收 二、釋放堆外內存

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

其實在初始化DirectByteBuffer對象時,若是當前堆外內存的條件很苛刻時(不夠時),會主動調用System.gc()強制執行FullGC,代碼以下:

五、總結

若是咱們大面積使用堆外內存而且沒有限制,那早晚會致使內存溢出,畢竟程序是跑在一臺資源受限的機器上,由於這塊內存的回收不是你直接能控制的,固然你能夠經過別的一些途徑,好比反射,直接使用Unsafe接口等,可是這些務必給你帶來了一些煩惱,Java與生俱來的優點被你徹底拋棄了—開發不須要關注內存的回收,由gc算法自動去實現。另外上面的gc機制與堆外內存的關係也說了,若是一直觸發不了cms gc或者full gc,那麼後果可能很嚴重。

你的贊和關注是我繼續創做的動力~

相關文章
相關標籤/搜索