文章出處

譯者語                               

  為加深對JVM的了解和日后查閱時更方便,于是對原文進行翻譯。內容是建立在我對JVM的認識的基礎上翻譯的,加上本人的英語水平有限,若有紕漏請大家指正,謝謝。

  原文地址:http://blog.jamesdbloom.com/JVMInternals.html

 

一、前言                              

  本文將介紹JVM內部架構。下圖展示符合Java7規范的JVM內部主要組件。

  

  下面我們將上述組件分為線程相關和線程獨立兩種類型來介紹。

二、目錄                              

Thread
  Per Thread
    program Counter (PC)
    Stack
    Native Stack
    Frame
      Local Variables Array
      Operand Stack
      Dynamic Linking
Shared Between Threads
  Heap
  Memory Management
  Non-Heap Memory
  Just In Time (JIT) Compilation
  Method Area
  Class File Structure
  Classloader
  Faster Class Loading
  Where Is The Method Area
  Classloader Reference
  Run Time Constant Pool
  Exception Table
  Symbol Table
  Interned Strings (String Table)

Thread                            

  JVM允許進程包含多個并發的線程。Hotspot JVM中的Java線程與OS線程是一一對應的。當線程工作存儲區(thread-local storage)、配置緩存(allocation buffers)、同步對象(synchronized objects)、棧和本地棧(stacks)和程序計數器(pragram counter)等Java線程相關的狀態均準備好后,就會啟動OS線程并有OS線程執行run函數。OS負責線程的調度。當以正常方式或異常拋出的方式退出run函數,OS線程均會判斷當前Java線程的終止是否會導致進程的終止(進程的工作線程是否都終止了?),若要終止進程的化,則釋放Java線程和OS線程所占的資源,否則就釋放Java線程的資源,并回收OS線程。

  JVM System Threads

    若你用過jconsole或其他調試工具,你會發現除了主線程外還存在數個有JVM創建的系統線程。Hotspot JVM的系統線程有這5個:

    1. VM thread(虛擬機線程)

    VM thread 用于為一些需要防止堆變化操作提供執行環境,當要執行防止堆變化的操作時,就是要求JVM啟動安全點(safe-point),此時將會暫停GC、線程棧操作、線程恢         復和偏向鎖解除。

  2. Periodic task thread(周期性任務線程)

    Periodic task thread負責定時事件(如interrupts),用于周期性執行計劃任務

  3. GC threads(垃圾回收線程)

    GC threads 負責不同類型垃圾回收活動。

    4. Compiler threads(編譯器線程)

      Compiler threads用于在運行時將字節碼編譯為CPU本地代碼。

  5. Signal dispatcher thread(信號量分發線程)

    Singal dispatcher thread用于接收發送給JVM的信號量,并將其分發到合適的JVM方法來處理。

