JVM的基本結構及其各部分詳解(一)

1 java虛擬機的基本結構如圖:

1)類加載子系統負責從文件系統或者網絡中加載Class信息,加載的類信息存放於一塊稱爲方法區的內存空間。除了類的信息外,方法區中可能還會存放運行時常量池信息,包括字符串字面量和數字常量(這部分常量信息是Class文件中常量池部分的內存映射)。

2)java堆在虛擬機啓動的時候建立,它是java程序最主要的內存工作區域。幾乎所有的java對象實例都存放在java堆中。堆空間是所有線程共享的,這是一塊與java應用密切相關的內存空間。

3)java的NIO庫允許java程序使用直接內存。直接內存是在java堆外的、直接向系統申請的內存空間。通常訪問直接內存的速度會優於java堆。因此出於性能的考慮,讀寫頻繁的場合可能會考慮使用直接內存。由於直接內存在java堆外,因此它的大小不會直接受限於Xmx指定的最大堆大小,但是系統內存是有限的,java堆和直接內存的總和依然受限於操作系統能給出的最大內存。

4)垃圾回收系統是java虛擬機的重要組成部分,垃圾回收器可以對方法區、java堆和直接內存進行回收。其中,java堆是垃圾收集器的工作重點。和C/C++不同,java中所有的對象空間釋放都是隱式的,也就是說,java中沒有類似free()或者delete()這樣的函數釋放指定的內存區域。對於不再使用的垃圾對象,垃圾回收系統會在後臺默默工作,默默查找、標識並釋放垃圾對象,完成包括java堆、方法區和直接內存中的全自動化管理。

5)每一個java虛擬機線程都有一個私有的java棧,一個線程的java棧在線程創建的時候被創建,java棧中保存着幀信息,java棧中保存着局部變量、方法參數,同時和java方法的調用、返回密切相關。

6)本地方法棧和java棧非常類似,最大的不同在於java棧用於方法的調用,而本地方法棧則用於本地方法的調用,作爲對java虛擬機的重要擴展,java虛擬機允許java直接調用本地方法(通常使用C編寫)

7)PC(Program Counter)寄存器也是每一個線程私有的空間,java虛擬機會爲每一個java線程創建PC寄存器。在任意時刻,一個java線程總是在執行一個方法,這個正在被執行的方法稱爲當前方法。如果當前方法不是本地方法,PC寄存器就會指向當前正在被執行的指令。如果當前方法是本地方法,那麼PC寄存器的值就是undefined

8)執行引擎是java虛擬機的最核心組件之一,它負責執行虛擬機的字節碼,現代虛擬機爲了提高執行效率,會使用即時編譯技術將方法編譯成機器碼後再執行。

 


 

2 java堆

java堆是和應用程序關係最爲密切的內存空間,幾乎所有的對象都存放在堆上。並且java堆是完全自動化管理的,通過垃圾回收機制,垃圾對象會被自動清理,而不需要顯示的釋放。

根據java回收機制的不同,java堆有可能擁有不同的結構。最爲常見的一種構成是將整個java堆分爲新生代和老年代。其中新生代存放新生對象或者年齡不大的對象,老年代則存放老年對象。新生代有可能分爲eden區、s0區、s1區,s0區和s1區也被稱爲from和to區,他們是兩塊大小相同、可以互換角色的內存空間。

如下圖:顯示了一個堆空間的一般結構:

在絕大多數情況下,對象首先分配在eden區,在一次新生代回收之後,如果對象還存活,則進入s0或者s1,每經過一次新生代回收,對象如果存活,它的年齡就會加1。當對象的年齡達到一定條件後,就會被認爲是老年對象,從而進入老年代。其具體的垃圾回收算法在後面會介紹。

例1 :通過簡單的示例,展示java堆、方法區和java棧之間的關係

package com.jvm;
public class SimpleHeap {
  private int id;
  public SimpleHeap(int id){
    this.id = id;
  }
  public void show(){
    System.out.println("My id is "+id);
  }

  public static void main(String[] args) {
    SimpleHeap s1 = new SimpleHeap(1);
    SimpleHeap s2 = new SimpleHeap(2);
    s1.show();
    s2.show();
  }
}

該代碼聲明瞭一個類,並在main函數中創建了兩個SimpleHeap實例。此時,各對象和局部變量的存放情況如圖:

SimpleHeap實例本身分配在堆中,描述SimpleHeap類的信息存放在方法區,main函數中的s1 s2局部變量存放在java棧上,並指向堆中兩個實例。

3 java棧

