【JAVA Reference】Cleaner 在堆外內存DirectByteBuffer中的應用(五)

我的原則:先會用再說,內部慢慢來。
學以致用,根據場景學源碼


一、DirectByteBuffer 架構

1.1 代碼UML

在這裏插入圖片描述

  • DirectByteBuffer對象本身其實是很小的,但是它後面可能關聯了一個非常大的堆外內存,因此我們通常稱之爲冰山對象.

1.2 申請內存Flow圖

在這裏插入圖片描述
=== 點擊查看top目錄 ===

二、DirectByteBuffer 實戰 Demo

2.1 使用 ByteBuffer.allocateDirect 申請堆外內存

// @VM args:-XX:MaxDirectMemorySize=40m 
public class _07_00_TestDirectByteBuffer {
    public static void main(String[] args) {
        while(true) {
            ByteBuffer buffer = ByteBuffer.allocateDirect(1 * 1024 * 1024);
        }
    }
}

結果: 設置堆內存最大40m,但是代碼完全不停歇,毫無對內存滿的事情發生。

  • 爲什麼不會 OOM 呢?
    把 GC 信息打印出來 : @VM -verbose:gc -XX:+PrintGCDetails
...
[Full GC (System.gc()) [PSYoungGen: 64K->0K(38400K)] [ParOldGen: 716K->716K(87552K)] 780K->716K(125952K), [Metaspace: 3145K->3145K(1056768K)], 0.0041255 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC (System.gc()) [PSYoungGen: 665K->64K(38400K)] 1382K->780K(125952K), 0.0009704 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 64K->0K(38400K)] [ParOldGen: 716K->716K(87552K)] 780K->716K(125952K), [Metaspace: 3145K->3145K(1056768K)], 0.0042594 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC (System.gc()) [PSYoungGen: 665K->96K(38400K)] 1382K->812K(125952K), 0.0008948 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 96K->0K(38400K)] [ParOldGen: 716K->716K(87552K)] 812K->716K(125952K), [Metaspace: 3145K->3145K(1056768K)], 0.0033712 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC (System.gc()) [PSYoungGen: 665K->64K(38400K)] 1382K->780K(125952K), 0.0014349 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 64K->0K(38400K)] [ParOldGen: 716K->716K(87552K)] 780K->716K(125952K), [Metaspace: 3145K->3145K(1056768K)], 0.0147563 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 38400K, used 1996K [0x0000000795580000, 0x0000000798000000, 0x00000007c0000000)
  eden space 33280K, 6% used [0x0000000795580000,0x0000000795773370,0x0000000797600000)
  from space 5120K, 0% used [0x0000000797600000,0x0000000797600000,0x0000000797b00000)
  to   space 5120K, 0% used [0x0000000797b00000,0x0000000797b00000,0x0000000798000000)
 ParOldGen       total 87552K, used 716K [0x0000000740000000, 0x0000000745580000, 0x0000000795580000)
  object space 87552K, 0% used [0x0000000740000000,0x00000007400b3350,0x0000000745580000)
 Metaspace       used 3154K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 341K, capacity 388K, committed 512K, reserved 1048576K
  • 結論: JVM 不斷地進行 Full GC,因此不會造成內存不足。

2.2 加上 -XX:+DisableExplicitGC 後

  • 這句話的效果是:禁止 System.gc(),也就是禁止主動調用垃圾回收

輸出:

Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
	at java.nio.Bits.reserveMemory(Bits.java:694)
	at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
	at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
	at indi.sword.util.basic.reference._07_00_TestDirectByteBuffer.main(_07_00_TestDirectByteBuffer.java:12)