三、Per Thread                          

  每個線程的執行環境均有以下的組件。

  1. Program Counter(PC)(程序計數器)

  用于存放當前指令(或操作碼)的地址,若該指令為本地指令那么PC為undefined。當執行完當前指令后PC會自增(根據當前指令的定義自增1或N)從而指向下一個指令的地  址,那么JVM就可以知道接下來要執行哪個指令了。事實上PC存放的是方法區(Method Area)中的內存地址。

  2. Stack(堆棧)

  每個線程有自定獨立的堆棧用于存放在該線程執行的方法。堆棧是一個后進先出(LIFO)的數據結構,元素稱為棧幀(frame)。當將要在線程上執行某方法時,則需要將代表  該方法的棧幀壓棧,當方法執行完畢后(正常退出或拋出未處理的異常)則將棧幀彈棧。棧幀可能分配在堆上(heap),而堆棧并不需要連續的存儲空間。

  3. Native Stack(本地堆棧)

   不是每種JVM都支持本地方法,對于支持本地方法的JVM它門會提供線程本地堆棧。若JVM實現了通過C鏈接模型(C-linkage Model)來實現JNI,那么本地堆棧實質就是C堆  棧(入參順序和返回值均與C程序一致)。本地方法一般都可以調用Java方法,此時會在Java的堆棧中壓入一個棧幀并按執行Java方法的流程處理。

      Stack Restrictions(堆棧約束):堆棧的容量有動態和固定兩種。當棧幀數量大于堆棧容量時就會拋出StackOverflowError;當堆中沒有足夠內存來分配新棧幀時則拋出OutOfMemoryError。

  4. Frame(堆棧的元素——棧幀)

      1. Local Varibles Array(局部變量表)

       局部變量表用于存放方法執行過程中this引用、方法入參和局部變量。對于靜態方法而言方法參數從局部變量表的第一位開始(下標為0),對于實例方法而言方法參數從局部變量表的第二位開始(下標為1,第一位是this引用)。局部變量表內可包含以下類型數據,boolean/byte/char/long/short/int/float/double/reference/returnAddress。

    局部變量表的每個元素占32bit,每32bit稱為1個slot。上述所支持的類型中除了long和double外均占1個slot,而它倆就占2個slot。

      2. Operand Stack(操作數棧)

    在執行方法內部的字節碼指令時需要使用操作數棧,大多數JVM的字節碼指令是用于操作操作數棧(壓棧、彈棧、賦值棧幀、棧幀互換位置或執行方法操作棧幀),實現數據在操作數棧和局部變量表之間頻繁移動。示例如下:

//java code
int i;

// bytecode
0: iconst_0 // 將0壓棧
1: istore_1 // 彈棧并將值賦值到局部變量表的第二個Slot槽中

      3. Dynamic Linking(動態鏈接)

    每個棧幀均包含一個指向運行時常量池(runtime constant pool)的引用。通過這個運行時常量池來實現動態鏈接。
C/C++的代碼會被編譯成一個一個獨立的對象文件,并通過靜態鏈接將對多個對象文件生成一個執行文件或dll類庫。在鏈接階段所有的符號引用會被直接引用取代,而直接引用則為相對于可執行文件的進程入口地址的相對地址。而Java的鏈接階段是在運行時動態發生的。
當將Java類編譯成字節碼時,所有對變量和方法的引用將被保存為常量池表中的一條條符號引用表項,這些符號引用為邏輯引用而不是指向物理內存地址的引用。JVM可以選擇不同的時刻將符號引用轉換為直接引用。一種是當class文件加載并驗證通過后,這種稱為靜態處理(eager or static resolution);另一種是在使用時才轉換為直接引用,這種稱為懶處理(lazy or late resolution)。對于字段通過綁定來處理,對于對象或類則通過將符號引用轉換直接引用來識別,動態鏈接后原有的符號引用將被直接引用替換,因此對于同一個符號引用,動態鏈接的操作僅發生一次。假如直接引用的類還未加載,則會加載該類。而直接引用所包含的地址相對于變量和方法在運行時的地址。

 

Shared Between Threads            

四、Heap(堆)                          

  堆用于在運行時分配對象和數組。由于棧幀的容量是固定的,因此無法將對象和數組等容量可變的數據存放到堆棧中,而是將對象和數組在堆中的地址存放在棧幀中從而操作對象和數組。由于對象和數組是存放在堆,因此需要通過垃圾回收器來回收它們所占的內存空間。垃圾回收機制將堆分成3部分:<br/>
  1. 新生代(再細分為初生空間和幸存空間)
  2. 老年代
  3. 永久代(譯者語:永久代不在堆上)

五、Memory Management(內存管理)             

  對象和數組不能被顯式地釋放,必須通過垃圾回收器來自動回收。一般的工作步驟如下:

  1. 新創建的對象和數組被存放在新生代;
  2. 次垃圾回收將會對新生代作操作,存活下來的將從初生空間移至幸存空間;
  3. 主垃圾回收(一般會導致應用的其他所有線程掛起),會將新生代的對象愛嗯挪動到老年代;
  4. 每次回收老年代對象時均會回收永久代的對象。當他們滿的時候就會觸發回收操作。

