最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Java堆外內存及調優(yōu)方式

 更新時間:2026年04月11日 10:07:56   作者:wzz2333  
直接內存并不是Java虛擬機規(guī)范中定義的內存區(qū)域,但使用廣泛,它能顯著提高IO性能,避免內存復制,然而,它也需要小心管理,以避免內存泄漏和溢出,通過設置參數(shù)和使用NMT特性,可以更好地管理和診斷直接內存

直接內存簡介

直接內存(Direct Memory) 并不是虛擬機運行時數(shù)據區(qū)的一部分,并非Java虛擬機規(guī)范中定義的內存區(qū)域。但是這部分內存的頻繁使用,也可能導致 OutOfMemoryError 異常。

直接內存的分配不受Java堆大小的限制,但是受限于本機總內存大小和處理器尋址空間。一般服務器運維人員會根據實際內存設置-Xmx等參數(shù),但經常忽略直接內存,使得動態(tài)擴展時出現(xiàn) OutOfMemeoryError 異常。

JDK 1.4中加入了NIO類,引入一種基于通道(Channel)與 緩沖區(qū)(Buffer)的I/O方式,它可以使用 Native 函數(shù)庫直接分配堆外內存。這樣在一些場景中能顯著提高性能,避免在 Java 堆中和 Native 堆中來回復制數(shù)據。

為什么DirectByteBuffer可以優(yōu)化 IO 性能

普通 IO 流讀取磁盤中數(shù)據時,內核態(tài)需要將磁盤中的數(shù)據拷貝到系統(tǒng)緩沖區(qū) Page Cache(內核地址空間),再從內核態(tài)拷貝到用戶空間中,C 程序里操作的就是用戶態(tài)的內存。

JVM 啟動時在用戶態(tài)申請一塊內存,這塊內存中包含了 Java 堆,幾乎所有創(chuàng)建的對象和數(shù)組都分配在堆上,堆上的實例受 GC 管理。除了Java堆,其余內存稱為 堆外內存,如果使用JNI直接調用 C 函數(shù)申請堆外內存(直接內存),這塊堆外內存不會進行垃圾回收(例如:Direct Memory 由 malloc 分配)。

Java 程序中進行文件的讀操作:

  1. 首先在內核態(tài),將數(shù)據從磁盤中讀取到系統(tǒng)緩存區(qū)中
  2. 再從系統(tǒng)緩沖區(qū)拷貝到用戶態(tài)的堆外內存(JVM實現(xiàn))
  3. 然后再從堆外拷貝到 Java 堆內的 byte 數(shù)組(用戶地址空間)。

讀操作示意圖如下:

上述傳統(tǒng) Java IO方式,經歷了兩次內存拷貝,而NIO中使用 DirectByteBuffer,不需要將數(shù)據從堆外拷貝到堆內,Java程序可以直接訪問堆外的 Direct Memory,減少了一次內存拷貝,也減輕了 GC 壓力,降低了Java堆內存占用。

示意圖如下:

為什么數(shù)據不能直接從系統(tǒng)緩沖區(qū)拷貝到 Java 堆

筆者認為原因主要在于 GC 會改變堆內對象的內存地址,例如:Young GC 時Eden 區(qū)存活對象會被拷貝到 Survivor 區(qū)。而內核態(tài)向用戶態(tài)的數(shù)據拷貝是由內核完成的,并不受 Java 程序控制。

因此,需要先拷貝到堆外內存(這個區(qū)域不會發(fā)生 GC,地址不改變),再從堆外內存拷貝數(shù)據到Java堆中。Java 堆內存和堆外內存同屬用戶地址空間,拷貝可由 Java 虛擬機完成。

Java Direct Buffer用于執(zhí)行很大數(shù)據量的IO密集操作時,存在很大的性能優(yōu)勢。

  • Direct Buffer 是使用malloc進行的堆外分配,生命周期內內存地址都不會再發(fā)生更改,進而內核可以安全地對其進行訪問,很多 IO 操作會很高效。
  • 減少了堆內對象存儲的可能額外維護工作(例如:垃圾回收時位置的移動),所以訪問效率可能有所提高。
  • Direct Buffer 的使用能提高網絡和文件IO效率,因為省去了從本地堆到Java堆的拷貝,降低 Java 堆的內存占用從而減輕了GC壓力。
  • Direct Buffer的創(chuàng)建和銷毀比堆內Buffer增加部分開銷,通常都建議用于長期使用、數(shù)據較大的場景。

