深刻理解JVM內部結構

下圖顯示了符合Java SE 7 版本的Java虛擬機規範的一個典型JVM中的關鍵內部組件。 java

001

       圖中顯示的組件將會在下面兩部分中進行逐一的解釋。第一部分涉及JVM爲每個線程都會建立的組件;第二部分則是獨立於線程進行建立的組件。
1. Thread
      Thread是一個程序中的一個執行線程。JVM容許一個應用程序有多個執行線程併發運行。在Sun的Hotspot JVM中,Java線程與本地操做系統線程間存在一個直接的一一映射。JVM先爲Java線程準備好全部的狀態,如線程局部存儲、分配緩衝區、同步對象、棧和程序計數器,以後對應的本地線程被建立。Java線程終止之後其本地線程將會被回收利用,由操做系統負責調度全部的線程並將它們分派到可用的CPU。一旦本地線程已經初始化完成,則會調用Java線程的run()方法,當run()方法返回時,未捕獲的異常將被處理,以後本地線程判斷JVM是否須要終止運行(如此線程是最後一個非守護線程)。當線程終止之後,本地線程及Java線程佔用的全部資源都會被釋放。
1.1 JVM系統線程
       若是你曾使用jconsole或其它的一些調試工具,也許你會注意到JVM中有許多的線程運行在後臺。這些運行的後臺線程,除了main線程之外,都是做爲調用public static void main(String [])的一部分被建立,此外,還有一些是被main線程所建立。以Sun的Hotspot JVM爲例,主要的後臺系統線程有:
(1) VM Thread
      此線程用於等待請求JVM到達一個「安全點」的操做的出現。之因此全部這類操做須要在一個單獨的線程——VM Thread中進行,是由於它們都要求JVM處於「安全點」,當JVM位於「安全點」時,不能對heap執行修改操做。VM Thread執行的操做類型是會「stop-the-world」的垃圾回收、線程堆棧轉儲、線程掛起和偏向鎖撤銷。
      注:stop-the-world,也即當執行此操做時,Java虛擬機須要中止運行。
(2) Periodic task thread(週期任務線程)
      此線程負責用於調度週期性操做的執行的定時事件。
(3) GC threads(垃圾回收線程)
      這些線程用於支持JVM中進行的不一樣類型的垃圾回收活動。
(4) Compiler threads
      這些線程在運行時將字節碼編譯成本地代碼。
(5) Signal dispatcher thread(信號分發線程)
      此線程負責接收發送給JVM進程的信號,處理信號,根據系統設置調用合適的JVM方法。
1.2 Per Thread
      每個執行的線程都具備下列組件:
(1) 程序計數器(program Counter,PC)
      除非是本地代碼,不然PC指向當前指令(也即opcode)的地址。若是當前方法是本地的,則PC是未定義的。全部的CPU都有一個PC,通常狀況下,PC會在每個指令以後進行遞增,以保存下一個執行指令的地址。JVM使用PC來記錄當前指令執行的位置,PC實際上指向了方法區的一個內存地址。
(2) 棧(Stack)
      每一個線程都有本身的棧,棧爲線程中執行的每一個方法保存了一個對應的幀(Frame)。棧是後進先出(LIFO)的數據結構,所以當前執行的方法老是位於棧的頂端。當調用一個方法時,爲其建立一個新的幀並壓棧,當方法正常返回或者是因爲執行期間拋出了未捕獲的異常而退出時,其對應的幀將被刪除(也即出棧)。除了執行幀的壓棧與出棧,沒法對棧進行其它的直接操做,所以幀對象也許是在堆(Heap)中進行內存分配,它們的內存也就無需是連續的。
(3) 本地棧(Native Stack)
      並不是全部的JVM都支持本地方法,然而,那些支持的JVM通常會爲每一個線程建立本地方法棧。若是JVM是使用「C連接」模型實現Java本地調用(JNI)的,則本地棧將是一個C棧。在這種狀況下,本地棧中參數及返回值的順序將與典型的C程序是一致的。一個本地方法通常會調回JVM,並調用Java方法,此類從本地方法到Java方法調用發生於正常的Java棧中,線程會離開本地棧,並在Java棧中爲方法建立一個新的幀。
(4)棧的限制(Stack Restrictions)
      棧能夠是動態的,也能夠是固定大小的。若是一個線程要求超過容許的大小的棧,則JVM會拋出StackOverflowError。若是線程要求建立一個新的幀,可是系統沒有足夠內存進行分配,則JVM會拋出OutOfMemoryError。