六、Non-Heap Memory(非堆內存)                                         

  非堆內存包含下列這些:
  1. 永久代
    1.1. 方法區
    1.2. 字符串區
  2. 代碼緩存
    用于存放被JIT編譯器編譯為本地代碼的方法。

七、Just In Time (JIT) Compilation(JIT編譯)           

  Java的字節碼是解析執行的,速度比CPU本地代碼差遠了。為了提高Java程序的執行效率,Oracle的Hotspot虛擬機將需要經常執行的字節碼編譯成本地代碼并存放在代碼緩存當中。Hotspot虛擬機會自動權衡解析執行字節碼和將字節碼編譯成本地代碼再執行之間的效率,然后選擇最優方案。

八、Method Area(方法區)                   

方法區存放每個類的信息,具體如下:
1. 類加載器引用
2. 運行時常量池
  2.1. 數字常量
  2.2. 字段引用
  2.3. 方法引用
  2.4. 屬性
3. 字段數據,每個字段包含以下信息
  3.1. 名稱
  3.2. 類型
  3.3. 修飾符
  3.4. 屬性
4. 方法數據,每個方法包含以下信息
  4.1. 名稱
  4.2. 返回值類型
  4.3. 入參的數據類型(保持入參的次序)
  4.4. 修飾符
  4.5. 屬性
5. 方法代碼,每個方法包含以下信息
  5.1. 字節碼
  5.2. 操作數棧容量
  5.3. 局部變量表容量
  5.4. 局部變量表
  5.5. 異常表,每個異常表項包含以下信息
    5.5.1. 起始地址
    5.5.2. 結束地址
    5.5.3. 異常處理代碼的地址
    5.5.4. 異常類在常量池的地址
所有線程均訪問同一個方法區,因此方法區的數據訪問和動態鏈接操作必須是線程安全才行。假如兩個線程試圖訪問某個未加載的類的字段或方法時,則會先掛起這兩個線程,等該類加載完后繼續執行。

 九、 Class File Structure(Class文件結構)                  

ClassFile {
    u4            magic;
    u2            minor_version;
    u2            major_version;
    u2            constant_pool_count;
    cp_info        contant_pool[constant_pool_count – 1];
    u2            access_flags;
    u2            this_class;
    u2            super_class;
    u2            interfaces_count;
    u2            interfaces[interfaces_count];
    u2            fields_count;
    field_info        fields[fields_count];
    u2            methods_count;
    method_info        methods[methods_count];
    u2            attributes_count;
    attribute_info    attributes[attributes_count];
}

magicminor_versionmajor_version:用于聲明JDK版本
constant_pool:類似符號表,但包含更多的信息
access_flags:存放該類的描述符列表
this_class:指向constant_pool中CONSTANT_Class_info類型常量的索引,該常量存放的是符號引用到當前類(如org/jamesdbloom/foo/bar)
super_class:指向constant_pool中CONSTANT_Class_info類型常量的索引,該常量存放的是符號引用到超類(如java/lang/Object)
interfaces:一組指向constant_pool中CONSTANT_Class_info類型常量的索引,該類常量存放的是符號引用到接口
fields:字段表,一個表項代表一個字段,表項的子項信息均有constant_pool提供。
methods:方法表,一個表項代表一個方法,表項的子項信息均有constant_pool提供。
attributes:屬性表,表項用于類提供額外的信息。java代碼中通過注解(約束為RetentionPolicy.CLASS或RetentionPolicy.RUNTIME的annotation)提供


通過`javap`命令我們可以查看解析后的字節碼

// java
package org.jvminternals;

public class SimpleClass {

    public void sayHello() {
        System.out.println("Hello");
    }

}

// shell or cmd
javap -v -p -s -sysinfo -constants classes/org/jvminternals/SimpleClass.class