java棧是一塊線程私有的內存空間。如果說,java堆和程序數據密切相關,那麼java棧就是和線程執行密切相關。線程執行的基本行爲是函數調用,每次函數調用的數據都是通過java棧傳遞的。

java棧與數據結構上的棧有着類似的含義,它是一塊先進後出的數據結構,只支持出棧和進棧兩種操作,在java棧中保存的主要內容爲棧幀。每一次函數調用,都會有一個對應的棧幀被壓入java棧,每一個函數調用結束,都會有一個棧幀被彈出java棧。如下圖:棧幀和函數調用。函數1對應棧幀1,函數2對應棧幀2,依次類推。函數1中調用函數2,函數2中調用函數3,函數3調用函數4.當函數1被調用時,棧幀1入棧,當函數2調用時,棧幀2入棧,當函數3被調用時,棧幀3入棧,當函數4被調用時,棧幀4入棧。當前正在執行的函數所對應的幀就是當前幀(位於棧頂),它保存着當前函數的局部變量、中間計算結果等數據。

當函數返回時,棧幀從java棧中被彈出,java方法區有兩種返回函數的方式,一種是正常的函數返回,使用return指令,另一種是拋出異常。不管使用哪種方式,都會導致棧幀被彈出。

在一個棧幀中,至少包含局部變量表、操作數棧和幀數據區幾個部分。

提示:由於每次函數調用都會產生對應的棧幀,從而佔用一定的棧空間,因此,如果棧空間不足,那麼函數調用自然無法繼續進行下去。當請求的棧深度大於最大可用棧深度時,系統會拋出StackOverflowError棧溢出錯誤。

例2 使用遞歸,由於遞歸沒有出口,這段代碼可能會拋出棧溢出錯誤,在拋出棧溢出錯誤時,打印最大的調用深度

  package com.jvm;

  public class TestStackDeep {
    private static int count =0;
    public static void recursion(){
      count ++;
      recursion();
    }

    public static void main(String[] args) {
      try{
        recursion();
      }catch(Throwable e){
        System.out.println("deep of calling ="+count);
        e.printStackTrace();
      }
    }
  }

使用參數-Xss128K執行上面代碼(在eclipse中右鍵選擇Run As-->run Configurations....設置Vm arguments),部分結果如圖:

可以看出,在進行大約1079次調用之後,發生了棧溢出錯誤,通過增大-Xss的值,可以獲得更深的層次調用,嘗試使用參數-Xss256K執行上述代碼,可能產生如下輸出,很明顯,調用層次有明顯的增加:

注意:函數嵌套調用的層次在很大程度上由棧的大小決定,棧越大,函數支持的嵌套調用次數就越多。

3.1 棧幀組成之局部變量表

局部變量表是棧幀的重要組成部分之一。它用於保存函數的參數以及局部變量,局部變量表中的變量只在當前函數調用中有效,當函數調用結束,隨着函數棧幀的彈出銷燬,局部變量表也會隨之銷燬。

由於局部變量表在棧幀之中,因此,如果函數的參數和局部變量很多,會使得局部變量表膨脹,從而每一次函數調用就會佔用更多的棧空間,最終導致函數的嵌套調用次數減少。

示例3:一個recursion函數含有3個參數和10個局部變量,因此,其局部變量表含有13個變量,而第二個recursion函數不再含有任何參數和局部變量,當這兩個函數被嵌套調用時,第二個recursion函數可以擁有更深的調用層次。

package com.jvm;

public class TestStackDeep2 {
  private static int count = 0;
  public static void recursion(long a,long b,long c){
    long e=1,f=2,g=3,h=4,i=5,k=6,q=7,x=8,y=9,z=10;
    count ++;
    recursion(a,b,c);
  }
  public static void recursion(){
    count++;
    recursion();
  }

  public static void main(String[] args) {
    try{
      recursion(0L,0L,0L);
      //recursion();
    }catch(Throwable e){
      System.out.println("deep of calling = "+count);
      e.printStackTrace();
    }
  }
}

使用參數-Xss128K執行上述代碼中的第一個帶參recursion(long a,long b,long c)函數,輸出結果爲:

使用虛擬機參數-Xss128K執行上述代碼中第二個不帶參數的recursion()函數(當然需要把第一個函數註釋掉),輸出結果爲:

可以看出,在相同的棧容量下,局部變量少的函數可以支持更深的函數調用。

使用jclasslib工具可以查看函數的局部變量表,如下圖:最大局部變量表大小

