Java 堆外內(nèi)存、零拷貝、直接內(nèi)存的思考

Java堆棧內(nèi)存與堆外內(nèi)存

  1. 堆棧內(nèi)存
    堆棧內(nèi)存指的是堆內(nèi)存和棧內(nèi)存:堆內(nèi)存是GC管理的內(nèi)存,棧內(nèi)存是線程內(nèi)存。

堆內(nèi)存結(jié)構(gòu):

image.png

還有一個更細致的結(jié)構(gòu)圖(包括MetaSpace還有code cache):

注意在Java8以后PermGen被MetaSpace代替,運行時可自動擴容,并且默認是無限大

image.png

我們看下面一段代碼來簡單理解下堆棧的關(guān)系:

public static void main(String[] args) {
Object o = new Object();
}
其中new Object()是在堆上面分配,而Object o這個變量,是在main這個線程棧上面。

應(yīng)用程序所有的部分都使用堆內(nèi)存,然后棧內(nèi)存通過一個線程運行來使用。
不論對象什么時候創(chuàng)建,他都會存儲在堆內(nèi)存中,棧內(nèi)存包含它的引用。棧內(nèi)存只包含原始值變量好和堆中對象變量的引用。
存儲在堆中的對象是全局可以被訪問的,然而棧內(nèi)存不能被其他線程所訪問。
通過JVM參數(shù)-Xmx我們可以指定最大堆內(nèi)存大小,通過-Xss我們可以指定每個線程線程棧占用內(nèi)存大小

  1. 堆外內(nèi)存
    2.1. 廣義的堆外內(nèi)存
    除了堆棧內(nèi)存,剩下的就都是堆外內(nèi)存了,包括了jvm本身在運行過程中分配的內(nèi)存,codecache,jni里分配的內(nèi)存,DirectByteBuffer分配的內(nèi)存等等

2.2. 狹義的堆外內(nèi)存 - DirectByteBuffer
而作為java開發(fā)者,我們常說的堆外內(nèi)存溢出了,其實是狹義的堆外內(nèi)存,這個主要是指java.nio.DirectByteBuffer在創(chuàng)建的時候分配內(nèi)存,我們這篇文章里也主要是講狹義的堆外內(nèi)存,因為它和我們平時碰到的問題比較密切

為啥要使用堆外內(nèi)存。通常因為:

在進程間可以共享,減少虛擬機間的復(fù)制
對垃圾回收停頓的改善:如果應(yīng)用某些長期存活并大量存在的對象,經(jīng)常會出發(fā)YGC或者FullGC,可以考慮把這些對象放到堆外。過大的堆會影響Java應(yīng)用的性能。如果使用堆外內(nèi)存的話,堆外內(nèi)存是直接受操作系統(tǒng)管理( 而不是虛擬機 )。這樣做的結(jié)果就是能保持一個較小的堆內(nèi)內(nèi)存,以減少垃圾收集對應(yīng)用的影響。
在某些場景下可以提升程序I/O操縱的性能。少去了將數(shù)據(jù)從堆內(nèi)內(nèi)存拷貝到堆外內(nèi)存的步驟。

  1. JNI調(diào)用與內(nèi)核態(tài)及用戶態(tài)
    內(nèi)核態(tài):cpu可以訪問內(nèi)存的所有數(shù)據(jù),包括外圍設(shè)備,例如硬盤,網(wǎng)卡,cpu也可以將自己從一個程序切換到另一個程序。
    用戶態(tài):只能受限的訪問內(nèi)存,且不允許訪問外圍設(shè)備,占用cpu的能力被剝奪,cpu資源可以被其他程序獲取。
    系統(tǒng)調(diào)用:為了使上層應(yīng)用能夠訪問到這些資源,內(nèi)核為上層應(yīng)用提供訪問的接口

我們舉個例子,文件讀?。籎ava本身并不能讀取文件,因為用戶態(tài)沒有權(quán)限訪問外圍設(shè)備。需要通過系統(tǒng)調(diào)用切換內(nèi)核態(tài)進行讀取。