Heap
 PSYoungGen      total 38400K, used 6017K [0x0000000795580000, 0x0000000798000000, 0x00000007c0000000)
  eden space 33280K, 18% used [0x0000000795580000,0x0000000795b60680,0x0000000797600000)
  from space 5120K, 0% used [0x0000000797b00000,0x0000000797b00000,0x0000000798000000)
  to   space 5120K, 0% used [0x0000000797600000,0x0000000797600000,0x0000000797b00000)
 ParOldGen       total 87552K, used 0K [0x0000000740000000, 0x0000000745580000, 0x0000000795580000)
  object space 87552K, 0% used [0x0000000740000000,0x0000000740000000,0x0000000745580000)
 Metaspace       used 3154K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 343K, capacity 388K, committed 512K, reserved 1048576K
  • 結論:禁止 System.gc(),JVM 內存不足了,爲什麼會這樣呢???
    那麼肯定是 allocateDirect 有調用 System.gc()。

=== 點擊查看top目錄 ===

三、DirectByteBuffer 源碼剖析

3.1 allocateDirect 方法

  • ByteBuffer#allocateDirect 方法
    public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
    }

=== 點擊查看top目錄 ===

3.2 構造方法 DirectByteBuffer(int)

  • DirectByteBuffer#DirectByteBuffer(int) 方法
 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));
        //=== 3.3 告訴內存管理器要分配內存
        Bits.reserveMemory(size, cap);

        long base = 0;
        try {
        	// 3.4 分配直接內存
            base = unsafe.allocateMemory(size);
        } catch (OutOfMemoryError x) {
	        // 3.5 通知 bits 釋放內存
            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;
        }
        // 3.6 創建Cleaner!!!! 重點講這個
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }

=== 點擊查看top目錄 ===

3.3 reserveMemory 方法

  • Bits#reserveMemory 方法
static void reserveMemory(long size, int cap) {
        if (!memoryLimitSet && VM.isBooted()) {
        	// 初始化maxMemory,就是使用-XX:MaxDirectMemorySize指定的最大直接內存大小
            maxMemory = VM.maxDirectMemory();
            memoryLimitSet = true;
        }

        //=== 3.3.1 第一次先採取最樂觀的方式直接嘗試告訴Bits要分配內存
        if (tryReserveMemory(size, cap)) {
            return;
        }
        // 內存獲取失敗 往下走。。。
        
        // === 3.3.2 獲取 JavaLangRefAccess
        final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();

        // retry while helping enqueue pending Reference objects
        // which includes executing pending Cleaner(s) which includes
        // Cleaner(s) that free direct buffer memory
        /*
        	tryHandlePendingReference方法:消耗 pending 隊列,1. 丟到 Enqueue隊列,2. 調用 cleaner.clean() 方法釋放內存。
        	=== 3.3.3 嘗試多次獲取
        	失敗:tryHandlePendingReference方法的 pending 隊列完盡
        	成功:釋放了空間,tryReserveMemory 成功
        */
        while (jlra.tryHandlePendingReference()) { // 這個地方返回 false ,也就是 pending 隊列完盡,就返回 false
            if (tryReserveMemory(size, cap)) {
                return;
            }
        }

        // trigger VM's Reference processing
        //=== 3.3.4 Full GC 看到沒有!!!! System.gc()在這
        System.gc();

        // a retry loop with exponential back-off delays
        // (this gives VM some time to do it's job)
        // === 3.3.5 9次循環,不斷延遲要求分配內存
        boolean interrupted = false;
        try {
            long sleepTime = 1;
            int sleeps = 0;
            // 不斷*2,按照1ms,2ms,4ms,...,256ms的等待間隔嘗試9次分配內存
            while (true) {
                if (tryReserveMemory(size, cap)) {
                    return;
                }
                if (sleeps >= MAX_SLEEPS) { //最多循環9次, final int MAX_SLEEPS = 9;
                    break;
                }
                if (!jlra.tryHandlePendingReference()) {
                    try {
                        Thread.sleep(sleepTime);
                        sleepTime <<= 1; // 左邊移動一位,也就 * 2
                        sleeps++; 
                    } catch (InterruptedException e) {
                        interrupted = true;
                    }
                }
            }

            // no luck
            throw new OutOfMemoryError("Direct buffer memory");

        } finally {
            if (interrupted) {
                // don't swallow interrupts
                Thread.currentThread().interrupt();
            }
        }
    }

=== 點擊查看top目錄 ===

