Java直接內(nèi)存分配與釋放原理

前言

在Java中分配直接內(nèi)存大有如下三種主要方式:

  1. Unsafe.allocateMemory()
  2. ByteBuffer.allocateDirect()
  3. native方法

Unsafe類

Java提供了Unsafe類用來進(jìn)行直接內(nèi)存的分配與釋放

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

示例

public class DirectMemoryMain {
    public static void main(String[] args) throws InterruptedException {
        Unsafe unsafe = getUnsafe();
        while (true) {
            for (int i = 0; i < 10000; i++) {
                long address = unsafe.allocateMemory(10000);
                // System.out.println(address);
                // unsafe.freeMemory(address);
            }
            Thread.sleep(1);
        }
    }

    // Unsafe無法直接使用,需要通過反射來獲取
    private static Unsafe getUnsafe() {
        try {
            Class clazz = Unsafe.class;
            Field field = clazz.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            return (Unsafe) field.get(null);
        } catch (IllegalAccessException | NoSuchFieldException e) {
            throw new RuntimeException(e);
        }
    }
}

下面為這段代碼的演示效果,其中JVM最大內(nèi)存設(shè)為64M,而真實(shí)內(nèi)存則可以無限增長。

DirectByteBuffer類

內(nèi)存分配

雖然Unsafe可以通過反射調(diào)用來進(jìn)行內(nèi)存分配,但是按照其設(shè)計(jì)方式,它并不是給開發(fā)者來使用的,而且Unsafe里面的方法也十分原始,更像是一個(gè)底層設(shè)施。而其上層的封裝則是DirectByteBuffer,這個(gè)才是最終留給開發(fā)者使用的。DirectByteBuffer的分配是通過ByteBuffer.allocateDirect(int capacity)方法來實(shí)現(xiàn)的。

DirectByteBuffer申請內(nèi)存的源碼如下:

DirectByteBuffer(int cap) {
    super(-1, 0, cap, cap);
    
    // 計(jì)算需要分配的內(nèi)存大小
    boolean pa = VM.isDirectMemoryPageAligned();
    int ps = Bits.pageSize();
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    
    // 告訴內(nèi)存管理器要分配內(nèi)存
    Bits.reserveMemory(size, cap);

    // 分配直接內(nèi)存
    long base = 0;
    try {
        base = unsafe.allocateMemory(size);
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    unsafe.setMemory(base, size, (byte) 0);
    
    // 計(jì)算內(nèi)存的地址
    if (pa && (base % ps != 0)) {
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    
    // 創(chuàng)建Cleaner
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;
}

整個(gè)DirectByteBuffer分配過程中,比較需要關(guān)注的Bits.reserveMemory()和Cleaner,Deallocator,其中Bits.reserveMemory()與分配相關(guān),Cleaner、Deallocator則與內(nèi)存釋放相關(guān)。

Bits.reserveMemory()

static void reserveMemory(long size, int cap) {

    // 初始化maxMemory,就是使用-XX:MaxDirectMemorySize指定的最大直接內(nèi)存大小
    if (!memoryLimitSet && VM.isBooted()) {
        maxMemory = VM.maxDirectMemory();
        memoryLimitSet = true;
    }

    // 第一次先采取最樂觀的方式直接嘗試告訴Bits要分配內(nèi)存
    if (tryReserveMemory(size, cap)) {
        return;
    }

    final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();

    // 嘗試執(zhí)行Cleaner來釋放直接內(nèi)存,直到內(nèi)存空間足夠
    while (jlra.tryHandlePendingReference()) {
        if (tryReserveMemory(size, cap)) {
            return;
        }
    }

    // GC
    System.gc();

    // 按照1ms,2ms,4ms,...,256ms的等待間隔嘗試9次分配內(nèi)存
    boolean interrupted = false;
    try {
        long sleepTime = 1;
        int sleeps = 0;
        while (true) {
            if (tryReserveMemory(size, cap)) {
                return;
            }
            if (sleeps >= MAX_SLEEPS) {
                break;
            }
            if (!jlra.tryHandlePendingReference()) {
                try {
                    Thread.sleep(sleepTime);
                    sleepTime <<= 1;
                    sleeps++;
                } catch (InterruptedException e) {
                    interrupted = true;
                }
            }
        }

        throw new OutOfMemoryError("Direct buffer memory");

    } finally {
        if (interrupted) {
            Thread.currentThread().interrupt();
        }
    }
}
// -XX:MaxDirectMemorySize限制的是總cap,而不是真實(shí)的內(nèi)存使用量,(在頁對齊的情況下,真實(shí)內(nèi)存使用量和總cap是不同的)
private static boolean tryReserveMemory(long size, int cap) {
    long totalCap;
    while (cap <= maxMemory - (totalCap = totalCapacity.get())) {
        if (totalCapacity.compareAndSet(totalCap, totalCap + cap)) {
            reservedMemory.addAndGet(size);
            count.incrementAndGet();
            return true;
        }
    }

    return false;
}

內(nèi)存釋放

內(nèi)存釋放是通過Cleaner和Deallocator來實(shí)現(xiàn)的。

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;
        }
        unsafe.freeMemory(address);
        address = 0;
        Bits.unreserveMemory(size, capacity);
    }
}