目前,JAVA的IO方式有基于流的傳統(tǒng)IO還有基于塊的NIO方式(雖然文件讀取其實不是嚴格意義上的NIO,哈哈)。面向流意味著從流中一次可以讀取一個或多個字節(jié),拿到讀取的這些做什么你說了算,這里沒有任何緩存(這里指的是使用流沒有任何緩存,接收或者發(fā)送的數(shù)據(jù)是緩存到操作系統(tǒng)中的,流就像一根水管從操作系統(tǒng)的緩存中讀取數(shù)據(jù))而且只能順序從流中讀取數(shù)據(jù),如果需要跳過一些字節(jié)或者再讀取已經(jīng)讀過的字節(jié),你必須將從流中讀取的數(shù)據(jù)先緩存起來。面向塊的處理方式有些不同,數(shù)據(jù)是先被 讀/寫到buffer中的,根據(jù)需要你可以控制讀取什么位置的數(shù)據(jù)。這在處理的過程中給用戶多了一些靈活性,然而,你需要額外做的工作是檢查你需要的數(shù)據(jù)是否已經(jīng)全部到了buffer中,你還需要保證當(dāng)有更多的數(shù)據(jù)進入buffer中時,buffer中未處理的數(shù)據(jù)不會被覆蓋。

我們這里只分析基于塊的NIO方式,在JAVA中這個塊就是ByteBuffer。

  1. Linux下零拷貝原理
    大部分web服務(wù)器都要處理大量的靜態(tài)內(nèi)容,而其中大部分都是從磁盤文件中讀取數(shù)據(jù)然后寫到socket中。我們以這個過程為例子,來看下不同模式下Linux工作流程

4.1. 普通Read/Write模式
涉及的代碼抽象:

//從文件中讀取,存入tmp_buf
read(file, tmp_buf, len);
//將tmp_buf寫入socket
write(socket, tmp_buf, len);
看上去很簡單的步驟但是經(jīng)過了很多復(fù)制:

當(dāng)調(diào)用 read 系統(tǒng)調(diào)用時,通過 DMA(Direct Memory Access)將數(shù)據(jù) copy 到內(nèi)核模式
然后由 CPU 控制將內(nèi)核模式數(shù)據(jù) copy 到用戶模式下的 buffer 中
read 調(diào)用完成后,write 調(diào)用首先將用戶模式下 buffer 中的數(shù)據(jù) copy 到內(nèi)核模式下的 socket buffer 中
最后通過 DMA copy 將內(nèi)核模式下的 socket buffer 中的數(shù)據(jù) copy 到網(wǎng)卡設(shè)備中傳送。
從上面的過程可以看出,數(shù)據(jù)白白從內(nèi)核模式到用戶模式走了一圈,浪費了兩次 copy(第一次,從kernel模式拷貝到user模式;第二次從user模式再拷貝回kernel模式,即上面4次過程的第2和3步驟。),而這兩次 copy 都是 CPU copy,即占用CPU資源

4.2. sendfile模式


image.png

通過 sendfile 傳送文件只需要一次系統(tǒng)調(diào)用,當(dāng)調(diào)用 sendfile 時:

首先通過 DMA copy 將數(shù)據(jù)從磁盤讀取到 kernel buffer 中
然后通過 CPU copy 將數(shù)據(jù)從 kernel buffer copy 到 sokcet buffer 中
最終通過 DMA copy 將 socket buffer 中數(shù)據(jù) copy 到網(wǎng)卡 buffer 中發(fā)送 sendfile 與 read/write 方式相比,少了 一次模式切換一次 CPU copy。但是從上述過程中也可以發(fā)現(xiàn)從 kernel buffer 中將數(shù)據(jù) copy 到socket buffer 是沒必要的。

4.3. sendfile模式改進
Linux2.4 內(nèi)核對sendFile模式進行了改進:

image.png

改進后的處理過程如下:

DMA copy 將磁盤數(shù)據(jù) copy 到 kernel buffer 中 2.向 socket buffer 中追加當(dāng)前要發(fā)送的數(shù)據(jù)在 kernel buffer 中的位置和偏移量
DMA gather copy 根據(jù) socket buffer 中的位置和偏移量直接將 kernel buffer 中的數(shù)據(jù) copy 到網(wǎng)卡上。
經(jīng)過上述過程,數(shù)據(jù)只經(jīng)過了 2 次 copy 就從磁盤傳送出去了。(事實上這個 Zero copy 是針對內(nèi)核來講的,數(shù)據(jù)在內(nèi)核模式下是 Zero-copy 的)。

當(dāng)前許多高性能 http server 都引入了 sendfile 機制,如 nginx,lighttpd 等。

  1. Java零拷貝實現(xiàn)的變化
    Zero-Copy技術(shù)省去了將操作系統(tǒng)的read buffer拷貝到程序的buffer,以及從程序buffer拷貝到socket buffer的步驟,直接將read buffer拷貝到socket buffer. Java NIO中的FileChannal.transferTo()方法就是這樣的實現(xiàn)

public void transferTo(long position,long count,WritableByteChannel target);
transferTo()方法將數(shù)據(jù)從一個channel傳輸?shù)搅硪粋€可寫的channel上,其內(nèi)部實現(xiàn)依賴于操作系統(tǒng)對zero copy技術(shù)的支持。在unix操作系統(tǒng)和各種linux的發(fā)型版本中,這種功能最終是通過sendfile()系統(tǒng)調(diào)用實現(xiàn)。下邊就是這個方法的定義:

include <sys/socket.h>

ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

在內(nèi)核為2.4或者以上版本的linux系統(tǒng)上,socket緩沖區(qū)描述符將被用來滿足這個需求。這個方式不僅減少了內(nèi)核用戶態(tài)間的切換,而且也省去了那次需要cpu參與的復(fù)制過程。 從用戶角度來看依舊是調(diào)用transferTo()方法,但是其本質(zhì)發(fā)生了變化:

調(diào)用transferTo方法后數(shù)據(jù)被DMA從文件復(fù)制到了內(nèi)核的一個緩沖區(qū)中。
數(shù)據(jù)不再被復(fù)制到socket關(guān)聯(lián)的緩沖區(qū)中了,僅僅是將一個描述符(包含了數(shù)據(jù)的位置和長度等信息)追加到socket關(guān)聯(lián)的緩沖區(qū)中。DMA直接將內(nèi)核中的緩沖區(qū)中的數(shù)據(jù)傳輸給協(xié)議引擎,消除了僅剩的一次需要cpu周期的數(shù)據(jù)復(fù)制。

5.3 對于JAVA普通字節(jié)流IO與NIOFileChannel實現(xiàn)的零拷貝性能:

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.FileChannel;

public class FileCopyTest {