(5) 幀(Frame)
      原文內容與棧部分大體一致,不冗述。
      每一個幀中包含有:1. 局部變量數組;2. 返回值;3. 操做對象棧;4. 到方法所屬類的運行時常量池的引用。
(6) 局部變量數組(Loca Variables Array)
      局部變量數組中包含了方法運行期間使用到的全部變量,包括到this變量的引用、全部的方法參數及其它方法中定義的局部變量。對於類方法(靜態方法),方法參數從「零」開始,而對於實例方法,「零」位置預留給this變量。
一個局部變量能夠是:
  • boolean
  • byte
  • char
  • long
  • short
  • int
  • float
  • double
  • reference
  • returnAddress

除了long和double,全部的類型都在局部變量數組中佔據一個位置,long及double則佔用兩個連續的位置(slot),因爲它們是雙倍的寬度,也即64位大小。 web

(7) 操做對象棧
      操做對象棧在字節碼指令執行期間使用,做用相似於CPU使用的通常用途的寄存器。大多數的JVM操縱操做對象棧的方式有:壓棧、出棧、複製、交換、經過執行某一操做產生或消耗成數值。所以,在字節碼中,在局部變量數組與操做對象棧之間移動數值的指令出現的次數不少。例如,一個簡單的變量就須要兩條與操做對象棧進行交互的字節碼。
  
  1. int i;

編譯之後的字節碼以下: 編程

  
  1. 0: iconst_0 // Push 0 to top of the operand stack
  2. 1: istore_1 // Pop value from top of operand stack and store as local variable 1
(8) 動態連接
      每一個幀包含了一個到運行時常量池的引用。此引用指向當前執行的方法對應的類的常量池,用於輔助支持動態連接功能。
當一個類被編譯之後,全部到變量及方法的引用都被存儲到類的常量池中做爲一個符號引用。一個符號引用是一個邏輯引用而不是直接指向物理內存地址的引用。JVM的實現能夠自由選擇什麼時候解析符號引用,可能的選擇有:類文件被驗證時亦或是被加載之後,這類方案被稱爲是急切的(eager)或靜態的(static)解析;還有一種被稱爲懶加載或延時加載方案,它是在符號引用第一次被使用時進行解析。然而,JVM必需要表現得像是當引用第一次被使用時才進行解析,而且此時須要拋出任何的解析錯誤。
       綁定是由符號引用標識的域、方法和類被直接引用替換的過程,此過程只發生一次。若是指向類的符號引用尚未被解析則此類會被加載。每個直接引用都被存儲爲一個相對偏移值,以與變量或方法的運行時位置關聯的存儲結構做爲基準。
1.3 Shared Between Threads
(1) 堆(Heap)
      堆用於運行時分配類實例及數組。數組及對象沒法存儲在棧中,由於幀在建立之後,其大小就沒法再改變。幀中只能存儲指向堆中對象或數組的引用。與簡單變量與在局部變量中的引用不一樣,數組對象老是存儲在堆中,所以當方法退出時,它們沒有被移除,對象只能被垃圾收集器移除。
爲了支持垃圾回收,堆被分爲三個部分:
  • Young Generation(年輕代),一般被分爲Eden和Survivor
  • Old Generation(老年代,也叫Tenured Generation)
  • Permanent Generation

(2) 內存管理 bootstrap

     對象及數組不會被顯示釋放,垃圾回收器會自動回收它們。
此過程以下:
1. 新的對象和數組被建立並放到年輕代中;
2. 一次小的垃圾收集會在年輕代上執行,若是對象存活下來,則它們將被從eden空間移動到survivor空間;
3. 主垃圾收集(Major)執行,它會引發應用線程中止運行,也即stop-the-world,同時會在不一樣的「代」中移動對象。通過這次垃圾回收後,若是對象仍然存活,則它們會被從年輕代移動到老年代。
4. 在每次老年代被回收時永久代也被回收,當任何一個空間滿時,它們都會被回收。
(3) 非堆內存
      從邏輯上考慮,被認爲是JVM結構的一部分的對象將不會在堆中進行建立。非堆內存包括有:1.包含方法區及interned字符串的永久代;2. 代碼緩存,用於編輯與存儲已經被JIT編譯器編譯爲本地代碼的方法。
(4) 即時編譯
      Java字節碼採用解釋執行方式,所以它無法像直接在JVM所在的主機上執行本地代碼那麼快。爲了提升性能,Oracle Hotspot VM尋找字節碼中被週期地執行的「熱區域」,並將它們編譯爲本地代碼。本地代碼被存儲到非堆內存的代碼緩存中,經過這樣的方式,Hotspot VM嘗試選擇最合適的方式來權衡編譯代碼花費的額外時間與執行解釋代碼花費的額外時間。