// Bytecodes
public class org.jvminternals.SimpleClass
  SourceFile: "SimpleClass.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#17         //  java/lang/Object."<init>":()V
   #2 = Fieldref           #18.#19        //  java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #20            //  "Hello"
   #4 = Methodref          #21.#22        //  java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #23            //  org/jvminternals/SimpleClass
   #6 = Class              #24            //  java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lorg/jvminternals/SimpleClass;
  #14 = Utf8               sayHello
  #15 = Utf8               SourceFile
  #16 = Utf8               SimpleClass.java
  #17 = NameAndType        #7:#8          //  "<init>":()V
  #18 = Class              #25            //  java/lang/System
  #19 = NameAndType        #26:#27        //  out:Ljava/io/PrintStream;
  #20 = Utf8               Hello
  #21 = Class              #28            //  java/io/PrintStream
  #22 = NameAndType        #29:#30        //  println:(Ljava/lang/String;)V
  #23 = Utf8               org/jvminternals/SimpleClass
  #24 = Utf8               java/lang/Object
  #25 = Utf8               java/lang/System
  #26 = Utf8               out
  #27 = Utf8               Ljava/io/PrintStream;
  #28 = Utf8               java/io/PrintStream
  #29 = Utf8               println
  #30 = Utf8               (Ljava/lang/String;)V
{
  public org.jvminternals.SimpleClass();
    Signature: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
        0: aload_0
        1: invokespecial #1    // Method java/lang/Object."<init>":()V
        4: return
    LineNumberTable:
        line 3: 0
    LocalVariableTable:
        Start  Length  Slot  Name   Signature
          0      5      0    this   Lorg/jvminternals/SimpleClass;

  public void sayHello();
    Signature: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
        0: getstatic      #2    // Field java/lang/System.out:Ljava/io/PrintStream;
        3: ldc            #3    // String "Hello"
        5: invokevirtual  #4    // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        8: return
    LineNumberTable:
        line 6: 0
        line 7: 8
    LocalVariableTable:
        Start  Length  Slot  Name   Signature
          0      9      0    this   Lorg/jvminternals/SimpleClass;
}

字節碼顯示三個主要的區域:常量池、構造函數和sayHello方法。
常量池:提供類似于符號表的信息。
方法:每個方法均含四個區域
  1. 簽名和訪問標志;
  2. 方法體的字節碼;
  3. 行號表:為調試器提供Java代碼與字節碼的行號映射關系信息。
  4. 局部變量表:羅列當且當前方法的所有局部變量名。

(譯者語:由于后續內容為對字節碼指令的講解,沒什么必要翻譯了所以..............)

十、Classloader(類加載器)                  

  JVM啟動時通過bootstrap classloader加載初始類。在執行 public static void main(String[]) 方法前,這個類需要經過鏈接、初始化操作。然后在執行這個方法時就會觸發其他類和接口的加載、鏈接和初始化操作。
  **加載**,通過特定的名稱搜索類或接口文件,并將其內容加載為字節數組。(譯者語:這里加載的工作已經完成了,后面內容是加載+鏈接的內容)然后字節數組被解析為符合Java版本號的類對象(如Object.class),而該類或接口的直接父類和直接父接口也會被加載。
  **鏈接**,由驗證Class文件合法性、準備和可選的解析三個步驟組成。
  1. **驗證**,就是要根據Java和JVM規范對類或接口字節碼的格式和語義進行校驗。下面羅列部分校驗項:
    1.1. 符號表具有一致和合法的格式;
    1.2. 不可更改的方法和類沒有被重寫;
    1.3. 方法含有效的訪問控制關鍵字;
    1.4. 方法含有效的入參類型和數目;
    1.5. 字節碼沒有對操作數棧進行非法操作;
    1.6. 變量先初始化后使用;
    1.7. 變量值與變量類型匹配。
    在類加載階段進行驗證雖然會減慢加載速度,但可以減少運行時對同一類或接口進行重復驗證。
  2. **準備**,為靜態字段、靜態方法和如方法表等JVM使用的數據分配內存空間,并對靜態字段進行初始化。但這個時候該類或接口的構造函數、靜態構造函數和方法均沒有被執行。
  3. **解析(可選項)**,檢查符號引用并加載所引用的類或接口(加載直接父類和直接接口)。當沒有執行這一步驟時,則在運行時中調用這個類或接口時在執行。
  **初始化**,執行類的靜態構造函數 <clinit> 。

  JVM中有多個不同類型的類加載器。bootstrap classloader是頂層的類加載器,其他類加載器均繼承自它。
  1. **Bootstrap Classloader**,由于在JVM加載時初始化,因此Bootstrap Classloader是用C++編寫的。用于加載Java的核心API,如rt.jar等位于boot類路徑的高信任度的類,而這些類在鏈接時需要的校驗步驟比一般類要少不止一點點。
  2. **Extenson Classloader**,用于加載Java的擴展APIs。
  3. **System Classloader**,默認的應用類加載器,用于從classpath中加載應用的類。
  4. **User Defined Classloaders**,應用內部按一定的需求將對類分組加載或對類進行重新加載。