這個(gè)類中主要方法為run(),里面的步驟也很簡單,包含兩步

  • 使用unsafe釋放內(nèi)存
  • 利用Bits管理內(nèi)存的釋放,就是標(biāo)記一下該內(nèi)存已釋放

每個(gè)DirectByteBuffer都有一個(gè)相對應(yīng)的Deallocator,而Deallocator則是由Cleaner來進(jìn)行調(diào)度。

Cleaner

Cleaner的數(shù)據(jù)結(jié)構(gòu)為一個(gè)雙向鏈表,如下

private static Cleaner first = null;  // 鏈表的頭節(jié)點(diǎn)
private Cleaner next = null;  // 下一個(gè)節(jié)點(diǎn)
private Cleaner prev = null;  // 上一個(gè)節(jié)點(diǎn)
private final Runnable thunk;   // 存放Deallocator

Cleaner中主要包含如下操作,add, remove,clean

主要操作

1. add

private static synchronized Cleaner add(Cleaner var0) {
    if (first != null) {
        var0.next = first;
        first.prev = var0;
    }

    first = var0;
    return var0;
}

add操作就是不斷地將新的Cleaner節(jié)點(diǎn)添加在鏈表頭部,之后將頭節(jié)點(diǎn)指針指向新的Cleaner

2. remove

private static synchronized boolean remove(Cleaner var0) {
    if (var0.next == var0) { // 已經(jīng)移除,防止重復(fù)移除
        return false;
    } else {
        if (first == var0) {
            if (var0.next != null) {
                first = var0.next;
            } else {
                first = var0.prev;
            }
        }

        if (var0.next != null) {
            var0.next.prev = var0.prev;
        }

        if (var0.prev != null) {
            var0.prev.next = var0.next;
        }

        var0.next = var0;
        var0.prev = var0;
        return true;
    }
}

remove操作就是將Cleaner節(jié)點(diǎn)從鏈表中刪除

3. clean

public void clean() {
    if (remove(this)) {
        try {
            this.thunk.run();
        } catch (final Throwable var2) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    if (System.err != null) {
                        (new Error("Cleaner terminated abnormally", var2)).printStackTrace();
                    }

                    System.exit(1);
                    return null;
                }
            });
        }

    }
}

clean操作則是移除Cleaner節(jié)點(diǎn)并調(diào)用Deallocator的run()方法

清理過程

疑問 Cleaner.clean()又是由誰在何時(shí)調(diào)用的呢?