(5) 方法區(Method Area)
方法區中存儲了每一個類的元信息,如:
  • 類加載器引用
  • 運行時常量池
    • 數值常量
    • 字段引用
    • 方法引用
    • 屬性值
  • 字段數據
    • 每一個字段
      • 名稱
      • 類型
      • Modifiers
      • 屬性值
  • 方法數據
    • 每一個方法
      • 名稱
      • 返回類型
      • 參數類型
      • Modifiers
      • 屬性值
  • 方法代碼
    • 每一個方法
      • 字節碼
      • 操做對象棧大小
      • 局部變量大小
      • 局部變量表
      • 異常表
        • 每一個異常處理器
          • 起始點
          • 結束點
          • 處理器代碼的PC偏移值
          • 捕抓的異常類的常量池索引

全部的線程共享相同的方法區,所以方法區數據的存取及動態連接的過程必須是線程安全的。若是兩個線程都企圖存取同一個類中的字段或方法,可是,此類尚未被加載,則它必須只被加載一次,而且線程必須等到類被加載完成之後才能繼續執行。 數組

(6) 類文件結構
      一個編譯完成的類文件包含如下的結構:
   
  1. ClassFile {
  2. u4 magic;
  3. u2 minor_version;
  4. u2 major_version;
  5. u2 constant_pool_count;
  6. cp_info contant_pool[constant_pool_count 1];
  7. u2 access_flags;
  8. u2 this_class;
  9. u2 super_class;
  10. u2 interfaces_count;
  11. u2 interfaces[interfaces_count];
  12. u2 fields_count;
  13. field_info fields[fields_count];
  14. u2 methods_count;
  15. method_info methods[methods_count];
  16. u2 attributes_count;
  17. attribute_info attributes[attributes_count];
  18. }

若是你想看一個編譯完成的類文件的字節碼,可使用命令行工具javap。 緩存

若是你編譯下面這個簡單的類:
   
  1. package org.jvminternals;
  2.  
  3. public class SimpleClass {
  4.  
  5. public void sayHello() {
  6. System.out.println("Hello");
  7. }
  8.  
  9. }
以後你能夠經過運行以下的javap命令,得到字節碼信息,如:
javap -v -p -s -sysinfo -constants classes/org/jvminternals/SimpleClass.class
    
  1. public class org.jvminternals.SimpleClass
  2. SourceFile: "SimpleClass.java"
  3. minor version: 0
  4. major version: 51
  5. flags: ACC_PUBLIC, ACC_SUPER
  6. Constant pool:
  7. #1 = Methodref #6.#17 // java/lang/Object."<init>":()V
  8. #2 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream;
  9. #3 = String #20 // "Hello"
  10. #4 = Methodref #21.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V
  11. #5 = Class #23 // org/jvminternals/SimpleClass
  12. #6 = Class #24 // java/lang/Object
  13. #7 = Utf8 <init>
  14. #8 = Utf8 ()V
  15. #9 = Utf8 Code
  16. #10 = Utf8 LineNumberTable
  17. #11 = Utf8 LocalVariableTable
  18. #12 = Utf8 this
  19. #13 = Utf8 Lorg/jvminternals/SimpleClass;
  20. #14 = Utf8 sayHello
  21. #15 = Utf8 SourceFile
  22. #16 = Utf8 SimpleClass.java
  23. #17 = NameAndType #7:#8 // "<init>":()V
  24. #18 = Class #25 // java/lang/System
  25. #19 = NameAndType #26:#27 // out:Ljava/io/PrintStream;
  26. #20 = Utf8 Hello
  27. #21 = Class #28 // java/io/PrintStream
  28. #22 = NameAndType #29:#30 // println:(Ljava/lang/String;)V
  29. #23 = Utf8 org/jvminternals/SimpleClass
  30. #24 = Utf8 java/lang/Object
  31. #25 = Utf8 java/lang/System
  32. #26 = Utf8 out
  33. #27 = Utf8 Ljava/io/PrintStream;
  34. #28 = Utf8 java/io/PrintStream
  35. #29 = Utf8 println
  36. #30 = Utf8 (Ljava/lang/String;)V
  37. {
  38. public org.jvminternals.SimpleClass();
  39. Signature: ()V
  40. flags: ACC_PUBLIC
  41. Code:
  42. stack=1, locals=1, args_size=1
  43. 0: aload_0
  44. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
  45. 4: return
  46. LineNumberTable:
  47. line 3: 0
  48. LocalVariableTable:
  49. Start Length Slot Name Signature
  50. 0 5 0 this Lorg/jvminternals/SimpleClass;
  51.  
  52. public void sayHello();
  53. Signature: ()V
  54. flags: ACC_PUBLIC
  55. Code:
  56. stack=2, locals=1, args_size=1
  57. 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
  58. 3: ldc #3 // String "Hello"
  59. 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  60. 8: return
  61. LineNumberTable:
  62. line 6: 0
  63. line 7: 8
  64. LocalVariableTable:
  65. Start Length Slot Name Signature
  66. 0 9 0 this Lorg/jvminternals/SimpleClass;
  67. }
