瞭解NonHeap嗎?

在咱們平常的開發過程當中,遇到問題除了普通的異常(空指針啊,數組越界啊 and so on),咱們遇到的比較大的問題無非就是OOM,頻繁FullGC或者是多線程方面的問題(這塊我說不上話🌚),咱們大都數產生的問題也都是與JVM相關的,而今日則談談與它有關聯的另一個地方。java

NonHeap

身爲一個java開發者,咱們首先熟悉的是JVM(儘管對裏面的各類各類回收算法還不算很清晰),它幫咱們管理着各個對象(是的,咱們都有對象🤔)的生命週期,助於程序可以正常的運行下去。可是還有一塊區域與它隔岸相望->非堆內存(以下圖)。程序員

咱們能夠清晰的看出NonHeap在程序中的位置(以上畫圖並不表明他們在內存中所佔的空間比例狀況)。算法

做用

咱們能肯定的是堆裏面的東西是咱們去本身操做的,而NonHeap就是JVM留給本身用的,因此方法區、JVM內部處理或優化所需的內存(如JIT編譯後的代碼緩存)、每一個類結構(如運行時常數池、字段和方法數據)以及方法和構造方法的代碼都在非堆內存中。數組

本地起來一個小的Demo,咱們經過Arthas能夠去查看堆空間與非堆空間的狀況,以及劃分的區域。緩存

使用方面

普通的開發者應該是用不到的(像我這樣🌚🌚🌚),高級以上的開發應該會使用到,由於他們知道如何使一個普通的程序變得不普通。安全

JAVA中,能夠經過UnsafeNIO包下的ByteBuffer來操做非堆內存。服務器

Unsafe

一看這名字就知道不安全😹,不過也的確不怎麼安全。它位於sun.misc包下的一個類,主要提供一些用於執行低級別、不安全操做的方法。內部API大多數是對系統內存直接操做的,這會提升咱們程序的運行效率等等,可是也一樣會很容易發生錯誤,他裏面操做相似於C語言同樣的指針操做,會增長了程序相關指針問題的風險。多線程

咱們能夠稍微👻康康👻其中部分方法:併發

// 分配內存 , 至關於 C++ 的 malloc 函數
public native long allocateMemory(long bytes);
// 擴充內存
public native long reallocateMemory(long address, long bytes);
// 釋放內存
public native void freeMemory(long address);
// 在給定的內存塊中設置值
public native void setMemory(Object o, long offset, long bytes, byte value);
// 內存拷貝
public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);
// 獲取給定地址值,忽略修飾限定符的訪問限制。與此相似操做還有 : getInt,getDouble,getLong,getChar 等
public native Object getObject(Object o, long offset);
// 爲給定地址設置值,忽略修飾限定符的訪問限制,與此相似操做還有 :putInt,putDouble,putLong,putChar 等
public native void putObject(Object o, long offset, Object x);
// 獲取給定地址的 byte 類型的值(當且僅當該內存地址爲 allocateMemory 分配時,此方法結果爲肯定的)
public native byte getByte(long address);
// 爲給定地址設置 byte 類型的值(當且僅當該內存地址爲 allocateMemory 分配時,此方法結果纔是肯定的)
public native void putByte(long address, byte x);
複製代碼

除了以上直接操做內存相關的方法,還有一些用於CAS的方法,j.u.c底下的併發集合操做以及相關的鎖操做其實大部分都是調用了Unsafe裏面的方法來控制。框架

DirectByteBuffer

DirectByteBufferJava用於實現堆外內存的一個重要類,一般用在通訊過程當中作緩衝池,如在 NettyNIO框架中應用普遍。DirectByteBuffer 對於堆外內存的建立、使用、銷燬等邏輯也是由 Unsafe 提供的堆外內存 API 來實現,其構造函數就能夠直接分配內存。

相關API康康:

// 分配size大小內存
 unsafe.allocateMemory(size);
 // 從base位置開始初始化size大小內存
 unsafe.setMemory(base, size, (byte) 0);
複製代碼

回收方面