該圖顯示了第一個帶參recursion(long a,long b,long c)的最大局部變量表的大小爲26個字,因爲該函數包含總共13個參數和局部變量,且都爲long型,long和double在局部變量表中需要佔用2個字,其他如int short byte 對象引用等佔用一個字。

說明:字(word)指的是計算機內存中佔據一個單獨的內存單元編號的一組二進制串,一般32位計算機上一個字爲4個字節長度。

通過jclasslib工具查看該類的Class文件中局部變量表的內容,(這裏說的局部變量表和上述說的局部變量表不同,這裏指Class文件的一個屬性,而上述的局部變量表指java棧空間的一部分

可以看到,在Class文件的局部變量表中,顯示了每個局部變量的作用域範圍、所在槽位的索引(index列)、變量名(name列)和數據類型(J表示long型)

棧幀中局部變量表中的槽位是可以重用的,如果一個局部變量過了其作用域,那麼在其作用域之後申明的新的局部變量就很有可能會複用過期局部變量的槽位,從而達到節省資源的目的。

示例4 :顯示局部變量表的複用,在localvar1函數中,局部變量a和b都作用到了函數的末尾,故b無法複用a所在的位置。而在localvar2()函數中,局部變量a在第?行不再有效,故局部變量b可以複用a的槽位(1個字)

package com.jvm;

public class TestReuse {
  public static void localvar1(){
    int a=0;
    System.out.println(a);
    int b=0;
  }
  public static void localvar2(){
    {
      int a=0;
      System.out.println(a);
    }
    int b=0;
  }
}

如圖顯示localvar1()函數的局部變量表,該函數局部變量大小爲2個字,(最大局部變量表中一般第一個局部變量槽位是this引用)第一個槽位是變量a,第二個槽位是變量b,每個變量佔一個字。

而localvar2()函數的局部變量表信息如下圖,雖然和localvar1()一樣,但是b複用了a的槽位,(從他們都佔用同一個槽位index都是0可以看出),因此在整個函數執行中,同時存在的局部變量爲1字。

局部變量表中的變量也是垃圾回收根節點,只要被局部變量表中直接或者間接引用的對象都是不會被回收的。

示例5:通過一個簡單示例,展示局部變量對垃圾回收的影響

package com.jvm;

public class LocalvarGC {
  public void localvarGc1(){
    byte[] a = new byte[6*1024*1024];//6M
    System.gc();
  }
  public void localvarGc2(){
    byte[] a = new byte[6*1024*1024];
    a = null;
    System.gc();
  } 
  public void localvarGc3(){
    {
      byte[] a = new byte[6*1024*1024];
    }
    System.gc();
  } 
  public void localvarGc4(){
    {
      byte[] a = new byte[6*1024*1024];
    }
    int c = 10;
    System.gc();
  } 
  public void localvarGc5(){
    localvarGc1();
    System.gc();
  } 
  public static void main(String[] args) {
    LocalvarGC ins = new LocalvarGC();
    ins.localvarGc1();
  }
}

每一個localvarGcN()函數都分配了一塊6M的堆內存,並使用局部變量引用這塊空間。

在localvarGc1()中,在申請空間後,立即進行垃圾回收,很明顯由於byte數組被變量a引用,因此無法回收這塊空間。

在localvarGc2()中,在垃圾回收前,先將變量a置爲null,使得byte數組失去強引用,故垃圾回收可以順利回收byte數組。

在localvarGc3()中,在進行垃圾回收前,先使局部變量a失效,雖然變量a已經離開了作用域,但是變量a依然存在於局部變量表中,並且也指向這塊byte數組,故byte數組依然無法被回收。

對於localvarGc4(),在垃圾回收之前,不僅使變量a失效,更是聲明瞭變量c,使變量c複用了變量a的字,由於變量a此時被銷燬,故垃圾回收器可以順利回收數組byte

對於localvarGc5(),它首先調用了localvarGc1(),很明顯,在localvarGc1()中並沒有釋放byte數組,但在localvarGc1()返回後,它的棧幀被銷燬,自然也包含了棧幀中的所有局部變量,故byte數組失去了引用,在localvarGc5()的垃圾回收中被回收。

可以使用-XX:+printGC執行上述幾個函數,在輸出日誌裏,可以看到垃圾回收前後堆的大小,進而推斷出byte數組是否被回收。

下面的輸出是函數localvarGc4()的運行結果:

[GC (System.gc()) 7618K->624K(94208K), 0.0015613 secs]
[Full GC (System.gc()) 624K->526K(94208K), 0.0070718 secs]

從日誌中可以看出,堆空間從回收前的7618K變爲回收後的624K,釋放了>6M的空間,byte數組已經被回收釋放。

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