    /**
     * 通過字節(jié)流的方式復(fù)制文件
     * @param fromFile 源文件
     * @param toFile   目標(biāo)文件
     * @throws FileNotFoundException 未找到文件異常
     */
    public static void fileCopyNormal(File fromFile, File toFile) throws FileNotFoundException {
        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            inputStream = new BufferedInputStream(new FileInputStream(fromFile));
            outputStream = new BufferedOutputStream(new FileOutputStream(toFile));
            //用戶態(tài)緩沖有1kB這么大,不算小了
            byte[] bytes = new byte[1024];
            int i;
            //讀取到輸入流數(shù)據(jù),然后寫入到輸出流中去,實現(xiàn)復(fù)制
            while ((i = inputStream.read(bytes)) != -1) {
                outputStream.write(bytes, 0, i);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
                if (outputStream != null) {
                    outputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 用filechannel進行文件復(fù)制
     *
     * @param fromFile 源文件
     * @param toFile   目標(biāo)文件
     */
    public static void fileCopyWithFileChannel(File fromFile, File toFile) {
        FileInputStream fileInputStream = null;
        FileOutputStream fileOutputStream = null;
        FileChannel fileChannelInput = null;
        FileChannel fileChannelOutput = null;
        try {
            fileInputStream = new FileInputStream(fromFile);
            fileOutputStream = new FileOutputStream(toFile);
            //得到fileInputStream的文件通道
            fileChannelInput = fileInputStream.getChannel();
            //得到fileOutputStream的文件通道
            fileChannelOutput = fileOutputStream.getChannel();
            //將fileChannelInput通道的數(shù)據(jù),寫入到fileChannelOutput通道
            fileChannelInput.transferTo(0, fileChannelInput.size(), fileChannelOutput);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fileInputStream != null) {
                    fileInputStream.close();
                }
                if (fileChannelInput != null) {
                    fileChannelInput.close();
                }
                if (fileOutputStream != null) {
                    fileOutputStream.close();
                }
                if (fileChannelOutput != null) {
                    fileChannelOutput.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws IOException {
        File fromFile = new File("D:/readFile.txt");
        File toFile = new File("D:/outputFile.txt");

        //預(yù)熱
        fileCopyNormal(fromFile, toFile);
        fileCopyWithFileChannel(fromFile, toFile);

        //計時
        long start = System.currentTimeMillis();
        for (int i = 0; i < 1000; i++) {
            fileCopyNormal(fromFile, toFile);
        }
        System.out.println("fileCopyNormal time: " + (System.currentTimeMillis() - start));

        start = System.currentTimeMillis();
        for (int i = 0; i < 1000; i++) {
            fileCopyWithFileChannel(fromFile, toFile);
        }
        System.out.println("fileCopyWithFileChannel time: " + (System.currentTimeMillis() - start));
    }
}

測試結(jié)果:

fileCopyNormal time: 14271
fileCopyWithFileChannel time: 6632

差了一倍多的時間(文件大小大概8MB),如果文件更大這個差距應(yīng)該更加明顯。

  1. DirectBuffer分配
    Java中NIO的核心緩沖就是ByteBuffer,所有的IO操作都是通過這個ByteBuffer進行的;Bytebuffer有兩種: 分配HeapByteBuffer

ByteBuffer buffer = ByteBuffer.allocate(int capacity);

分配DirectByteBuffer

ByteBuffer buffer = ByteBuffer.allocateDirect(int capacity);

6.1. 為何HeapByteBuffer會多一次拷貝?
6.1.1. FileChannel的force api說明
FileChannel的force方法: FileChannel.force()方法將通道里尚未寫入磁盤的數(shù)據(jù)強制寫到磁盤上。出于性能方面的考慮,操作系統(tǒng)會將數(shù)據(jù)緩存在內(nèi)存中,所以無法保證寫入到FileChannel里的數(shù)據(jù)一定會即時寫到磁盤上。要保證這一點,需要調(diào)用force()方法。 force()方法有一個boolean類型的參數(shù),指明是否同時將文件元數(shù)據(jù)(權(quán)限信息等)寫到磁盤上。

6.1.2. FileChannel和SocketChannel依賴的IOUtil源碼解析
無論是FileChannel還是SocketChannel,他們的讀寫方法都依賴IOUtil的相同方法,我們這里來看下: IOUtil.java

static int write(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {
    //如果是DirectBuffer,直接寫
    if (var1 instanceof DirectBuffer) {
        return writeFromNativeBuffer(var0, var1, var2, var4);
    } else {
        //非DirectBuffer
        //獲取已經(jīng)讀取到的位置
        int var5 = var1.position();
        //獲取可以讀到的位置
        int var6 = var1.limit();

        assert var5 <= var6;
        //申請一個源buffer可讀大小的DirectByteBuffer
        int var7 = var5 <= var6 ? var6 - var5 : 0;
        ByteBuffer var8 = Util.getTemporaryDirectBuffer(var7);

        int var10;
        try {

            var8.put(var1);
            var8.flip();
            var1.position(var5);
            //通過DirectBuffer寫
            int var9 = writeFromNativeBuffer(var0, var8, var2, var4);
            if (var9 > 0) {
                var1.position(var5 + var9);
            }

            var10 = var9;
        } finally {
            //回收分配的DirectByteBuffer
            Util.offerFirstTemporaryDirectBuffer(var8);
        }

        return var10;
    }
}
//讀的方法和寫類似,這里省略

6.1.3. 為何一定要復(fù)制到DirectByteBuffer來讀寫(系統(tǒng)調(diào)用)
首先,先說一點,執(zhí)行native方法的線程,被認為是處于SafePoint,所以,會發(fā)生 NIO 如果不復(fù)制到 DirectByteBuffer,就會有 GC 發(fā)生重排列對象內(nèi)存的情況

傳統(tǒng) BIO 是面向 Stream 的,底層實現(xiàn)可以理解為寫入的是 byte 數(shù)組,調(diào)用 native 方法寫入 IO,傳的參數(shù)是這個數(shù)組,就算GC改變了內(nèi)存地址,但是拿這個數(shù)組的引用照樣能找到最新的地址,,對應(yīng)的方法時是:(bio 也是會申請臨時的直接內(nèi)存的)FileOutputStream.write

private native void writeBytes(byte b[], int off, int len, boolean append)
throws IOException;

但是NIO,為了提升效率,傳的是內(nèi)存地址,省去了一次間接應(yīng)用,但是就必須用 DirectByteBuffer 防止內(nèi)存地址改變,對應(yīng)的是 NativeDispatcher.write

abstract int write(FileDescriptor fd, long address, int len)
throws IOException;

那為何內(nèi)存地址會改變呢?GC會回收無用對象,同時還會進行碎片整理,移動對象在內(nèi)存中的位置,來減少內(nèi)存碎片。DirectByteBuffer不受GC控制。如果不用DirectByteBuffer而是用HeapByteBuffer,如果在調(diào)用系統(tǒng)調(diào)用時,發(fā)生了GC,導(dǎo)致HeapByteBuffer內(nèi)存位置發(fā)生了變化,但是內(nèi)核態(tài)并不能感知到這個變化導(dǎo)致系統(tǒng)調(diào)用讀取或者寫入錯誤的數(shù)據(jù)。所以一定要通過不受GC影響的HeapByteBuffer

假設(shè)我們要從網(wǎng)絡(luò)中讀入一段數(shù)據(jù),再把這段數(shù)據(jù)發(fā)送出去的話,采用Non-direct ByteBuffer的流程是這樣的:

網(wǎng)絡(luò) –> 臨時的DirectByteBuffer –> 應(yīng)用 Non-direct ByteBuffer –> 臨時的Direct ByteBuffer –> 網(wǎng)絡(luò)

這種方式是直接在堆外分配一個內(nèi)存(即,native memory)來存儲數(shù)據(jù), 程序通過JNI直接將數(shù)據(jù)讀/寫到堆外內(nèi)存中。因為數(shù)據(jù)直接寫入到了堆外內(nèi)存中,所以這種方式就不會再在JVM管控的堆內(nèi)再分配內(nèi)存來存儲數(shù)據(jù)了,也就不存在堆內(nèi)內(nèi)存和堆外內(nèi)存數(shù)據(jù)拷貝的操作了。這樣在進行I/O操作時,只需要將這個堆外內(nèi)存地址傳給JNI的I/O的函數(shù)就好了。

采用Direct ByteBuffer的流程是這樣的:

網(wǎng)絡(luò) –> 應(yīng)用 Direct ByteBuffer –> 網(wǎng)絡(luò)

可以看到,除開構(gòu)造和析構(gòu)臨時Direct ByteBuffer的時間外,起碼還能節(jié)約兩次內(nèi)存拷貝的時間。那么是否在任何情況下都采用Direct Buffer呢?

不是。對于大部分應(yīng)用而言,兩次內(nèi)存拷貝的時間幾乎可以忽略不計,而構(gòu)造和析構(gòu)DirectBuffer的時間卻相對較長。在JVM的實現(xiàn)當(dāng)中,某些方法會緩存一部分臨時Direct ByteBuffer,意味著如果采用Direct ByteBuffer僅僅能節(jié)約掉兩次內(nèi)存拷貝的時間, 而無法節(jié)約構(gòu)造和析構(gòu)的時間。就用Sun的實現(xiàn)來說,write(ByteBuffer)和read(ByteBuffer)方法都會緩存臨時Direct ByteBuffer,而write(ByteBuffer[])和read(ByteBuffer[])每次都生成新的臨時Direct ByteBuffer。

6.2. ByteBuffer創(chuàng)建
6.2.1. ByteBuffer創(chuàng)建HeapByteBuffer
分配在堆上的,直接由Java虛擬機負責(zé)垃圾收集,你可以把它想象成一個字節(jié)數(shù)組的包裝類

6.2.2. DirectByteBuffer
這個類就沒有HeapByteBuffer簡單了

DirectByteBuffer(int cap) {                   // package-private
    super(-1, 0, cap, cap);
    boolean pa = VM.isDirectMemoryPageAligned();
    int ps = Bits.pageSize();
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    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;

Bits.reserveMemory(size, cap) 方法

static void reserveMemory(long size, int cap) {
    synchronized (Bits.class) {
        if (!memoryLimitSet && VM.isBooted()) {
            maxMemory = VM.maxDirectMemory();
            memoryLimitSet = true;
        }
        // -XX:MaxDirectMemorySize limits the total capacity rather than the
        // actual memory usage, which will differ when buffers are page
        // aligned.
        if (cap <= maxMemory - totalCapacity) {
            reservedMemory += size;
            totalCapacity += cap;
            count++;
            return;
        }
    }
    System.gc();
    try {
        Thread.sleep(100);
    } catch (InterruptedException x) {
        // Restore interrupt status
        Thread.currentThread().interrupt();
    }
    synchronized (Bits.class) {
        if (totalCapacity + cap > maxMemory)
            throw new OutOfMemoryError("Direct buffer memory");
        reservedMemory += size;
        totalCapacity += cap;
        count++;
    }
}

在DirectByteBuffer中,首先向Bits類申請額度,Bits類有一個全局的totalCapacity變量,記錄著全部DirectByteBuffer的總大小,每次申請,都先看看是否超限,堆外內(nèi)存的限額默認與堆內(nèi)內(nèi)存(由-Xmx 設(shè)定)相仿,可用 -XX:MaxDirectMemorySize 重新設(shè)定。

如果不指定,該參數(shù)的默認值為Xmx的值減去1個Survior區(qū)的值。 如設(shè)置啟動參數(shù)-Xmx20M -Xmn10M -XX:SurvivorRatio=8,那么申請20M-1M=19M的DirectMemory

如果已經(jīng)超限,會主動執(zhí)行Sytem.gc(),期待能主動回收一點堆外內(nèi)存。System.gc()會觸發(fā)一個full gc,當(dāng)然前提是你沒有顯示的設(shè)置-XX:+DisableExplicitGC來禁用顯式GC。并且你需要知道,調(diào)用System.gc()并不能夠保證full gc馬上就能被執(zhí)行。然后休眠一百毫秒,看看totalCapacity降下來沒有,如果內(nèi)存還是不足,就拋出OOM異常。如果額度被批準,就調(diào)用大名鼎鼎的sun.misc.Unsafe去分配內(nèi)存,返回內(nèi)存基地址

所以,一般的框架里面,會在啟動時申請一大塊DirectByteBuffer,然后自己做內(nèi)存管理

最后,創(chuàng)建一個Cleaner,并把代表清理動作的Deallocator類綁定 – 降低Bits里的totalCapacity,并調(diào)用Unsafe調(diào)free去釋放內(nèi)存。

6.2.3. ByteBuffer回收
HeapByteBuffer就不要說了,GC就幫忙處理了。這兒主要說下DirectByteBuffer 存在于堆內(nèi)的DirectByteBuffer對象很小,只存著基地址和大小等幾個屬性,和一個Cleaner,但它代表著后面所分配的一大段內(nèi)存,是所謂的冰山對象。

image.png

其中first是Cleaner類的靜態(tài)變量,Cleaner對象在初始化時會被添加到Clener鏈表中,和first形成引用關(guān)系,ReferenceQueue是用來保存需要回收的Cleaner對象。

如果該DirectByteBuffer對象在一次GC中被回收了

image.png

此時,只有Cleaner對象唯一保存了堆外內(nèi)存的數(shù)據(jù)(開始地址、大小和容量),在下一次Full GC時,把該Cleaner對象放入到ReferenceQueue中,并觸發(fā)clean方法。

快速回顧一下堆內(nèi)的GC機制,當(dāng)新生代滿了,就會發(fā)生young gc;如果此時對象還沒失效,就不會被回收;撐過幾次young gc后,對象被遷移到老生代;當(dāng)老生代也滿了,就會發(fā)生full gc。

這里可以看到一種尷尬的情況,因為DirectByteBuffer本身的個頭很小,只要熬過了young gc,即使已經(jīng)失效了也能在老生代里舒服的呆著,不容易把老生代撐爆觸發(fā)full gc,如果沒有別的大塊頭進入老生代觸發(fā)full gc,就一直在那耗著,占著一大片堆外內(nèi)存不釋放。

這時,就只能靠前面提到的申請額度超限時觸發(fā)的system.gc()來救場了。但這道最后的保險其實也不很好,首先它會中斷整個進程,然后它讓當(dāng)前線程睡了整整一百毫秒,而且如果gc沒在一百毫秒內(nèi)完成,它仍然會無情的拋出OOM異常。還有,萬一,萬一大家迷信某個調(diào)優(yōu)指南設(shè)置了-DisableExplicitGC禁止了system.gc(),那就不好玩了。

所以,堆外內(nèi)存還是自己主動點回收更好,比如Netty就是這么做的

  1. 查看DirectBuffer使用情況的方法:

7.1. 進程內(nèi)獲取:

MBeanServer mbs = ManagementFactory. getPlatformMBeanServer() ;
ObjectName objectName = new ObjectName("java.nio:type=BufferPool,name=direct" ) ;
MBeanInfo info = mbs.getMBeanInfo(objectName) ;
for(MBeanAttributeInfo i : info.getAttributes()) {
    System.out .println(i.getName() + ":" + mbs.getAttribute(objectName , i.getName()));
}

7.2. 遠程進程
JMX獲取 如果目標(biāo)機器沒有啟動JMX,那么添加jvm參數(shù):

-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremotAe.ssl=false

重啟進程 然后本機通過JMX連接訪問:

String jmxURL = "service:jmx:rmi:///jndi/rmi://10.125.6.204:9999/jmxrmi" ;
JMXServiceURL serviceURL = new JMXServiceURL(jmxURL);
Map map = new HashMap() ;
String[] credentials = new String[] { "monitorRole" , "QED" } ;
map.put( "jmx.remote.credentials" , credentials) ;
JMXConnector connector = JMXConnectorFactory. connect(serviceURL , map);
MBeanServerConnection mbsc = connector.getMBeanServerConnection() ;
ObjectName objectName = new ObjectName("java.nio:type=BufferPool,name=direct" ) ;
MBeanInfo mbInfo = mbsc.getMBeanInfo(objectName) ;
for(MBeanAttributeInfo i : mbInfo.getAttributes()) {
    System.out .println(i.getName() + ":" + mbsc.getAttribute(objectName , i.getName()));
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容