仔細(xì)觀察可以發(fā)現(xiàn),Cleaner繼承了PhantomReference,其referent為DirectByteBuffer

Reference

在Reference初次加載的過程中會調(diào)用一段靜態(tài)代碼

static {
    ThreadGroup tg = Thread.currentThread().getThreadGroup();
    for (ThreadGroup tgn = tg;
         tgn != null;
         tg = tgn, tgn = tg.getParent());
    Thread handler = new ReferenceHandler(tg, "Reference Handler");
    handler.setPriority(Thread.MAX_PRIORITY);
    handler.setDaemon(true);
    handler.start();

    // provide access in SharedSecrets
    SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
        @Override
        public boolean tryHandlePendingReference() {
            return tryHandlePending(false);
        }
    });
}

這段代碼中包含了兩種可以調(diào)用Cleaner的方式:

  • ReferenceHandler,會不停地循環(huán)調(diào)用tryHandlePending
  • SharedSecrets.JavaLangRefAccess,在Bits.reserveMemory()中被調(diào)用

事實(shí)上直接內(nèi)存的回收過程也的確是由這兩種方式混合組成,這兩種方式有一個(gè)共同點(diǎn),他們都會調(diào)用Reference.tryHandlePending()方法。

static boolean tryHandlePending(boolean waitForNotify) {
    Reference<Object> r;
    Cleaner c;
    try {
        synchronized (lock) {
            if (pending != null) {
                r = pending;
                c = r instanceof Cleaner ? (Cleaner) r : null;
                pending = r.discovered;
                r.discovered = null;
            } else {
                if (waitForNotify) {
                    lock.wait();
                }
                return waitForNotify;
            }
        }
    } catch (OutOfMemoryError x) {
        Thread.yield();
        return true;
    } catch (InterruptedException x) {
        return true;
    }

    if (c != null) {
        c.clean();
        return true;
    }

    ReferenceQueue<? super Object> q = r.queue;
    if (q != ReferenceQueue.NULL) q.enqueue(r);
    return true;
}

其中pending和discovered由JVM來操作,兩個(gè)共同組成一個(gè)等待隊(duì)列鏈表,對于PhantomReference的情況,當(dāng)對象不存在其他引用,便會直接加入等待隊(duì)列。每當(dāng)?shù)却?duì)列中出現(xiàn)Cleaner,就會執(zhí)行其clean()方法。

總結(jié)

1. 整個(gè)DirectByteBuffer的分配與釋放流程如下

2. -XX:MaxDirectMemorySize參數(shù)只對由DirectByteBuffer分配的內(nèi)存有效,對Unsafe直接分配的內(nèi)存無效

native方法

疑問 native方法中分配的內(nèi)存是否是屬于DirectByteBuffer對象呢?

這個(gè)疑問來自于一次內(nèi)存泄漏問題的排查,一直沒有機(jī)會去研究,正好借這次機(jī)會尋找一下該問題的答案。

demo

寫了一個(gè)簡單的demo程序如下

// java部分
public class NativeMain {
    public native void allocateMemory();

    static {
        System.setProperty("java.library.path", ".");
        System.loadLibrary("nativemain");
    }

    public static void main(String[] args) throws Exception {
        NativeMain nativeMain = new NativeMain();
        while (true) {
            for (int i = 0; i < 10000; i++) {
                nativeMain.allocateMemory();
            }
            Thread.sleep(1);
        }
    }
}
// c++實(shí)現(xiàn)部分
#include "jni.h"
#include "NativeMain.h"
#include <stdlib.h>

JNIEXPORT void JNICALL Java_NativeMain_allocateMemory(JNIEnv *, jobject) {
    char *ptr = (char*)malloc(1000);
}

運(yùn)行發(fā)現(xiàn)native方法分配的內(nèi)存并不會產(chǎn)生DirectByteBuffer對象,同樣的也不受-XX:MaxDirectMemorySize影響。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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