直接內存的分配

1.通過NIO中的DirectByteBuffer實例引用直接內存

public static void main(String[] args) {
    ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
    // ...
}

allocateDircet 方法返回 DirectByteBuffer 實例:

public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}

2.DirectByteBuffer 類的構造函數(shù)中,通過Unsafe#allocateMemory分配直接內存空間,并且創(chuàng)建對應的 Cleaner 實例用于回收直接內存,Cleaner 實例是一個指向 DirectByteBuffer 實例的虛引用。

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));
    // 嘗試保留size大小的內存, 如果內存不夠, 處理pending鏈表上的引用
    // 內存仍然不足,則顯式GC, 將不可達的引用放入pending鏈表中, 再從pending回收內存
    // 內存不夠, 則拋出OOM錯誤
    Bits.reserveMemory(size, cap);

    long base = 0;
    try {
        // base為直接內存的基址
        base = unsafe.allocateMemory(size);
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    // 將分配到的直接內存每一個Byte設置為0
    unsafe.setMemory(base, size, (byte) 0);
    // 如果需要直接內存對齊, 且基址base不整除pageSize, 則調整起始地址為base+pageSize減去base%pageSize
    if (pa && (base % ps != 0)) {
        // address為ByteBuffer緩沖區(qū)可使用部分的起始地址
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    // CLeaner 持有 DirectByteBuffer 的幻影(虛)引用
    // Deallocator實現(xiàn)Runnable接口, 執(zhí)行釋放直接內存的操作
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;
}

直接內存的回收

Cleaner類繼承虛引用 PhantomReference,虛引用的referent字段指向 DirectByteBuffer 實例。

虛引用:最弱的引用關系,一個對象是否有虛引用存在不對其生存時間構成影響,也無法通過虛引用獲取對象實例,get 方法返回null。 

為一個對象設置虛引用關聯(lián)的唯一目的是能在這個對象被收集器回收時收到系統(tǒng)通知。

public class Cleaner extends PhantomReference<Object> {
    ...
    // Cleaner.create: var1傳入DirectByteBuffer引用, var2傳入Deallocator實例
    private Cleaner(Object var1, Runnable var2) {
        super(var1, dummyQueue);// DirectByteBuffer作為虛引用
        this.thunk = var2; // 
    }
    public static Cleaner create(Object var0, Runnable var1) {
        return var1 == null ? null : add(new Cleaner(var0, var1));
    }
}

DirectByteBuffer 實例不存在強引用后,垃圾回收時它的 PhantomReference 實例會被放入 pending 鏈表,等待 ReferenceHandler 線程將它從 pending 鏈表中取出,加入到引用隊列queue中。 

ReferenceHandler 線程執(zhí)行邏輯實現(xiàn)于 tryHandlePending 方法:

從 pending 鏈表中取出頭部的 Reference 實例,如果引用實例為 Cleaner 類型,需要調用它的 clean 方法釋放直接內存。隨后,將 Reference 實例加入到引用隊列 queue 中。

public void run() {
    while (true) {
        tryHandlePending(true);
    }
}

static boolean tryHandlePending(boolean waitForNotify) {
    Reference<Object> r;
    Cleaner c;
    try {
        synchronized (lock) {
            if (pending != null) {
                r = pending;
                // Cleaner繼承了虛引用, 需要調用clean方法, 因此特判。
                c = r instanceof Cleaner ? (Cleaner) r : null;
                // pending頭節(jié)點更新為r的下一個節(jié)點
                pending = r.discovered;
                r.discovered = null;
            } else {
                // pending鏈表中元素為空, wait-notify等待喚醒
                if (waitForNotify) {
                    lock.wait();
                }
                // retry if waited
                return waitForNotify;
            }
        }
    }// ...
    // 如果Reference類型為Cleaner, 需要調用clean方法, 直接內存此時會被回收
    if (c != null) {
        c.clean();
        return true;
    }
    // 將Reference實例加入到引用隊列中
    ReferenceQueue<? super Object> q = r.queue;
    // 注冊了引用隊列, 則入隊, 入隊后修改r.queue = ReferenceQueue.ENQUEUED, next指向隊列中的后繼
    if (q != ReferenceQueue.NULL) q.enqueue(r);
    return true;
}

從 pending 鏈表取出時,會調用 Cleaner#clean方法,clean方法會調用運行 Unsafe#freeMemory 釋放直接內存。

// Cleaner
public void clean() {
    if (remove(this)) {
        try {
            this.thunk.run(); // thunk為Deallocator實例
        } // catch
    }
}

// private static class Deallocator implements Runnable
public void run() {
    if (address == 0) {
        // Paranoia
        return;
    }
    // 釋放直接內存, address為直接內存基址
    unsafe.freeMemory(address);
    address = 0;
    Bits.unreserveMemory(size, capacity);
}

Direct Buffer 性能優(yōu)化方面的建議:

  • 應用程序中,System.gc() 觸發(fā)Full GC,將 DirectByteBuffer 回收時調用 Cleaner#clean 方法釋放直接內存。
  • 不要開啟 -XX:+DisableExplicitGC 禁用顯式GC,默認不禁用;
  • 使用 -XX:+ExplicitGCInvokesConcurrent 改變 Full GC 的行為(配合 CMS 使用)。添加該選項后,垃圾收集線程在可達性標記階段與用戶線程并發(fā)運行,減少了STW的時間。

另一種思路是,在大量使用Direct Buffer的部分框架中,框架會自己程序中顯式地調用Unsafe#freeMemory方法,例如Netty。(使用反射獲取 Unsafe 實例,再調用成員方法 freeMemory)

重復利用 Direct Buffer,減少它的創(chuàng)建和銷毀。

直接內存跟蹤與診斷

直接內存的容量大小可通過 -XX:MaxDirectMemorySize 參數(shù)指定,默認與 Java堆最大值一致。使用反射越過 DirectByteBuffer 類,直接通過反射獲取 Unsafe 實例(theUnsafe靜態(tài)屬性),進行內存分配。

Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
// theUnsafe為static final字段
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
// 分配直接內存
long address = unsafe.allocateMemory(1024);
unsafe.freeMemory(address);

由直接內存導致的內存溢出,在Heap Dump文件中不會看見明顯的異常情況。

如果發(fā)現(xiàn)內存溢出后,產生的Dump文件很小,而程序中直接或間接使用了Direct Memory(NIO),就可以考慮檢查直接內存溢出。

通常的垃圾收集日志等記錄,并不包含 Direct Buffer 等信息。從JDK 1.8開始,可以使用 Native Memory Tracking(NMT) 特性來進行診斷,可以在程序啟動時加上下面參數(shù):

-XX:NativeMemoryTracking={summary|detail}

運行時,采用如下命令交互式對比:

// 打印NMT信息
jcmd <pid> VM.native_memory detail

// 進行baseline,以對比分配內存變化
jcmd <pid> VM.native_memory baseline

// 對比baseline, 顯示出各個部分內存的變化
jcmd <pid> VM.native_memory detail.diff

下面案例中,先使用 VM.native_memory 的 baseline 命令,作為對比的參照;當打印出 Begin allocate 后,執(zhí)行detail.diff,進行對比。

public class DirectMemory {
    public static void main(String[] args) {
        try {
            Thread.sleep(40000);// 進行baseline, 作為比對的參照
            System.out.println("Begin allocate: ...");
            ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024 * 3);    
            Thread.sleep(40000);    
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

結果如下圖所示,Internal部分的內存增加了3078KB,3MB = 3072KB

總結

以上為個人經驗,希望能給大家一個參考,也希望大家多多支持腳本之家。

相關文章

最新評論

灵武市| 阿拉尔市| 永嘉县| 广东省| 蒲江县| 嘉义县| 讷河市| 永泰县| 金坛市| 中牟县| 西华县| 浦北县| 城固县| 洛川县| 当雄县| 确山县| 临西县| 酉阳| 太原市| 黄陵县| 灌云县| 伊通| 黎川县| 浦城县| 珲春市| 凤台县| 南汇区| 正定县| 六枝特区| 安庆市| 湄潭县| 海淀区| 泾阳县| 广灵县| 利辛县| 延寿县| 饶平县| 沂南县| 大厂| 安国市| 凉城县|