此類文件有三個主要部分:常量池、構造函數和sayHello方法。
  • 常量池, 它提供了與符號表相同的信息,這個將在後面進行詳述。
  • 方法, 每一個方法包含了四個部分:
    • 簽名及訪問標記
    • 字節碼
    • 行號表, 它主要用於爲調試器提供信息,行號表指示了每一個字節碼指令對應的行數,如Java代碼中的第六行對應了sayHello方法中的字節碼0,第七行對應了字節碼8.
  • 局部變量表, 它列出了幀中提供的全部局部變量,在本例子中,只有this這一個局部變量。

此類文件中使用到的字節碼操做數有: 安全

aload_0
此操做碼是格式爲aload_<n>的操做碼組的成員之一,它們都是用於加載一個對象引用到操做
對象棧中. <n>指示了對象在局部變量數組中的位置,可選的數字只能是0,1,2,3之一。還有其它相似的操做
碼用於加載數值,而非對象引用。如iload_<n>,lload_<n>, fload_<n>和dload_<n>,其中i是指int類型
l是long,f是float,d是double類型。對於索引值超過3的局部變量,可使用iload,lload,fload,dload進行
加載。這些操做碼都只有一個操做數,用於指定要加載的局部變量的索引。
ldc
這個指令(操做碼)用於將一個運行時常量池中的常量壓入操做對象棧中
getstatic
此指令用於將運行時常量池的靜態字段列表中的一個靜態值壓入到操做對象棧中。
invokespecial,
invokevirtual
這兩個指令屬於調用方法的指令組成員之一,調用方法的指令有:invokedynamic,invokestatic,
invokevirtual。在這個類文件中,invokevirtual用於基於對象的類進行方法調用,而invokespecial指令則用
於調用當前類的實例初始化方法,私有方法及父類方法。
return
此指令時返回型指令組成員之一,其它的還有ireturn,lreturn,freturn,dreturn,areturn及
return。每一個指令都是一類返回不一樣類型數值的返回聲明,i是int類型,l是long,f是float,d是
double,a則是對象引用。沒有前置類型字符的return則是返回void類型。
       在任何典型的字節碼中,局部變量、操做對象棧和運行時常量池間操做數交互的主要過程以下:
002
       構造函數中有兩條指令,首先是aload_0將this壓入到操做對象棧中,以後調用父類的構造函數,並使用this對象,所以this從操做對象棧中出棧。
sayHello()方法比構造函數複雜,由於它必需要使用運行時常量池將符號引用解析爲直接引用,可參考上一篇翻譯文章。第一個指令getstatic用於壓入一個到System類的靜態字段out的引用到操做對象棧中,下一個指令ldc把字符串「Hello」壓入到操做對象棧中,最後的指令invokevirtual調用System.out的println方法,將「Hello」出棧並做爲println方法的一個參數,爲當前線程建立一個新的幀。整個過程以下:

003

(7) 類加載器
      JVM經過使用bootstrap類加載器加載一個初始類進行啓動。初始類在調用public static void main(String [])方法以前被連接和初始化。main方法的執行最終驅使了其它須要的附加類和接口的加載、連接和初始化。
       加載是根據其特定的名字找到表示類或接口類型的類文件,並將其讀取到byte數組中的過程。下一步將會解析byte數組,以確保它們表示的是一個類文件並具備正確的主及副版本號。此類的任何直接父類也會被加載。一旦加載過程完成,一個類或接口對象將被依據其二進制表示建立。
