JVM效能調優(1) —— JVM記憶體模型和類載入執行機制
一、JVM記憶體模型
執行一個 Java 應用程式,必須要先安裝 JDK 或者 JRE 包。因為 Java 應用在編譯後會變成位元組碼,通過位元組碼執行在 JVM 中,而 JVM 是 JRE 的核心組成部分。JVM 不僅承擔了 Java 位元組碼的分析和執行,同時也內建了自動記憶體分配管理機制。這個機制可以大大降低手動分配回收機制可能帶來的記憶體洩露和記憶體溢位風險,使 Java 開發人員不需要關注每個物件的記憶體分配以及回收,從而更專注於業務本身。
在 Java 中,JVM 記憶體模型主要分為堆、方法區、程式計數器、虛擬機器棧和本地方法棧。其中,堆和方法區被所有執行緒共享,虛擬機器棧、本地方法棧、程式計數器是執行緒私有的。
1、堆
堆是 JVM 記憶體中最大的一塊記憶體空間,該記憶體被所有執行緒共享,幾乎所有物件和陣列都被分配到了堆記憶體中。堆被劃分為新生代和老年代,新生代又被進一步劃分為 Eden 和 Survivor 區,最後 Survivor 由 From Survivor 和 To Survivor 組成。
但需要注意的是,這些區域的劃分因不同的垃圾收集器而不同。大部分垃圾收集器都是基於分代收集理論設計的,就會採用這種分代模型。而一些新的垃圾收集器不採用分代設計,比如 G1 收集器就是把堆記憶體拆分為多個大小相等的 Region。
2、方法區
在 jdk8 之前,HotSopt 虛擬機器的方法區又被稱為永久代,由於永久代的設計容易導致記憶體溢位等問題,jdk8 之後就沒有永久代了,取而代之的是元空間(MetaSpace)。元空間並沒有處於堆記憶體上,而是直接佔用的本地記憶體,因此元空間的最大大小受本地記憶體限制。
方法區與堆空間類似,是所有執行緒共享的。方法區主要是用來存放已被虛擬機器載入的型別資訊、常量、靜態變數等資料。方法區是一個邏輯分割槽,包含元空間、執行時常量池、字串常量池,元空間物理上使用的本地記憶體,執行時常量池和字串常量池是在堆中開闢的一塊特殊記憶體區域。這樣做的好處之一是可以避免執行時動態生成的常量的複製遷移,可以直接使用堆中的引用。要注意的是,字串常量池在 jvm 中只有一個,而執行時常量池是和型別資料繫結的,每個 Class 一個。
1)型別資訊(類或介面):
- 這個型別的全限定名
- 這個型別的直接超類的全限定名(只有 java.lang.Object 沒有超類)
- 這個型別的訪問修飾符(public、abstract、final)
- 這個型別是介面型別還是類型別
- 任何直接超介面的的全限定名的有序列表
2)執行時常量池:
- Class 檔案被裝載進虛擬機器後,Class 常量池表中的字面量和符號引用都會存放到執行時常量池中,平時我們說的常量池一般指執行時常量池。
- 執行時常量池相比Class常量池具備動態性,執行時可以將新的常量放入池中,比如呼叫 String.intern() 方法使字串駐留。
3)欄位資訊:
- 欄位名
- 欄位的型別(包括 void)
- 欄位的修飾符(public、private、protected、static、final、volatile、transient)
4)方法資訊:
- 方法名
- 方法的返回型別
- 方法引數的數量和型別
- 方法的修飾符(public、private、protected、static、final、synchronized、native、abstract)
- 方法的位元組碼
- 運算元棧和該方法的棧幀中的區域性變數的大小
- 異常表
5)指向類載入器的引用:
- jvm 使用類載入器來載入一個類,這個類載入器是和這個型別繫結的,因此會在型別資訊中儲存這個類載入器的引用
6)指向 Class 類的引用:
- 每一個被載入的型別,jvm 都會在堆中建立一個 java.lang.Class 的例項,型別資訊中會儲存 Class 例項的引用
- 在程式碼中,可以使用 Class 例項訪問方法區儲存的資訊,如類載入器、類名、介面等
3、虛擬機器棧
每當啟動一個新的執行緒,虛擬機器都會在虛擬機器棧裡為它分配一個執行緒棧,執行緒棧與執行緒同生共死。執行緒棧以 棧幀 為單位儲存執行緒的執行狀態,虛擬機器只會對執行緒棧執行兩種操作:以棧幀為單位的壓棧或出棧。每個方法在執行的同時都會建立一個棧幀,每個方法從呼叫開始到結束,就對應著一個棧幀線上程棧中壓棧和出棧的過程。方法可以通過兩種方式結束,一種通過 return 正常返回,一種通過丟擲異常而終止。方法返回後,虛擬機器都會彈出當前棧幀然後釋放掉。
當虛擬機器呼叫一個Java方法時.它從對應類的型別資訊中得到此方法的區域性變數區和運算元棧的大小,並據此分配棧幀記憶體,然後壓入Java棧中。
棧幀由三部分組成:區域性變數區、運算元棧、幀資料區。
1)區域性變數區:
- 區域性變數區是一個數組結構,主要存放對應方法的引數和區域性變數。
- 如果是例項方法,區域性變量表第一個引數是一個 reference 引用型別,存放的是當前物件本身 this。
2)運算元棧:
- 運算元棧也是一個數組結構,但並不是通過索引來訪問的,而是棧的壓棧和出棧操作。
- 運算元棧是虛擬機器的工作區,大多數指令都要從這裡彈出資料、執行運算、然後把結果壓回運算元棧。
3)幀資料區:主要儲存常量池入口、異常表、正常方法返回的資訊
- 常量池入口引用:某些指令要從常量池取資料,獲取類、欄位資訊等
- 異常表引用:當方法丟擲異常時,虛擬機器根據異常表來決定如何處理。如果在異常表找到了匹配的 catch 子句,就會把控制權轉交給 catch 子句的程式碼。沒有則立即異常中止,然後恢復發起呼叫的方法的棧幀,然後在發起呼叫的方法的上下文中重新丟擲同樣的異常。
- 方法返回資訊:方法正常返回時,虛擬機器通過這些資訊恢復發起呼叫的方法的棧幀,設定PC暫存器指向發起呼叫的方法。方法如果有返回值,還會把返回結果壓入到發起呼叫的方法的運算元棧。
4、本地方法棧
本地方法棧與虛擬機器棧所發揮的作用是相似的,當執行緒呼叫Java方法時,會建立一個棧幀並壓入虛擬機器棧;而呼叫本地方法時,虛擬機器會保持棧不變,不會壓入新的棧幀,虛擬機器只是簡單的動態連結並直接呼叫指定的本地方法,使用的是某種本地方法棧。比如某個虛擬機器實現的本地方法介面是使用C連線模型,那麼它的本地方法棧就是C棧。
本地方法可以通過本地方法介面來訪問虛擬機器的執行時資料區,它可以做任何他想做的事情,本地方法不受虛擬機器控制。
5、程式計數器
每一個執行的執行緒都會有它的程式計數器(PC暫存器),與執行緒的生命週期一樣。執行某個方法時,PC暫存器的內容總是下一條將被執行的地址,這個地址可以是一個本地指標,也可以是在方法位元組碼中相對於該方法起始指令的偏移量。如果該執行緒正在執行一個本地方法,那麼此時PC暫存器的值是 undefined。
程式計數器是程式控制流的指示器,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成。多執行緒環境下,為了執行緒切換後能恢復到正確的執行位置,每條執行緒都需要有一個獨立的程式計數器,各條執行緒之間計數器互不影響,獨立儲存。
二、類載入機制
寫好的原始碼,需要編譯後加載到虛擬機器才能執行。java 原始檔編譯成 class 檔案後,jvm 通過類載入器把 class 檔案載入到虛擬機器,然後經過類連線(類連線又包括驗證、準備、解析三個階段),最後經過初始化,位元組碼就可以被解釋執行了。對於一些熱點程式碼,虛擬機器還存在一道即時編譯,會把位元組碼編譯成本地平臺相關的機器碼,以提高熱點程式碼的執行效率。
裝載、驗證、準備、初始化這幾個階段的順序是確定的,型別的載入過程必須按照這種順序開始,而解析階段可以在初始化階段之後再開始,一般是在第一次使用到這個物件時才會開始解析。這些階段通常都是互相交叉地混合進行的,會在一個階段執行的過程中呼叫、啟用另一個階段,比如發現引用了另一個類,那麼就會先觸發另一個類的載入過程。
接下來通過如下類和程式碼來詳細分析下類載入的過程:
1 package com.lyyzoo.jvm.test01; 2 3 public class Person<T> { 4 5 public static final String SEX_MAN = "Male"; 6 public static final String SEX_WOMAN = "Female"; 7 8 static { 9 System.out.println("Person static init"); 10 System.out.println("SEX_MAN: " + SEX_MAN); 11 } 12 13 public void sayHello(T str) { 14 System.out.println("Person say hello: " + str); 15 } 16 } 17 18 19 ///////////////////////////////////////////////////////////////////// 20 21 22 package com.lyyzoo.jvm.test01; 23 24 import java.io.Serializable; 25 26 public class User extends Person<String> implements Serializable { 27 private static final long serialVersionUID = -4482416396338787067L; 28 29 // 靜態常量 30 public static final String FIELD_NAME = "username"; 31 public static final int AGE_MAX = 100; 32 33 // 靜態變數 34 private static String staticName = "Rambo"; 35 private static int staticAge = 20; 36 37 // 類屬性 38 private String name = "蘭博"; 39 private int age = 25; 40 41 // 靜態程式碼塊 42 static { 43 System.out.println("user static init"); 44 System.out.println("staticName=" + staticName); 45 System.out.println("staticAge=" + staticAge); 46 } 47 48 public User() { 49 } 50 51 public User(String name, int age) { 52 this.name = name; 53 this.age = age; 54 } 55 56 // 例項方法 57 public void printInfo() { 58 System.out.println("name:" + name + ", age:" + age); 59 } 60 61 // 靜態方法 62 public static void staticPrintInfo() { 63 System.out.println("FIELD_NAME:" + FIELD_NAME + ", AGE_MAX:" + AGE_MAX); 64 } 65 66 // 泛型方法過載 67 @Override 68 public void sayHello(String str) { 69 super.sayHello(str); 70 System.out.println("User say hello: " + str); 71 } 72 73 // 方法將丟擲異常 74 public int willThrowException() { 75 int i = 0; 76 try { 77 int r = 10 / i; 78 return r; 79 } catch (Exception e) { 80 System.out.println("catch exception"); 81 return i; 82 } finally { 83 System.out.println("finally handle"); 84 } 85 } 86 } 87 88 89 ///////////////////////////////////////////////////////////////////// 90 91 92 package com.lyyzoo.jvm.test01; 93 94 public class Main { 95 96 public static void main(String[] args) { 97 System.out.println("FIELD_NAME: " + User.FIELD_NAME); 98 99 User.staticPrintInfo(); 100 101 User user = new User(); 102 user.printInfo(); 103 } 104 }View Code
三、類編譯和Class 檔案結構
*.java 檔案被編譯成 *.class 檔案的過程,這個編譯一般稱為前端編譯,主要使用 javac 來完成前端編譯。Java class檔案是8位位元組的二進位制流,資料項按順序儲存在class檔案中,相鄰的項之間沒有任何間隔,這樣可以使class檔案緊湊。class 檔案主要包含 版本資訊、常量池、型別索引、欄位表、方法表、屬性表等資訊。
將 User 類編譯成 class 檔案後,再通過 javap 反編譯 class 檔案,可以看到一個 class 檔案大體包含的結構:
1 說明:用“【】”標識的是手動新增的註釋 2 3 Mechrevo@hello-world MINGW64 /e/repo-study/test-concurrent/target/classes/com/lyyzoo/jvm/test01 4 【javap -v 命令反編譯 Class】 5 $ javap -v User.class 6 Classfile /E:/repo-study/test-concurrent/target/classes/com/lyyzoo/jvm/test01/User.class 7 Last modified 2020-9-3; size 2389 bytes 8 【魔數】 9 MD5 checksum ec5a961c2a46926522bafddcb3204fb9 10 Compiled from "User.java" 11 public class com.lyyzoo.jvm.test01.User extends com.lyyzoo.jvm.test01.Person<java.lang.String> implements java.io.Serializable 12 【版本號】 13 minor version: 0 14 major version: 52 15 flags: ACC_PUBLIC, ACC_SUPER 16 【常量池】 17 Constant pool: 18 #1 = Methodref #29.#76 // com/lyyzoo/jvm/test01/Person."<init>":()V 19 #2 = String #77 // 蘭博 20 #3 = Fieldref #14.#78 // com/lyyzoo/jvm/test01/User.name:Ljava/lang/String; 21 #4 = Fieldref #14.#79 // com/lyyzoo/jvm/test01/User.age:I 22 #5 = Fieldref #80.#81 // java/lang/System.out:Ljava/io/PrintStream; 23 #6 = Class #82 // java/lang/StringBuilder 24 #7 = Methodref #6.#76 // java/lang/StringBuilder."<init>":()V 25 #8 = String #83 // name: 26 #9 = Methodref #6.#84 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 27 #10 = String #85 // , age: 28 #11 = Methodref #6.#86 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 29 #12 = Methodref #6.#87 // java/lang/StringBuilder.toString:()Ljava/lang/String; 30 #13 = Methodref #88.#89 // java/io/PrintStream.println:(Ljava/lang/String;)V 31 #14 = Class #90 // com/lyyzoo/jvm/test01/User 32 #15 = String #91 // FIELD_NAME:username, AGE_MAX:100 33 #16 = Methodref #29.#92 // com/lyyzoo/jvm/test01/Person.sayHello:(Ljava/lang/Object;)V 34 #17 = String #93 // User say hello: 35 #18 = String #94 // finally handle 36 #19 = Class #95 // java/lang/Exception 37 #20 = String #96 // catch exception 38 #21 = Class #97 // java/lang/String 39 #22 = Methodref #14.#98 // com/lyyzoo/jvm/test01/User.sayHello:(Ljava/lang/String;)V 40 #23 = String #99 // Rambo 41 #24 = Fieldref #14.#100 // com/lyyzoo/jvm/test01/User.staticName:Ljava/lang/String; 42 #25 = Fieldref #14.#101 // com/lyyzoo/jvm/test01/User.staticAge:I 43 #26 = String #102 // user static init 44 #27 = String #103 // staticName= 45 #28 = String #104 // staticAge= 46 #29 = Class #105 // com/lyyzoo/jvm/test01/Person 47 #30 = Class #106 // java/io/Serializable 48 #31 = Utf8 serialVersionUID 49 #32 = Utf8 J 50 #33 = Utf8 ConstantValue 51 #34 = Long -4482416396338787067l 52 #36 = Utf8 FIELD_NAME 53 #37 = Utf8 Ljava/lang/String; 54 #38 = String #107 // username 55 #39 = Utf8 AGE_MAX 56 #40 = Utf8 I 57 #41 = Integer 100 58 #42 = Utf8 staticName 59 #43 = Utf8 staticAge 60 #44 = Utf8 name 61 #45 = Utf8 age 62 #46 = Utf8 <init> 63 #47 = Utf8 ()V 64 #48 = Utf8 Code 65 #49 = Utf8 LineNumberTable 66 #50 = Utf8 LocalVariableTable 67 #51 = Utf8 this 68 #52 = Utf8 Lcom/lyyzoo/jvm/test01/User; 69 #53 = Utf8 (Ljava/lang/String;I)V 70 #54 = Utf8 MethodParameters 71 #55 = Utf8 printInfo 72 #56 = Utf8 staticPrintInfo 73 #57 = Utf8 sayHello 74 #58 = Utf8 (Ljava/lang/String;)V 75 #59 = Utf8 str 76 #60 = Utf8 willThrowException 77 #61 = Utf8 ()I 78 #62 = Utf8 r 79 #63 = Utf8 e 80 #64 = Utf8 Ljava/lang/Exception; 81 #65 = Utf8 i 82 #66 = Utf8 StackMapTable 83 #67 = Class #90 // com/lyyzoo/jvm/test01/User 84 #68 = Class #95 // java/lang/Exception 85 #69 = Class #108 // java/lang/Throwable 86 #70 = Utf8 (Ljava/lang/Object;)V 87 #71 = Utf8 <clinit> 88 #72 = Utf8 Signature 89 #73 = Utf8 Lcom/lyyzoo/jvm/test01/Person<Ljava/lang/String;>;Ljava/io/Serializable; 90 #74 = Utf8 SourceFile 91 #75 = Utf8 User.java 92 #76 = NameAndType #46:#47 // "<init>":()V 93 #77 = Utf8 蘭博 94 #78 = NameAndType #44:#37 // name:Ljava/lang/String; 95 #79 = NameAndType #45:#40 // age:I 96 #80 = Class #109 // java/lang/System 97 #81 = NameAndType #110:#111 // out:Ljava/io/PrintStream; 98 #82 = Utf8 java/lang/StringBuilder 99 #83 = Utf8 name: 100 #84 = NameAndType #112:#113 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 101 #85 = Utf8 , age: 102 #86 = NameAndType #112:#114 // append:(I)Ljava/lang/StringBuilder; 103 #87 = NameAndType #115:#116 // toString:()Ljava/lang/String; 104 #88 = Class #117 // java/io/PrintStream 105 #89 = NameAndType #118:#58 // println:(Ljava/lang/String;)V 106 #90 = Utf8 com/lyyzoo/jvm/test01/User 107 #91 = Utf8 FIELD_NAME:username, AGE_MAX:100 108 #92 = NameAndType #57:#70 // sayHello:(Ljava/lang/Object;)V 109 #93 = Utf8 User say hello: 110 #94 = Utf8 finally handle 111 #95 = Utf8 java/lang/Exception 112 #96 = Utf8 catch exception 113 #97 = Utf8 java/lang/String 114 #98 = NameAndType #57:#58 // sayHello:(Ljava/lang/String;)V 115 #99 = Utf8 Rambo 116 #100 = NameAndType #42:#37 // staticName:Ljava/lang/String; 117 #101 = NameAndType #43:#40 // staticAge:I 118 #102 = Utf8 user static init 119 #103 = Utf8 staticName= 120 #104 = Utf8 staticAge= 121 #105 = Utf8 com/lyyzoo/jvm/test01/Person 122 #106 = Utf8 java/io/Serializable 123 #107 = Utf8 username 124 #108 = Utf8 java/lang/Throwable 125 #109 = Utf8 java/lang/System 126 #110 = Utf8 out 127 #111 = Utf8 Ljava/io/PrintStream; 128 #112 = Utf8 append 129 #113 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder; 130 #114 = Utf8 (I)Ljava/lang/StringBuilder; 131 #115 = Utf8 toString 132 #116 = Utf8 ()Ljava/lang/String; 133 #117 = Utf8 java/io/PrintStream 134 #118 = Utf8 println 135 { 136 【欄位表集合】 137 public static final java.lang.String FIELD_NAME; 138 descriptor: Ljava/lang/String; 139 flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL 140 ConstantValue: String username 141 142 public static final int AGE_MAX; 143 descriptor: I 144 flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL 145 ConstantValue: int 100 146 147 【方法表】 148 public com.lyyzoo.jvm.test01.User(); 149 【描述符索引】 150 descriptor: ()V 151 【訪問標誌】 152 flags: ACC_PUBLIC 153 【方法體程式碼指令】 154 Code: 155 【方法棧大小】 156 stack=2, locals=1, args_size=1 157 0: aload_0 158 1: invokespecial #1 // Method com/lyyzoo/jvm/test01/Person."<init>":()V 159 4: aload_0 160 5: ldc #2 // String 蘭博 161 7: putfield #3 // Field name:Ljava/lang/String; 162 10: aload_0 163 11: bipush 25 164 13: putfield #4 // Field age:I 165 16: return 166 【屬性表,方法區域性變數】 167 LineNumberTable: 168 line 27: 0 169 line 17: 4 170 line 18: 10 171 line 28: 16 172 【本地變量表,方法入參】 173 LocalVariableTable: 174 Start Length Slot Name Signature 175 0 17 0 this Lcom/lyyzoo/jvm/test01/User; 176 177 public com.lyyzoo.jvm.test01.User(java.lang.String, int); 178 descriptor: (Ljava/lang/String;I)V 179 flags: ACC_PUBLIC 180 Code: 181 stack=2, locals=3, args_size=3 182 0: aload_0 183 1: invokespecial #1 // Method com/lyyzoo/jvm/test01/Person."<init>":()V 184 4: aload_0 185 5: ldc #2 // String 蘭博 186 7: putfield #3 // Field name:Ljava/lang/String; 187 10: aload_0 188 11: bipush 25 189 13: putfield #4 // Field age:I 190 16: aload_0 191 17: aload_1 192 18: putfield #3 // Field name:Ljava/lang/String; 193 21: aload_0 194 22: iload_2 195 23: putfield #4 // Field age:I 196 26: return 197 LineNumberTable: 198 line 30: 0 199 line 17: 4 200 line 18: 10 201 line 31: 16 202 line 32: 21 203 line 33: 26 204 LocalVariableTable: 205 Start Length Slot Name Signature 206 【可以看出,物件例項方法的第一個引數始終都是 this,這也是為什麼我們可以在方法內呼叫 this 的原因】 207 0 27 0 this Lcom/lyyzoo/jvm/test01/User; 208 0 27 1 name Ljava/lang/String; 209 0 27 2 age I 210 MethodParameters: 211 Name Flags 212 name 213 age 214 215 public void printInfo(); 216 descriptor: ()V 217 flags: ACC_PUBLIC 218 Code: 219 stack=3, locals=1, args_size=1 220 0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 221 3: new #6 // class java/lang/StringBuilder 222 6: dup 223 7: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V 224 10: ldc #8 // String name: 225 12: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 226 15: aload_0 227 16: getfield #3 // Field name:Ljava/lang/String; 228 19: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 229 22: ldc #10 // String , age: 230 24: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 231 27: aload_0 232 28: getfield #4 // Field age:I 233 31: invokevirtual #11 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 234 34: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 235 37: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 236 40: return 237 LineNumberTable: 238 line 37: 0 239 line 38: 40 240 LocalVariableTable: 241 Start Length Slot Name Signature 242 0 41 0 this Lcom/lyyzoo/jvm/test01/User; 243 244 public static void staticPrintInfo(); 245 descriptor: ()V 246 【訪問標誌】 247 flags: ACC_PUBLIC, ACC_STATIC 248 Code: 249 stack=2, locals=0, args_size=0 250 0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 251 3: ldc #15 // String FIELD_NAME:username, AGE_MAX:100 252 5: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 253 8: return 254 LineNumberTable: 255 line 42: 0 256 line 43: 8 257 【注意,靜態方法第一個引數就不再是 this 了】 258 259 260 public void sayHello(java.lang.String); 261 descriptor: (Ljava/lang/String;)V 262 flags: ACC_PUBLIC 263 Code: 264 stack=3, locals=2, args_size=2 265 0: aload_0 266 1: aload_1 267 2: invokespecial #16 // Method com/lyyzoo/jvm/test01/Person.sayHello:(Ljava/lang/Object;)V 268 5: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 269 8: new #6 // class java/lang/StringBuilder 270 11: dup 271 12: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V 272 15: ldc #17 // String User say hello: 273 17: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 274 20: aload_1 275 21: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 276 24: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 277 27: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 278 30: return 279 LineNumberTable: 280 line 48: 0 281 line 49: 5 282 line 50: 30 283 LocalVariableTable: 284 Start Length Slot Name Signature 285 【第一個引數為 this】 286 0 31 0 this Lcom/lyyzoo/jvm/test01/User; 287 0 31 1 str Ljava/lang/String; 288 MethodParameters: 289 Name Flags 290 str 291 292 public int willThrowException(); 293 descriptor: ()I 294 flags: ACC_PUBLIC 295 Code: 296 stack=2, locals=5, args_size=1 297 0: iconst_0 298 1: istore_1 299 2: bipush 10 300 4: iload_1 301 5: idiv 302 6: istore_2 303 7: iload_2 304 8: istore_3 305 9: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 306 12: ldc #18 // String finally handle 307 14: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 308 17: iload_3 309 18: ireturn 310 19: astore_2 311 20: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 312 23: ldc #20 // String catch exception 313 25: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 314 28: iload_1 315 29: istore_3 316 30: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 317 33: ldc #18 // String finally handle 318 35: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 319 38: iload_3 320 39: ireturn 321 40: astore 4 322 42: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 323 45: ldc #18 // String finally handle 324 47: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 325 50: aload 4 326 52: athrow 327 【方法異常表】 328 Exception table: 329 from to target type 330 2 9 19 Class java/lang/Exception 331 2 9 40 any 332 19 30 40 any 333 40 42 40 any 334 LineNumberTable: 335 line 54: 0 336 line 56: 2 337 line 57: 7 338 line 62: 9 339 line 57: 17 340 line 58: 19 341 line 59: 20 342 line 60: 28 343 line 62: 30 344 line 60: 38 345 line 62: 40 346 line 63: 50 347 LocalVariableTable: 348 Start Length Slot Name Signature 349 7 12 2 r I 350 20 20 2 e Ljava/lang/Exception; 351 0 53 0 this Lcom/lyyzoo/jvm/test01/User; 352 2 51 1 i I 353 StackMapTable: number_of_entries = 2 354 frame_type = 255 /* full_frame */ 355 offset_delta = 19 356 locals = [ class com/lyyzoo/jvm/test01/User, int ] 357 stack = [ class java/lang/Exception ] 358 frame_type = 84 /* same_locals_1_stack_item */ 359 stack = [ class java/lang/Throwable ] 360 361 public void sayHello(java.lang.Object); 362 descriptor: (Ljava/lang/Object;)V 363 【過載泛型方法時,會多出 ACC_BRIDGE、ACC_SYNTHETIC 兩個標誌,ACC_BRIDGE代表是jvm自動生成的橋接方法,ACC_SYNTHETIC代表是jvm生成的不可見方法】 364 flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC 365 Code: 366 stack=2, locals=2, args_size=2 367 0: aload_0 368 1: aload_1 369 2: checkcast #21 // class java/lang/String 370 5: invokevirtual #22 // Method sayHello:(Ljava/lang/String;)V 371 8: return 372 LineNumberTable: 373 line 5: 0 374 LocalVariableTable: 375 Start Length Slot Name Signature 376 0 9 0 this Lcom/lyyzoo/jvm/test01/User; 377 MethodParameters: 378 Name Flags 379 str synthetic 380 381 【靜態程式碼塊】 382 static {}; 383 descriptor: ()V 384 flags: ACC_STATIC 385 Code: 386 stack=3, locals=0, args_size=0 387 0: ldc #23 // String Rambo 388 2: putstatic #24 // Field staticName:Ljava/lang/String; 389 5: bipush 20 390 7: putstatic #25 // Field staticAge:I 391 10: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 392 13: ldc #26 // String user static init 393 15: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 394 18: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 395 21: new #6 // class java/lang/StringBuilder 396 24: dup 397 25: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V 398 28: ldc #27 // String staticName= 399 30: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 400 33: getstatic #24 // Field staticName:Ljava/lang/String; 401 36: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 402 39: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 403 42: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 404 45: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 405 48: new #6 // class java/lang/StringBuilder 406 51: dup 407 52: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V 408 55: ldc #28 // String staticAge= 409 57: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 410 60: getstatic #25 // Field staticAge:I 411 63: invokevirtual #11 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 412 66: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 413 69: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 414 72: return 415 LineNumberTable: 416 line 13: 0 417 line 14: 5 418 line 22: 10 419 line 23: 18 420 line 24: 45 421 line 25: 72 422 } 423 Signature: #73 // Lcom/lyyzoo/jvm/test01/Person<Ljava/lang/String;>;Ljava/io/Serializable; 424 SourceFile: "User.java"View Code
我們也可以安裝 [jclasslib Bytecode viewer] 外掛,就可以在IDEA中清晰地看到 Class 包含的資訊:
1、魔數與Class檔案資訊
魔數唯一作用是確定這個檔案是否為一個能被虛擬機器接受的Class檔案。使用魔數而不是副檔名來進行識別主要是基於安全考慮,因為副檔名可以隨意改動。
Minior version 是次版本號,Major version 是主版本號。Java的版本號是從45開始的,JDK 1.1之後的每個JDK大版本釋出主版本號向上加 1,所以 jdk1.8 的 Major version 是 52。高版本的JDK能向下相容以前版本的Class檔案,但不能執行以後版本的Class檔案。
Access flags 用於識別類或者介面層次的訪問資訊,比如這個Class是類還是介面;是否定義為public型別;是否定義為abstract型別 等等。
2、常量池
虛擬機器把常量池組織為入口列表,常量池中的許多入口都指向其他的常量池入口(比如引用了其它類),而且 class 檔案中的許多條目也會指向常量池中的入口。列表中的第一項索引值為1,第二項索引值為2,以此類推。雖然沒有索引值為0的入口,但是 constant_pool_count 會把這一入口也算進去,比如上面的 Constant pool count 為 119,而常量池實際的索引值最大為 118。
常量池主要存放兩大類常量:字面量和符號引用。
- 字面量:字面量主要是文字字串、final 常量值、類名和方法名的常量等。
- 符號引用:符號引用對java動態連線起著非常重要的作用。主要的符號引用有:類和介面的全限定名、欄位的名稱和描述符、方法的名稱和描述符等。
常量池中每一項都是一個表,常量表主要有如下17種常量型別。
常量池的專案型別:
再理解下符號引用和直接應用:
- 符號引用:java 檔案在前端編譯期間,class 檔案並不知道它引用的那些類、方法、欄位的具體地址,不能被class檔案中的位元組碼直接引用。因此使用符號引用來代替,執行時再動態連線到具體引用上。符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。
- 直接引用:直接引用是可以直接指向目標的指標、相對偏移量或者是一個能間接定位到目標的控制代碼。直接引用是和虛擬機器實現的記憶體佈局直接相關的。如果有了直接引用,那引用的目標必定已經在虛擬機器的記憶體中存在。在執行時,Java虛擬機器從常量池獲得符號引用,然後在執行時解析引用項的實際地址。
比如看 sayHello 這個方法,首先要呼叫 super.sayHello,即父類 Person 的 sayHello 方法,那麼第三個指令就會在常量池尋找 [#16] 這個索引,然後可以從常量池找到這個方法的相關資訊,再通過 [#29] 找到 Person 類資訊。
3、類索引、父類索引與介面索引
Class檔案中由這三項資料來確定該型別的繼承關係。類索引用於確定這個類的全限定名,父類索引用於確定這個類的父類的全限定名。
它們各自指向一個型別為 CONSTANT_Class_info 的常量表,通過 CONSTANT_Class_info 常量中的索引值可以找到定義在 CONSTANT_Utf8_info 型別的常量中的全限定名字串。
4、欄位表
欄位表用於描述介面或者類中宣告的變數。Java語言中的“欄位”包括類級變數以及例項級變數,但不包括在方法內部宣告的區域性變數。
描述符:
- descriptor 是描述符,描述符的作用是用來描述欄位的資料型別、方法的引數列表(包括數量、型別以及順序)和返回值。
- 對於陣列型別,每一維度將使用一個前置的 “[” 字元來描述,如一個定義為 “java.lang.String[][]” 型別的二維陣列將被記錄成 “[[Ljava/lang/String;”,一個整型陣列“int[]”將被記錄成 “[I”。
- 用描述符來描述方法時,按照先引數列表、後返回值的順序描述,引數列表按照引數的嚴格順序放在一組小括號“()”之內。
描述符標識字元含義:
比如從構造方法的描述符 <Ljava/lang/String;I)V> 可以看出,方法的引數包括物件型別 java.lang.String、基本型別 int,返回值為 void。
5、方法表
方發表與欄位表類似,方發表用於描述方法的訪問標誌、名稱索引、描述符索引、屬性表集合、程式碼指令等
1)異常表:
如果方法表有異常捕獲的話,還會有異常表。當方法丟擲異常時,就會從異常表查詢能處理的異常處理器。
2)過載多出的方法:
如果父類方法在子類中被重寫,那方法表中就會包含父類方法的資訊,如果重寫泛型方法,還會出現編譯器自動新增的橋接方法。
因為泛型編譯後的實際型別為 Object,如果子類泛型不是 Object,那麼編譯器會自動在子類中生成一個 Object 型別的橋接方法。橋接方法的內部會先做型別轉換檢查,然後呼叫過載的方法。因為我們在宣告變數時一般是宣告的超類,實際型別為子類,而超類方法的引數是Object型別的,因此就會呼叫到橋接方法,進而呼叫子類過載後的方法。
而且,當我們通過反射根據方法名獲取方法時,要注意泛型過載可能獲取到橋接方法,此時可以通過 method.isBridge() 方法判斷是否是橋接方法。
3)類構造器和例項構造器:
方法表還包括例項構造方法 <init> 和類構造方法 <clinit> 。<init> 就是對應的例項構造器。<clinit> 是編譯時將類初始化的程式碼蒐集在一起形成的類初始化方法,如靜態變數賦值、靜態程式碼塊。
初始化階段會呼叫類構造器 <clinit> 來初始化類,因此其一定是執行緒安全的,是由虛擬機器來保證的。這種機制我們可以用來實現安全的單例模式,列舉類的初始化也是在 <clinit> 方法中初始化的。
6、屬性表
屬性表集合主要是為了正確識別Class檔案而定義的一些屬性,如 Code、Deprecated、ConstantValue、Exceptions、SourceFile 等等。
每一個屬性,它的名稱都要從常量池中引用一個 CONSTANT_Utf8_info 型別的常量來表示。
四、類載入
1、類初始化的時機
類和介面被載入的時機因不同的虛擬機器可能不同,但類初始化的觸發時機有且僅有六種情況:
- 當建立某個類的例項,如 new、反射、克隆、反序列化
- 當呼叫某個類的靜態方法時
- 當使用某個介面或類的靜態欄位,或者賦值時(final 修飾的常量除外,它在編譯期把結果放入常量池中了)
- 使用java.lang.reflect包的方法對型別進行反射呼叫的時候,如果型別沒有進行過初始化,則需要先觸發其初始化
- 當初始化類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化(介面除外)
- 當虛擬機器啟動時,會先初始化要執行的主類(包含main()方法的那個類)
這六種情況稱為對一個型別進行主動引用。除此之外,所有引用型別的方式都不會觸發初始化,稱為被動引用。
觸發介面初始化的情況:
- 類初始化時並不會觸發其實現的介面的初始化,介面初始化時也不會要求父介面初始化
- 在介面所宣告的非常量欄位被使用時,該接口才會被初始化
- 如果介面定義了 default 方法,那子類重寫了這個方法,就會先觸發介面的初始化
1)主動初始化:
從輸出可以看出,對 final 常量的引用不會觸發類的初始化,呼叫靜態方法時觸發了類的初始化,同時,一定會先觸發父類的初始化,而且類只會被初始化一次。
注意初始化的順序是按程式碼的順序從上到下初始化:
2)被動初始化,如下被動引用不會觸發類的初始化:
- 通過子類引用父類的靜態欄位,不會導致子類初始化。對於靜態欄位,只有直接定義這個欄位的類才會被初始化
- 通過陣列定義來引用類,不會觸發此類的初始化。但是會觸發一個 “[com.lyyzoo.jvm.test01.User” 型別的初始化,即一維陣列型別
- 引用類的常量不會觸發類的初始化。常量在編譯階段會存入呼叫類的常量池中,本質上沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化
3)不難判斷,例子中定義的類的載入順序如下:
2、載入
在載入階段,Java虛擬機器必須完成以下三件事情:
- 通過一個類的全限定名來獲取定義此類的二進位制位元組流。
- 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。
- 在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口。
這個二進位制流可以從 Class 檔案中獲取,從JAR包、WAR包中獲取,從網路中獲取,實時生成、還可以從加密檔案中獲取,在載入時再解密(防止Class檔案被反編譯)。這個載入是由類載入器載入進虛擬機器的,非陣列型別可以使用內建的引導類載入器來載入,也可以使用開發人員自定義的類載入器來載入,我們可以自己控制位元組流的獲取方式。而陣列型別本身不通過類載入器載入,它是由虛擬機器直接在記憶體中構造出來的。
載入階段會把 Class 常量池中的各項常量存放到執行時常量池中(下圖中的常量池只挑選了部分常量來展示)。載入階段的最終產品就是 Class 類的例項物件,它成為程式與方法區內部資料結構之間的入口,可以通過這個 Class 例項來獲得類的資訊、方法、欄位、類載入器等等。
在裝載過程中,虛擬機器還會確認裝載類的所有超類是否都被裝載了,根據 super class 項解析符號引用,這就會導致超類的裝載、連線和初始化。
3、驗證
這一階段的目的是確保Class檔案的位元組流中包含的資訊符合《Java虛擬機器規範》的全部約束要求,保證這些資訊被當作程式碼執行後不會危害虛擬機器自身的安全。
驗證階段會完成下面四個階段的檢驗:
- 檔案格式驗證:保證輸入的位元組流能正確地解析並存儲於方法區之內,通過這個階段的驗證之後,這段位元組流會進入Java虛擬機器記憶體的方法區中進行儲存,後面的驗證就是基於方法區的儲存結構而進行了。
- 元資料驗證:對類的元資料資訊進行語義校驗,如這個類是否有父類(除 java.lang.Object 外,所有的類都有父類)、是否繼承了 final 的類、實現了 final 的方法等。
- 位元組碼驗證:通過資料流分析和控制流分析,確定程式語義是合法的、符合邏輯的。對類的方法體進行校驗分析,保證被校驗類的方法在執行時不會做出危害虛擬機器安全的行為。
- 符號引用驗證:最後一個階段的校驗發生在虛擬機器將符號引用轉化為直接引用的時候,這個轉化動作將在連線的第三階段——解析階段中發生。
4、準備
準備階段是為類中定義的變數(即靜態變數,被static修飾的變數)分配記憶體並設定類變數初始值的階段,初始值是指這個資料型別的零值,而賦值的過程是放在 <clinit> 方法中,在初始化階段執行的。注意例項變數是在建立例項物件時才初始化值的。
基本資料型別的零值:
準備階段還會為常量欄位(final 修飾的常量,即欄位表中有 ConstantValue 屬性的欄位)分配記憶體並直接賦值為定義的字面值。
User 類經過準備階段後:
5、解析
解析過程就是根據符號引用查詢到實體,再把符號引用替換成一個直接引用的過程。因為所有的符號引用都儲存在常量池中,所以這個過程常被稱作常量池解析。
1)靜態解析與動態連線:
所有方法呼叫的目標方法在Class檔案裡面都是一個常量池中的符號引用,位元組碼中的方法呼叫指令就以常量池裡指向方法的符號引用作為引數。這些符號引用一部分會在類載入階段或者第一次使用的時候就被轉化為直接引用,這種轉化被稱為靜態解析。另外一部分將在執行期間用到時轉化為直接引用,這部分稱為動態連線。
靜態解析的前提是:方法在程式真正執行之前就有一個可確定的呼叫版本,並且這個方法的呼叫版本在執行期是不可改變的。這類方法包含 靜態方法、私有方法、例項構造器、父類方法以及被 final 修飾的方法,這5種方法呼叫會在類載入的時候就把符號引用解析為該方法的直接引用(有可能是在初始化的時候去解析的)。
動態連線這個特性給Java帶來了更強大的動態擴充套件能力,比如使用執行時物件型別,因為要到執行期間才能確定具體使用的型別。這也使得Java方法呼叫過程變得相對複雜,某些呼叫需要在類載入期間,甚至到執行期間才能確定目標方法的直接引用。
2)符號引用解析:
對於符號引用型別如 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info 等,會查詢到對應的型別資料、方法地址、欄位地址的直接引用,然後將符號引用替換為直接引用。
對於 CONSTANT_String _info 型別指向的字面量,虛擬機器會檢查字串常量池中是否已經有相同字串的引用,有則替換為這個字串的引用,否則在堆中建立一個新的字串物件,並將物件的引用放到字串常量池中,然後替換常量池中的符號引用。
對於數值型別的常量,如 CONSTANT_Long_info、CONSTANT_Integer_info,並不需要解析,虛擬機器會直接使用那些常量值。
6、初始化
直到初始化階段,Java虛擬機器才真正開始執行類中編寫的Java程式程式碼,初始化階段就是執行類構造器 <clinit> 方法的過程。
1)<clinit> 方法:
- <clinit> 方法是由編譯器自動收集類中的所有類變數的賦值語句和靜態程式碼塊合併產生的,程式碼執行的順序就是原始檔中的順序。
- Java虛擬機器會保證在子類的 <clinit> 方法執行前,父類的 <clinit> 方法會先執行完畢,即先初始化直接超類。
- <clinit> 方法對於類或介面來說不是必需的,如果一個類中沒有靜態語句塊,也沒有對變數的賦值操作,那麼編譯器可以不為這個類生成 <clinit> 方法。
- 介面中不能使用靜態語句塊,但仍然有變數初始化的賦值操作,因此介面與類一樣都會生成 <clinit> 方法。
- 執行介面的 <clinit> 方法不需要先執行父介面的 <clinit> 方法,因為只有當父介面中定義的變數被使用時,父接口才會被初始化。此外,介面的實現類在初始化時也一樣不會執行介面的 <clinit> 方法。
- Java虛擬機器會保證一個類的 <clinit> 方法在多執行緒環境中被正確地加鎖同步,<clinit> 一定是執行緒安全的。
2)User 類初始化後:
一個類被裝載、連線和初始化完成後,它就隨時可以使用了。程式可以訪問它的靜態欄位,呼叫它的靜態方法,或者建立它的例項。
7、即時編譯
初始化完成後,類在呼叫執行過程中,執行引擎會把位元組碼轉為機器碼,然後在作業系統中才能執行。在位元組碼轉換為機器碼的過程中,虛擬機器中還存在著一道編譯,那就是即時編譯。
最初,虛擬機器中的位元組碼是由直譯器( Interpreter )完成編譯的,當虛擬機發現某個方法或程式碼塊的執行特別頻繁的時候,就會把這些程式碼認定為“熱點程式碼”。為了提高熱點程式碼的執行效率,在執行時,即時編譯器(JIT)會把這些程式碼編譯成與本地平臺相關的機器碼,並進行各層次的優化,然後儲存到記憶體中,這樣可以減少直譯器的中間損耗,獲得更高的執行效率。如果沒有即時編譯,每次執行相同的程式碼都會使用直譯器編譯。
五、類載入器
1、類載入器子系統
在Java虛擬機器中,負責查詢並裝載型別的那部分被稱為類載入器子系統。類載入器子系統會負責整個類載入的過程:裝載、驗證、準備、解析、初始化。
1)Java 虛擬機器有兩種類載入器,啟動類載入器和使用者自定義類載入器:
- 啟動類載入器:是Java虛擬機器實現的一部分,啟動類載入器主要用來載入受信任的Java API 的 Class 檔案。
- 使用者自定義類載入器:是Java程式的一部分,使用者自定義的類載入器都是 java.lang.ClassLoader 的子類例項,開發人員可以自己控制位元組流的載入方式。
2)類唯一性:
對於任意一個類,都必須由載入它的類載入器和這個類本身一起共同確立其在Java虛擬機器中的唯一性。每一個類載入器,都擁有一個獨立的類名稱空間,由不同的類載入器載入的類將被放在虛擬機器內部的不同名稱空間中。比較兩個類是否“相等”,只有在這兩個類是由同一個類載入器載入的前提下才有意義,否則,即使這兩個類來源於同一個Class檔案,被同一個Java虛擬機器載入,只要載入它們的類載入器不同,那這兩個類就必定不相等。這就是有時候我們測試程式碼時發現明明是同一個Class,卻報強轉失敗之類的錯誤。
2、雙親委派模型
Java 1.8 之前採用三層類載入器、雙親委派的類載入架構。三層類載入器包括啟動類載入器、擴充套件類載入器、應用程式類載入器。
1)三層類載入器
- 啟動類載入器(Bootstrap ClassLoader):負責將 $JAVA_HOME/lib 或者 -Xbootclasspath 引數指定路徑下面的檔案(按照檔名識別,如rt.jar、tools.jar,名字不符合的類庫即使放在lib目錄中也不會被載入) 載入到虛擬機器記憶體中。它用來載入 Java 的核心庫,是用原生程式碼實現的,並不繼承自 java.lang.ClassLoader,啟動類載入器無法直接被 java 程式碼引用。
- 擴充套件類載入器(Extension ClassLoader):負責載入 $JAVA_HOME/lib/ext 目錄中的檔案,或者 java.ext.dirs 系統變數所指定的路徑的類庫,它用來載入 Java 的擴充套件庫。
- 應用程式類載入器(Application ClassLoader):一般是系統的預設載入器,也稱為系統類載入器,它根據 Java 應用的類路徑(CLASSPATH)來載入 Java 類。一般 Java 應用的類都是由它來完成載入的,可以通過 ClassLoader.getSystemClassLoader() 來獲取它。
2)雙親委派模型
除了啟動類載入器之外,所有的類載入器都有一個父類載入器。應用程式類載入器的父類載入器是擴充套件類載入器,擴充套件類載入器的父類載入器是啟動類載入器。一般來說,開發人員自定義的類載入器的父類載入器一般是應用程式類載入器。
雙親委派模型:類載入器在嘗試去查詢某個類的位元組程式碼並定義它時,會先代理給其父類載入器,由父類載入器先去嘗試載入這個類,如果父類載入器沒有,繼續尋找父類載入器,依次類推,如果到啟動類載入器都沒找到才從自身查詢。這個類載入過程就是雙親委派模型。
首先要明白,Java 虛擬機器判定兩個 Java 類是否相同,不僅要看類的全名是否相同,還要看載入此類的類載入器是否一樣。只有兩個類來源於同一個Class檔案,並且被同一個類載入器載入,這兩個類才相等。不同類載入器載入的類之間是不相容的。
雙親委派模型就是為了保證 Java 核心庫的型別安全的。所有 Java 應用都至少需要引用 java.lang.Object 類,也就是說在執行的時候,java.lang.Object 這個類需要被載入到 Java 虛擬機器中。如果這個載入過程由 Java 應用自己的類載入器來完成或者自己定義了一個 java.lang.Object 類的話,很可能就存在多個版本的 java.lang.Object 類,而這些類之間是不相容的。通過雙親委派模型,對於 Java 核心庫的類載入工作由引導類載入器來統一完成,保證了 Java 應用所使用的都是同一個版本的 Java 核心庫的類,是互相相容的。有了雙親委派模型,就算自己定義了一個 java.lang.Object 類,也不會被載入。
3)ClassLoader
類載入器之間的父子關係一般不是以繼承的關係來實現的,通常是使用組合、委託關係來複用父載入器的程式碼。ClassLoader 中有一個 parent 屬性來表示父類載入器,如果 parent 為 null,就會呼叫本地方法直接使用啟動類載入器來載入類。類載入器在成功載入某個類之後,會把得到的 java.lang.Class 類的例項快取起來。下次再請求載入該類的時候,類載入器會直接使用快取的類的例項,而不會嘗試再次載入。
3、執行緒上下文類載入器
執行緒上下文類載入器可通過 java.lang.Thread 中的方法 getContextClassLoader() 獲得,可以通過 setContextClassLoader(ClassLoader cl) 來設定執行緒的上下文類載入器。如果沒有通過 setContextClassLoader(ClassLoader cl) 方法進行設定的話,執行緒將繼承其父執行緒的上下文類載入器。Java 應用執行的初始執行緒的上下文類載入器是應用程式類載入器。線上程中執行的程式碼可以通過此類載入器來載入類和資源。執行緒上線文類載入器使得父類載入器可以去請求子類載入器完成類載入的行為,這在一定程度上是違背了雙親委派模型的原則。
六、物件及其生命週期
1、例項化物件
1)例項化一個類有四種途徑:
- 明確地使用 new 操作符
- 呼叫 Class 或者 java.lang.reflcct.Constructor 物件的 newInstance() 方法
- 呼叫任何現有物件的 clone() 方法
- 通過 java.io.ObjectInputStream 類的 getObject() 方法反序列化
2)例項化物件的過程:
- 1、當虛擬機器要例項化一個物件時,首先從常量池中找到這個類的符號引用,並檢查這個符號引用代表的類是否已被載入、解析和初始化過。如果沒有,就會觸發相應的類載入過程。
- 2、在類載入檢查通過後,虛擬機器將為新生物件分配記憶體,為物件分配空間就是把一塊確定大小的記憶體塊從Java堆中劃分出來。
- 3、記憶體分配完成之後,虛擬機器必須將分配到的記憶體空間(不包括物件頭)都初始化為零值。這步操作保證了物件的例項欄位在Java程式碼中可以不賦初始值就直接使用,使程式能訪問到這些欄位的資料型別所對應的零值。
- 4、接下來,虛擬機器還要對物件進行必要的設定,例如這個物件的型別資訊、元資料地址、物件的雜湊碼、物件的GC分代年齡等資訊,這些資訊存放在物件的物件頭之中。根據虛擬機器當前執行狀態的不同,如是否啟用偏向鎖等,物件頭會有不同的設定方式。
- 5、最後開始執行物件的建構函式,即Class檔案中的 <init> 方法,按照開發人員的意圖對物件進行初始化,這樣一個真正可用的物件才算完全被構造出來。
2、物件的記憶體佈局
在 HotSpot 虛擬機器裡,物件在堆記憶體中的儲存佈局可以劃分為三個部分:物件頭(Header)、例項資料(Instance Data)和對齊填充(Padding)。
1)物件頭:
物件頭主要由兩部分組成:Mark Word 和型別指標,如果是陣列物件,還會包含一個數組長度。
- Mark Word:用於儲存物件自身的執行時資料,如雜湊碼(HashCode)、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等。synchronized 鎖升級就依賴鎖標誌、偏向執行緒等鎖資訊,垃圾回收新生代物件轉移到老年代則依賴於GC分代年齡。
- 型別指標:物件指向它的型別元資料的指標,Java虛擬機器通過這個指標來確定該物件是哪個類的例項。
- 陣列長度:有了陣列長度,虛擬機器就可以通過普通Java物件的元資料資訊確定Java物件的大小,如果陣列的長度是不確定的,將無法通過元資料中的資訊推斷出陣列的大小。
這三部分資料的長度在32位和64位的虛擬機器(未開啟壓縮指標)中分別為32個位元和64個位元。64 位虛擬機器中,為了節約記憶體可以使用選項 +UseCompressedOops 開啟指標壓縮,某些資料會由 64位壓縮至32位。
2)例項資料:
例項資料部分是物件真正儲存的有效資訊,即物件的各個欄位資料,無論是從父類繼承下來的,還是在子類中定義的欄位都必須記錄起來。
3)對齊填充:
對齊填充僅僅起著佔位符的作用,由於HotSpot虛擬機器的自動記憶體管理系統要求物件起始地址必須是8位元組的整數倍,就是任何物件的大小都必須是8位元組的整數倍。物件頭部分已經被設計成正好是8位元組的倍數,因此,如果物件例項資料部分沒有對齊的話,就需要通過對齊填充來補全。
4)計算物件佔用記憶體大小:
從上面的內容可以看出,一個物件對記憶體的佔用主要分兩部分:物件頭和例項資料。在64位機器上,物件頭中的 Mark Word 和型別指標各佔 64 位元,就是16位元組。例項資料部分,可以根據型別來判斷,如 int 佔 4 個位元組,long 佔 8 個位元組,字串中文佔3個位元組、數字或字母佔1個位元組來計算,就大概能計算出一個物件佔用的記憶體大小。當然,如果是陣列、Map、List 之類的物件,就會佔用更多的記憶體。
3、物件訪問定位
建立物件後,這個引用變數會壓入棧中,即一個 reference,它是一個指向物件的引用,這個引用定位的方式主要有兩種:使用控制代碼訪問物件和直接指標訪問物件。
1)通過控制代碼訪問物件:
使用控制代碼訪問的話,Java堆中將可能會劃分出一塊記憶體來作為控制代碼池,reference中儲存的就是物件的控制代碼地址,而控制代碼中包含了物件例項資料與型別資料各自具體的地址資訊。
使用控制代碼來訪問的最大好處就是 reference 中儲存的是穩定控制代碼地址,在物件被移動(垃圾收集時移動物件)時只會改變控制代碼中的例項資料指標,而 reference 本身不需要被修改。
2)通過直接指標訪問物件:
如果使用直接指標訪問的話,Java堆中物件的記憶體佈局就必須放置訪問型別資料的相關資訊(Mark Word 中記錄了型別指標),reference 中儲存的直接就是物件地址,如果只是訪問物件本身的話,就不需要多一次間接訪問的開銷。
使用直接指標來訪問最大的好處就是速度更快,它節省了一次指標定位的時間開銷,HotSpot 虛擬機器主要就是使用這種方式進行物件訪問。
4、垃圾收集
當物件不再被程式所引用時,它所使用的堆空間就需要被回收,以便被後續的新物件所使用。JVM 的記憶體分配管理機制會自動幫我們回收無用的物件,它知道如何確定物件不再被引用,什麼時候去回收這些垃圾物件,使用什麼回收策略來回收更高效,以及如何管理記憶體,這部分就是JVM的垃圾收集相關的內容了。
參考
本文是學習、參考瞭如下書籍和課程,再通過自己的總結和實踐總結而來。如果想了解更多深入的細節,建議閱讀原著。
《深入JAVA虛擬機器 第二版》
《深入理解Java虛擬機器:JVM高階特性與最佳實踐 第三版》
《極客時間:Java效能調優實戰》
&n