十一、Faster Class Loading(更快的類加載)            

  從HotSpot5.0開始引入了共享類數據(CDS)特性。在安裝JVM時則會將如rt.jar中的類加載到一個內存映射共享文檔中。然后各JVM實例啟動時直接讀取該內存中的類,提高JVM的啟動速度。

十二、 Where Is The Method Area(方法區在哪?)       

  《Java Virtual Machine Specification Java SE 7 Edition》明確聲明:“雖然方法區邏輯上位于堆中,簡單的實現方式應該是被垃圾回收。”矛盾的是Oracle JVM的jconsole告知我們方法區和代碼緩存是位于非堆內存空間中的。而OpenJDK則將代碼緩存設置為虛擬機外的ObjectHeap中。

十三、Classloader Reference(類加載器引用)             

  每個類都持有一個指向加載它的類加載器指針,同樣每個類加載都持有一組由它加載的類的指引。

十四、Run Time Constant Pool(運行時常量池)        

  每個類都對應一個運行時常量池(有Class文件中的常量池生成)。運行時常量池與符號表類似但包含更多的信息。字節碼指令中需要對數據進行操作,但由于數據太大無法直接存放在字節碼指令當中,于是通過將數據存放在常量池,而字節碼指令存放數據位于常量池的索引值來實現指令對數據的操作。動態鏈接也是通過運行時常量池來實現的。
  運行時常量池包含以下的類型的數據:
  1. 數字字面量;
  2. 字符串字面量;
  3. 類引用;
  4. 字段引用;
  5. 方法引用。
  舉個栗子:

// java
Object foo = new Object();

// bytecodes
0:     new #2             // Class java/lang/Object
1:    dup
2:    invokespecial #3    // Method java/ lang/Object "<init>"()V

  `new`操作碼后的#2操作數就是常量池第2項的索引,該項為類型引用,內含一個縮略UTF8類型的常量來存放類的全限定名(java/lang/Object)。在進行動態符號鏈接時則通過該名稱來查找類對象`java.lang.Object`。而`new`操作碼會創建一個類的實例、初始化實例的字段,并將該對象壓入操作數棧。`dup`復制棧頂元素并壓棧,然后`invokespecial`則彈出操作數棧頂的一個元素執行對象的構造函數。

  再舉個栗子:

// java
package org.jvminternals;

public class SimpleClass {

    public void sayHello() {
        System.out.println("Hello");
    }

}