3.3.1 tryReserveMemory 方法
  • Bits#tryReserveMemory 方法
// -XX:MaxDirectMemorySize限制的是總cap,而不是真實的內存使用量,(在頁對齊的情況下,真實內存使用量和總cap是不同的)
private static boolean tryReserveMemory(long size, int cap) {
        // -XX:MaxDirectMemorySize limits the total capacity rather than the
        // actual memory usage, which will differ when buffers are page
        // aligned.
        long totalCap;
        while (cap <= maxMemory - (totalCap = totalCapacity.get())) {
            if (totalCapacity.compareAndSet(totalCap, totalCap + cap)) {
                reservedMemory.addAndGet(size);
                count.incrementAndGet();
                return true;
            }
        }

        return false;
    }

=== 點擊查看top目錄 ===

3.3.2 getJavaLangRefAccess 方法
  • SharedSecrets.getJavaLangRefAccess()
    這個是個啥呀?
    public static JavaLangRefAccess getJavaLangRefAccess() {
        return javaLangRefAccess;
    }

看下 set 方法的引用(Idea中 Fn + Option + 7 )

在這裏插入圖片描述

原來是存在與 Reference 類的 static 塊裏面

看下:【JAVA Reference】ReferenceQueue 與 Reference 源碼剖析(二)# Reference的static代碼塊

3.3.3 tryHandlePendingReference 方法
  • JavaLangRefAccess#tryHandlePendingReference 方法
package sun.misc;

public interface JavaLangRefAccess {

    /**
     * Help ReferenceHandler thread process next pending
     * {@link java.lang.ref.Reference}
     *
     * @return {@code true} if there was a pending reference and it
     *         was enqueue-ed or {@code false} if there was no
     *         pending reference
     */
    boolean tryHandlePendingReference();
}

在這裏插入圖片描述

        while (jlra.tryHandlePendingReference()) { // 這個地方返回 false ,也就是 pending 隊列沒有了,就返回 false
            if (tryReserveMemory(size, cap)) {
                return;
            }
        }

while 能夠把當前全部 pending 隊列中的 reference 都消化掉,要麼Enqueue,要麼Cleaner去進行 clean() 操作。

=== 3.3.3 while 死循環嘗試申請內存
tryHandlePendingReference方法:消耗 pending 隊列,1. 丟到 Enqueue隊列,2. 調用 cleaner.clean() 方法釋放內存。
失敗:tryHandlePendingReference方法的 pending 隊列完盡
成功:釋放了空間,tryReserveMemory 成功
只有這樣消耗光了 pending,纔會往下走 === 3.3.4 System.gc() ==

=== 點擊查看top目錄 ===

3.3.4 System.gc()
  • 假如上述的步驟還是沒能釋放內存的話,那麼將會觸發 Full GC。但我們知道,調用System.gc()並不能夠保證full gc馬上就能被執行。所以在後面代碼中,會進行最多MAX_SLEEPS = 9次嘗試,看是否有足夠的可用堆外內存來分配堆外內存。並且每次嘗試之前,都對延遲(*2)等待時間,已給JVM足夠的時間去完成full gc操作。
  • 這個地方要注意:如果設置了-XX:+DisableExplicitGC,將會禁用顯示GC,這會使System.gc()調用無效。

=== 點擊查看top目錄 ===

3.3.5 獲取內存的9次嘗試
  • 如果9次嘗試後依舊沒有足夠的可用堆外內存來分配本次堆外內存,則 throw new OutOfMemoryError(“Direct buffer memory”);

=== 點擊查看top目錄 ===

3.4 allocateMemory 方法

在這裏插入圖片描述

  • sun.misc.Unsafe#allocateMemory
public native long allocateMemory(long bytes);
  • 看下 unsafe
class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer{
    // Cached unsafe-access object
    protected static final Unsafe unsafe = Bits.unsafe();
}
  • java.nio.Bits#unsafe
