JVM執行時資料區探索與直接記憶體的使用

首席菜鳥發表於2018-04-01

話不多說,先上簡圖(<JDK1.8):


從圖中可以看出JVM的執行時資料區大致可以分為資料和指令兩塊內容,指令這塊本質上也屬於資料,不過大部分資料跟指令有關係。右邊有3個部分都是執行緒私有的,計數器儲存了當前執行緒執行的位元組碼指令的地址,不過這僅限於java方法,如果時native方法那這個計數器時為null的。(檢視位元組碼可以在命令列使用javap -v class檔名),而在虛擬機器棧裡面,一個java方法對應一個棧幀,每個棧幀儲存了該方法的區域性變數表和運算元棧以及動態連結等等,因此執行緒執行的過程相當於棧幀出棧的過程。在方法執行的過程中,資料在棧幀中時如何運轉的呢,下面來舉個簡單的例子:

public  void  add(int i){
        int a=0;
        int b=1;
        a = b+i;
        
    }

在執行該方法前,區域性變數表先把變數i,a,b(位置為0,1,2)儲存,然後執行相關的命令時就從變數表移到運算元棧,每次指令到來的時候就從運算元棧彈出資料並運算,再將結果壓入運算元棧。具體的位元組碼如下:

   stack=2, locals=3, args_size=1
         0: iconst_0  //把整數 0 壓入運算元棧
         1: istore_1
         2: iconst_1  //把整數 1 壓入運算元棧
         3: istore_2   // 把棧頂的內容放入區域性變數表中索引為 2 的 slot 中
         4: iload_2 // 把區域性變數表索引為 2 的 slot 中存放的變數值(b)載入至運算元棧
         5: iload_0  // 把區域性變數表索引為 0 的 slot 中存放的變數值(i)載入至運算元棧
         6: iadd   //棧頂的兩個數出棧後相加,結果入棧
         7: istore_1
         8: return  //有興趣的同學可以去查詢一下相關指令的說明

本地方法棧與虛擬機器棧的作用類似。方法區又被稱為堆的邏輯部分(JDK1.8移除了它),它主要儲存靜態變數以及已載入的類資訊還有一些編譯後的程式碼等等。堆空間是我們比較關心的部分,也是GC工作的主要部分,它主要存放了例項物件以及陣列物件和常量池。

        還有一部分記憶體是直接記憶體,但是它並不是JVM執行時資料區的一部分,所以它並不在GC收集器的執行範圍內。下面我們來追蹤這部分記憶體的建立和回收過程。建立的語句如下:

 ByteBuffer  a = ByteBuffer.allocate(1024);

不能直接使用DirectByteBuffer的原因是它所有的構造器都沒有字首,也就是訪問許可權是default,這裡我們來複習一下java的訪問修飾符

  訪問許可權   類   包  子類  其他包

   public   1   1    1     1       (對任何人都是可用的)

   protect   1   1    1     0    (繼承的類可以訪問以及和private一樣的許可權)

   default   1   1    0     0    (包訪問許可權,即在整個包內均可被訪問)

   private   1   0    0     0    (除型別建立者和型別的內部方法之外的任何人都不能訪問的元素)

只有該類的物件和同包下的其他類可以訪問到。下面是它的構造器,當然它的構造器有多個,原理都差不多:

 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);// 分配堆外記憶體(由C的malloc實現),並返回堆外記憶體的地址
        } 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)); //為它構建一個Cleaner物件用於跟蹤DirectByteBuffer物件的垃圾回收
        att = null;

    }

Cleaner物件的宣告如下:

public class Cleaner extends PhantomReference<Object>

因此可以確定在回收堆外記憶體的時候是使用了虛引用的方式,我們知道虛引用是作用和名字描述一樣,一旦GC碰到了就會將其回收並加入ReferenceQueue,通常是用來追蹤GC回收的過程的。構建這個Cleaner物件的時候還引入了一個Deallocator物件,它的實現是一個執行緒Runnable物件,它的run方法如下:

  public void run() {
            if (address == 0) {
                // Paranoia
                return;
            }
            unsafe.freeMemory(address); //使用本地方法將該地址指向的記憶體釋放
            address = 0;
            Bits.unreserveMemory(size, capacity);//在系統中儲存總分配記憶體(按頁分配)的大小和實際記憶體的大小。
        }

在Cleaner類的方法裡面存在clean方法:

        if (remove(this)) {
            try {
                this.thunk.run();  //這個thunk就是傳入的Deallocator物件
            } 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;
                    }
                });
            }

綜上可以得出結論,直接記憶體是通過本地方法建立,由GC回收例項引用,再由Cleaner的呼叫clean方法釋放記憶體。下篇將會介紹JDK1.8與JDK1.7的資料區差異以及GC回收演算法


相關文章