// Bytecodes
Constant pool:
   #1 = Methodref          #6.#17         //  java/lang/Object."<init>":()V
   #2 = Fieldref           #18.#19        //  java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #20            //  "Hello"
   #4 = Methodref          #21.#22        //  java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #23            //  org/jvminternals/SimpleClass
   #6 = Class              #24            //  java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lorg/jvminternals/SimpleClass;
  #14 = Utf8               sayHello
  #15 = Utf8               SourceFile
  #16 = Utf8               SimpleClass.java
  #17 = NameAndType        #7:#8          //  "<init>":()V
  #18 = Class              #25            //  java/lang/System
  #19 = NameAndType        #26:#27        //  out:Ljava/io/PrintStream;
  #20 = Utf8               Hello
  #21 = Class              #28            //  java/io/PrintStream
  #22 = NameAndType        #29:#30        //  println:(Ljava/lang/String;)V
  #23 = Utf8               org/jvminternals/SimpleClass
  #24 = Utf8               java/lang/Object
  #25 = Utf8               java/lang/System
  #26 = Utf8               out
  #27 = Utf8               Ljava/io/PrintStream;
  #28 = Utf8               java/io/PrintStream
  #29 = Utf8               println
  #30 = Utf8               (Ljava/lang/String;)V

  Class的常量池包含以下類型:

Integer   一個4bytes的整型常量
Long       一個8bytes的長整型常量
Float       一個4bytes的浮點型常量
Double    一個4bytes的雙精度浮點型常量
String     字符串引用,指向一個縮略Utf8常量
Utf8       縮略Utf8編碼的字符串
Class      類型引用,指向一個縮略Utf8常量,存放類全限定名(用于動態鏈接)
NameAndType 存放兩個引用,一個指向用于存放字段或方法名的縮略Utf8常量,一個指向存放字段數據類型或方法返回值類型和入參的縮略Utf8常量
Fieldref,          存放兩個引用,一個指向表示所屬類或接口的Class常量,一個指向描述字段、方法名稱和描述符的NameAndType常量
Methodref,
InterfaceMethodref

十五、Exception Table(異常表)                

  異常表的每一項表示一項異常處理,表項字段如下:起始位置、結束位置、處理代碼的起始位置和指向常量池Class常量的位置索引。
  只要Java代碼中出現try-catch或try-finally的異常處理時,就會創建異常表,異常表的表項用于存放try語句塊在字節碼指令集中的范圍、捕捉的異常類和相應的字節碼處理指令的起始位置。(譯者注:try-finally所創建的表項的異常類引用為0)<br/>
當發生異常并沒有被捕獲處理,則會從線程棧的當前棧幀拋出并觸發彈棧操作,再棧頂棧幀接收,直到異常被某個棧幀捕獲處理或該線程棧為空并退出線程然后異常有系統異常處理機制捕獲。
  finally語句塊的代碼無論是否拋出異常均會執行。

十六、Symbol Table(符號表)                  

  HotSpot虛擬機在永久代中增加了符號表。該表為哈希表用于將直接引用與運行時常量池的符號引用作映射。<br/>
另外每個表項還有個引用計數器,用來記錄有多少個符號引用指向同一個直接引用。假如某個類被卸載了那么類中的所有符號引用將無效,則對應的符號表表項的引用計數器減1,當計數器為0時則將該表項移除。
十七、 Interned Strings (String Table)(字符串表)        
  Java語言說明中要求字符串字面量必須唯一,一樣的字符串字面量必須為同一個String實例。
HotSport虛擬機通過字符串表來實現。字符串表位于永久代中,表項為String實例地址與字符串字面量的映射關系信息。加載類時成功執行鏈接的準備階段時,Class文件常量池下的CONSTANT_String_info常量的信息均加載到字符串表中。而執行階段可以通過String#intern()方法將字符串字面量加入到字符串表中。如:

new String("jvm") == "jvm"; // false
(new String("jvm")).intern() == "jvm"; // true

String#intern(),會先去字符串表查找字面量相同的表項,有則返回對應的對象引用,沒有則先將新的字符串對象和字面量添加到表中,然后再返回對象引用。

 

總結                              

   本文對JVM內存模型做了概要的說明,讓初次接觸JVM的朋友對它有一個初步的big photo,在此感謝作者的分享。

   尊重原創,轉載請注明來自:http://www.cnblogs.com/fsjohnhuang/p/4260417.html  ^_^肥仔John

   原文地址:http://blog.jamesdbloom.com/JVMInternals.html

   


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()