連接是進行類或接口的驗證,準備類型及其直接父類和實現的接口的過程。連接包含了三個步驟:驗證、準備和可選的解析。
1. 驗證,是肯定類或接口的表示是結構正確的,且遵照Java編程語言及JVM的語義要求,好比說它應該具備合適的符號表。
2. 準備,它涉及了靜態存儲空間的分配和JVM使用到的任何數據結構,如方法表的分配。靜態字段被建立並初始化爲它們的默認值,然而,此時沒有初始化塊或代碼被執行,所以這些是在初始化階段進行。
3. 解析,它是一個可選的階段,包含有經過加載引用類或接口來檢查符號引用,確認引用是正確的。若是沒有在這裏進行符號引用的解析,則能夠將其推遲到字節碼指令使用此符號引用以前進行。
      類或接口的初始化包含執行類或接口的初始化方法<clinit>.

004

       在JVM中有多個類加載器,它們扮演了不一樣的角色。每一個類的加載被委託給它的父類加載器,除了bootstrap類加載器,所以它是最上層的類加載器。     Bootstrap類加載器的責任是加載基本的Java API,如rt.jar, 它只加載在具備更高的信任級別的根類路徑中發現的類,也正因如此,它省去了許多對於普通的類須要進行的驗證工做。JVM還包含了一個擴展類加載器(extension class loader),它用於加載標準Java 擴展API中的類,如安全擴展功能。而系統類加載器(system class loader)是默認的應用類加載器,它從classpath中加載應用類。
       用戶自定義類加載器也能夠做爲應用類加載器。使用用戶自定義類加載器的緣由有許多,如運行時重加載類,分開不一樣組的加載類,這個功能通常web服務器都須要用到,如Tomcat。

005

(8) 加速類的加載
       從5.0版本開始,Hotspot JVM中引入了一個稱爲類數據共享(Class Data Sharing,CDS)的特點功能。在JVM的安裝過程當中,安裝程序加載一系列的關鍵JVM類到一個內存映射的共享檔案中(memory-mapped shared archive),如rt.jar。CDS能夠減小用於加載這些類的時間,從而提升JVM的啓動速度,並容許不一樣的JVM實例共享這些類,減小內存的使用。
(9) 方法區位於何處?
       Java虛擬機規範中清楚的說明了:「儘管方法區邏輯上是屬於堆的一部分,簡單實現也許會選擇既不對其進行垃圾回收,也不進行壓縮。」與jconsole中顯示Oracle JVM中方法區屬於非堆內存相反,OpenJDK的代碼顯示CodeCache是VM的ObjectHeap的一個獨立字段。
(10) 類加載器的引用
       全部被加載的類都包含了一個到加載它們的類加載器的引用。反過來,類加載器一樣包含了一個到它加載的全部類的引用。
(11) 運行時常量池
      JVM爲每種類型都維護了一個對應的常量池,常量池是一個運行時數據結構,有點相似於符號表,不過他包含了更多的數據。Java中的字節碼須要數據,一般這些數據太大而沒法直接存儲在字節碼中,所以將它們存儲在常量池中,而字節碼包含一個到常量池的引用。
有幾種類型的數據會被存儲在常量池中,如:
  • 數值型字面值
  • 字符串字面值
  • 類引用
  • 字段引用
  • 方法引用

以下面的代碼: 服務器

         
  1. Object foo = new Object();

其對應的字節碼以下: 數據結構

         
  1. 0: new #2 // Class java/lang/Object
  2. 1: dup
  3. 2: invokespecial #3 // Method java/ lang/Object "<init>"( ) V
      new操做碼後面跟着#2操做數,此操做數是一個常量池的索引,指向常量池中的第二個實體,第二個實體是一個類引用,此實體轉而引用常量池中另外一個包含類名字的實體,類的名字用一個值爲java/lang/Object的UTF8字符串常量表示。以後,此符號連接能夠用於查詢java.lang.Object類。new操做碼建立一個類實例並初始化它的變量,一個指向新建立的類實例的引用被添加到操做對象棧中。dup操做碼則在操做對象棧頂建立此引用的兩份拷貝。最後,經過invokespecial指令調用實例的初始化方法。這個指令的操做數一樣包含了一個到常量池的引用,此初始化方法消耗操做數池頂端的一個引用做爲方法的參數,最後,產生了一個指向已經被建立並初始化的新對象的引用。
(12) 異常表
異常表存儲了每一個異常處理器信息,如:
  • 起始點
  • 結束點
  • 處理器代碼的PC偏移值
  • 被捕抓的異常類的常量池索引

若是一個方法定義了一個try-catch或try-finally異常處理器,則一個異常表將會被建立。異常表包含了每一個異常處理器或者是finally塊的信息,如異常處理應用的範圍,那種類型的異常會被處理及異常處理代碼所在的位置。 併發

      當一個異常被拋出時,JVM會在當前方法中查找匹配的異常處理器。
相關文章
相關標籤/搜索