class Bits {   
	private static final Unsafe unsafe = Unsafe.getUnsafe();
	static Unsafe unsafe() {
   		return unsafe;
	}
}
  • sun.misc.Unsafe#getUnsafe
    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class<?> caller = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(caller.getClassLoader()))
            throw new SecurityException("Unsafe");
        return theUnsafe;
    }
  • 總結: ByteBuffer.allocateDirect(int capacity) 的底層是 unsafe.allocateMemory()
3.4.1 Unsafe類

Java提供了Unsafe類用來進行直接內存的分配與釋放

public native long allocateMemory(long var1);
public native void freeMemory(long var1);

=== 點擊查看top目錄 ===

3.5 unreserveMemory 方法

  • java.nio.Bits#unreserveMemory
    static void unreserveMemory(long size, int cap) {
        long cnt = count.decrementAndGet();
        long reservedMem = reservedMemory.addAndGet(-size);
        long totalCap = totalCapacity.addAndGet(-cap);
        assert cnt >= 0 && reservedMem >= 0 && totalCap >= 0;
    }

=== 點擊查看top目錄 ===

3.6 構造 Cleaner對象

        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));

這個create靜態方法提供給我們來實例化Cleaner對象,需要兩個參數:

  1. 被引用的對象
  2. 實現了Runnable接口的對象,這個用來回調的時候執行內部的 run 方法
    新創建的Cleaner對象被加入到了 dummyQueue 隊列裏。

Cleaner#create方法「【JAVA Reference】Cleaner 源碼剖析(三)」

=== 點擊查看top目錄 ===

3.6.1 Deallocator 對象
  • 內部類 java.nio.DirectByteBuffer.Deallocator
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;
            }
            // 3.6.2 調用C++代碼釋放堆外內存
            unsafe.freeMemory(address);
            address = 0; // 設置爲0,表示已經釋放了
            // 剛剛的 3.5 釋放後,標記資源
            Bits.unreserveMemory(size, capacity);
        }

    }

=== 點擊查看top目錄 ===

3.6.2 freeMemory 方法
  /**
     * Disposes of a block of native memory, as obtained from {@link
     * #allocateMemory} or {@link #reallocateMemory}.  The address passed to
     * this method may be null, in which case no action is taken.
     *
     * @see #allocateMemory
     */
    public native void freeMemory(long address);

=== 點擊查看top目錄 ===

四、總結

4.1 tryHandlePendingReference 的調用場景
  1. 幽靈線程死循環調用 看下:【JAVA Reference】ReferenceQueue 與 Reference 源碼剖析(二)# Reference的static代碼塊
  2. 申請內存的時候,會調用(本文)
4.2 堆外緩存的特點
  1. 對垃圾回收停頓的改善可以明顯感覺到
  2. 對於大內存有良好的伸縮性
  3. 在進程間可以共享,減少虛擬機間的複製
  • netty 就是使用堆外緩存,可以減少數據的複製操作,提高性能
4.3 使用堆外內存的原因
  1. 可以一定程度改善垃圾回收停頓的影響。full gc 意味着徹底回收,過大的堆會影響Java應用的性能。如果使用堆外內存的話,堆外內存是直接受操作系統管理( 而不是JVM )。
  2. 在某些場景下可以提升程序I/O操縱的性能。減少了將數據從堆內內存拷貝到堆外內存的步驟。
4.4 對外內存的使用場景
  1. 直接的文件拷貝操作,或者I/O操作。直接使用堆外內存就能少去內存從用戶內存拷貝到系統內存的操作,因爲I/O操作是系統內核內存和設備間的通信,而不是通過程序直接和外設通信的。
  2. 堆外內存適用於生命週期中等或較長的對象。( 如果是生命週期較短的對象,在YGC的時候就被回收了,就不存在大內存且生命週期較長的對象在FGC對應用造成的性能影響 )。

同時,還可以使用池+堆外內存 的組合方式,來對生命週期較短,但涉及到I/O操作的對象進行堆外內存的再使用。( Netty中就使用了該方式 )

五、番外篇

下一章節:【JAVA Reference】Finalizer 剖析 (六)
上一章節:【JAVA Reference】Cleaner 對比 finalize 對比 AutoCloseable(四)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章