咱們瞭解到Heap的回收都是依賴的是jvm各個區域的回收算法實現,那麼非堆的回收是如何進行呢?以及什麼狀況下去進行呢?

目前瞭解到的兩種方式:

  1. 其垃圾回收依賴於代碼顯式調用System.gc()。
  2. 依賴垃圾回收追蹤對象 Cleaner 實現堆外內存釋放。

第一種暫且不過多討論了。 第二種這裏提一提:

咱們經過DirectByteBuffer源碼查看下當前的類結構,主要注意的是當前對象裏面包含了一個Deallocator私有靜態內部類以及私有成員屬性Cleaner

private final Cleaner cleaner;

    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);
        }
    }
    
複製代碼

從上面咱們能夠大概知道最後進行非堆內存的回收確定是靜態內部類進行操做的。同時也與成員變量有關係,那麼他是怎麼進行操做的呢?

這裏咱們要注意的就是Cleaner對象了。

Cleaner 繼承自 Java 四大引用類型之一的虛引用 PhantomReference(咱們瞭解到沒法經過虛引用獲取與之關聯的對象實例,且當對象僅被虛引用引用時,在任何發生 GC的時候,其都可被回收),一般 PhantomReference 與引用隊列ReferenceQueue 結合使用,能夠實現虛引用關聯對象被垃圾回收時可以進行系統通知、資源清理等功能。以下圖所示,當某個被 Cleaner 引用的對象將被回收時,JVM 垃圾收集器會將此對象的引用放入到對象引用中的 pending 鏈表中,等待Reference-Handler進行相關處理。其中,Reference-Handler爲一個擁有最高優先級的守護線程,會循環不斷的處理 pending 鏈表中的對象引用,執行 Cleanerclean 方法進行相關清理工做。

因此當DirectByteBuffer 僅被 Cleaner 引用(即爲虛引用)時,其能夠在任意GC 時段被回收。當 DirectByteBuffer 實例對象被回收時,在 Reference-Handler線程操做中,會調用 Cleanerclean 方法根據建立 Cleaner 時傳入的 Deallocator 來進行堆外內存的釋放。

做用

咱們瞭解過堆的做用,那麼咱們就好奇下非堆在咱們的程序中佔着什麼樣子的做用?

總結下有兩點:

  1. 對垃圾回收停頓的改善。因爲堆外內存是直接受操做系統管理而不是 JVM, 因此當咱們使用堆外內存時,便可保持較小的堆內內存規模。從而在 GC 時減 少回收停頓對於應用的影響。
  2. 提高程序 I/O 操做的性能。一般在 I/O 通訊過程當中,會存在堆內內存到堆外內 存的數據拷貝操做,對於須要頻繁進行內存間數據拷貝且生命週期較短的暫存 數據,都建議存儲到堆外內存。

第一點說明:

咱們知道jvm中的全部gc是針對於當前容器內的對象進行回收處理的,在Ygc階段,涉及到垃圾標記的過程,從GCRoot開始標記,一旦掃描到引用到了老年代的對象則中斷本次掃描,加速Ygc的進度,可是Ygc階段中的old-gen sacnning階段則用於掃描被老年代引用的對象,那麼一旦老年代過大,則Ygc所須要的時間就過長(時間與大小成正比),則不利於當前程序的垃圾回收。因此一旦引入非堆,咱們就能夠保持較小的堆內存規模,從而保證gc的正常進行。

第二點說明:

這裏面涉及的主要關於服務器的用戶態以及內核態,咱們瞭解到在服務器上面操做的一個文件傳輸出去,會涉及到用戶態轉內核態,而後內核態轉用戶態等等步驟,其中有些操做是消耗cpu資源的(從內存地址緩存區讀取以及寫入),咱們就會其中的操做是能夠省略的,咱們能夠直接將文件從磁盤到內存地址緩存區,而後再到套接字緩衝區,這就是所謂的零拷貝技術。

結尾

以上部分就是簡單的說下非堆在java中的做用。使用非堆我以爲大部分的程序員應該還使用不到(我是暫且摸不到的),不過你們能夠了解下,增加知識準沒錯🙈🙈。最後祝你們過個好年~

相關文章
相關標籤/搜索