類加載階段
1. 加載
- 加載:將類的字節碼載入方法區中,內部采用C++的instanceKlass描述java類。
- 如果這個類的父類還沒加載,則先加載父類
- 加載和鏈接可能是交替運行的
- 通過全限定名獲取字節碼
- 從文件系統(
.class
文件)、JAR 包、網絡、動態代理生成等途徑讀取二進制數據。
- 從文件系統(
- 將字節碼解析為方法區的運行時數據結構
- 在方法區(元空間)存儲類的靜態結構(如類名、字段、方法、父類、接口等)。
- 在堆中生成
Class
對象- 創建一個
java.lang.Class
實例,作為方法區數據的訪問入口。
- 創建一個
2. 鏈接
- 驗證:驗證類是否符合JVM規范(安全性檢查)
- 準備:為static變量分配空間,設置默認值
- static變量分配空間和賦值是兩個步驟,分配空間在準備階段完成,賦值在初始化階段完成。
- 如果static變量是final基本類型以及字符串常量:編譯階段就確定了,賦值在準備階段完成
- 如果static變量是final的,但是屬于引用類型,賦值也會在初始化階段完成
- static變量分配空間和賦值是兩個步驟,分配空間在準備階段完成,賦值在初始化階段完成。
- 解析:將常量池中的符號引用解析為直接引用。(用符號描述目標轉變為用他們在內存中的地址描述他們)
3. 初始化
<cint>()V
方法
初始化即調用 <cint>()V
方法,虛擬機會保證這個類的構造方法的線程安全
發生的時機
類的初始化是懶惰的。
- main方法所在的類,優先被初始化
- 首次訪問這個類的靜態變量或靜態方法
- 子類初始化時,如果父類還沒初始化,會先初始化父類
- 子類訪問父類的靜態變量,只會觸發父類的初始化。
- 執行Class.forName
- new會導致初始化
不會導致初始化:
- 訪問類的static final靜態常量(基本類型和字符串),不會觸發初始化
- 類對象.class不會觸發初始化
- 創建該類的數組不會觸發初始化
- 類加載器的loadClass方法,不會觸發初始化
- Class.forName的參數2為false時,不會觸發初始化
public class Load01 {public static void main(String[] args) {System.out.println(E.a); // 不會被初始化(基本類型)System.out.println(E.b); // 不會被初始化(字符串)System.out.println(E.c); // 會被初始化(包裝類型)}
}
class E {public static final int a = 10;public static final String b = "hello";public static final Integer c = 20;static {System.out.println("init E");}
}
懶惰初始化單例模式
public class Load02 {public static void main(String[] args) {Singleton.test();System.out.println(Singleton.getInstance()); // 懶漢式,只有調用getInstance()方法時,才會加載內部的LazyHolder}
}
class Singleton {// 私有構造方法private Singleton(){}public static void test() {System.out.println("test");}private static class LazyHolder {private static Singleton SINGLETON = new Singleton();static {System.out.println("LazyHolder init");}}public static Singleton getInstance() {return LazyHolder.SINGLETON;}
}
類加載器
名稱 | 加載哪的類 | 說明 |
---|---|---|
Bootstrap ClassLoader | JAVA_HOME/jre/lib | 無法直接訪問 |
Extension ClassLoader | JAVA_HOME/jre/lib/ext | 上級為Bootstrap |
Application ClassLoader | classpath | 上級為Extension |
自定義類加載器 | 自定義 | 上級為Applicaiton |
啟動類加載器
啟動類加載器是由C++程序編寫的,不能直接通過java代碼訪問,如果打印出來的是null,說明是啟動類加載器。
public class Load03 {public static void main(String[] args) throws ClassNotFoundException {Class<?> aClass = Class.forName("pers.xiaolin.jvm.load.F");System.out.println(aClass.getClassLoader()); // null}
}
public class F {static {System.out.println("bootstarp F init");}
}
使用
java -Xbootclasspath/a:. pers.xiaolin.jvm.load.Load03
將這個類加入bootclasspath之后,輸出null,說明是啟動類加載器加載的這個類
java -Xbootclasspath:<new bootclasspath>
java -Xbootclasspath/a:<追加路徑>
java -Xbootclasspath/p:<追加路徑>
應用程序類加載器
public class Load04 {public static void main(String[] args) throws ClassNotFoundException {Class<?> aClass = Class.forName("pers.xiaolin.jvm.load.G");System.out.println(aClass.getClassLoader()); // sun.misc.Launcher$AppClassLoader@18b4aac2(應用程序類加載器)}
}public class G {static {System.out.println("G init");}
}
雙親委派模式
雙親委派:調用類加載器loadClass方法時,查找類的規則。
每次都去上級類加載器中找,如果找到了就加載,如果上級沒找到,才由本級的類加載器進行加載。
執行流程
protected Class<?> loadClass(String name, boolean resolve) {synchronized (getClassLoadingLock(name)) {// 1. 檢查類是否已加載Class<?> c = findLoadedClass(name);if (c == null) {try {// 2. 委托父加載器加載if (parent != null) {c = parent.loadClass(name, false);} else { // parent == null,說明到了啟動類加載器c = findBootstrapClassOrNull(name); // 父加載器是 Bootstrap}} catch (ClassNotFoundException e) {}// 3. 父加載器未找到,則自行加載if (c == null) {c = findClass(name);}}return c;}
}
核心作用
- 避免類重復加載:確保一個類在JVM中只存在一份(由最頂層的類加載器優先加載),如果用戶自己定義了一個
java.lang.String
,那么這個類并不會被加載,而是由最頂層的Bootstrap加載核心的String類 - 保證安全性:防止核心類被篡改,通過優先委托父類加載器,確保核心類由可信源加載
- 分工明確:Bootstrap(加載JVM核心類)、Extension(加載擴展功能)、Application(加載用戶代碼)
破壞雙親委派場景
雙親委派并非強制約束,有些情況也會破壞它,否則有些類他是找不到的。
- 核心庫(JDBC)需要調用用戶實現的驅動(mysql-connector-java)
通過
Thread.currentThread().getContextClassLoader()
獲取線程上下文加載器(通常是Application ClassLoader
),直接加載用戶類。
- 不同模塊可能需要隔離或共享類
自定義類加載器,按照需要選擇是否委派父加載器
- 熱部署:動態替換已經加載的類
自定義類加載器直接重新加載類,不委派父類加載器
自定義類加載器
使用場景
- 需要加載非classpath路徑中的類文件
- 框架設計:都是通過接口來實現,希望解耦
- tomcat容器:這些類有多種版本,不同版本的類希望能隔離。
步驟
- 繼承ClassLoader父類
- 要遵守雙親委派機制,重寫findClass方法(注意不是重寫loadClass方法,否則不會走雙親委派)
- 讀取類文件中的字節碼
- 調用父類的defineClass方法來加載類
- 使用者調用該類加載器的loadClass方法
public class Load05 {public static void main(String[] args) throws ClassNotFoundException {MyClassLoader classLoader = new MyClassLoader();// 5. 使用者調用該類加載器的loadClass方法Class<?> c1 = classLoader.loadClass("MapImpl1");Class<?> c2 = classLoader.loadClass("MapImpl1");System.out.println(c1 == c2); // trueMyClassLoader classLoader2 = new MyClassLoader();Class<?> c3 = classLoader2.loadClass("MapImpl1");System.out.println(c1 == c3); // false }
}// 1. 繼承ClassLoader父類
class MyClassLoader extends ClassLoader {// 2. 重寫findClass方法@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException { // name就是類名稱String path = "d:\\myclasspath" + name + ".class";try {ByteArrayOutputStream os = new ByteArrayOutputStream();Files.copy(Paths.get(path), os);// 3. 讀取類文件中的字節碼byte[] bytes = os.toByteArray();// 4. 調用父類的defineClass方法來加載類return defineClass(name, bytes, 0, bytes.length); // byte[] -> *.class} catch (IOException e) {e.printStackTrace();throw new ClassNotFoundException("類文件未找到", e);}}
}
唯一確定類的方式應該是:
包名
、類名
、類加載器
相同
運行期優化
逃逸分析
【現象
】:循環內創建了1000個Object對象,但未被外部引用。
【JIT優化
】:JIT編譯器(尤其是C2編譯器)會通過逃逸分析(Escape Analysis)發現這些對象是方法局部作用域且未逃逸(即不會被其他線程或方法訪問),因此會直接優化掉對象分配。實際運行時,這些對象可能根本不會在堆上分配內存,而是被替換為標量或直接在寄存器中處理。
public class JIT01 {public static void main(String[] args) {for(int i = 0; i < 200; ++i) {long start = System.nanoTime();for(int j = 0; j < 1000; ++j) {new Object();}long end = System.nanoTime();System.out.printf("%d\t%d\n", i, (end - start));}}
}
在運行期間,虛擬機會對這段代碼進行優化。
JVM將執行狀態分為5個層次:
- 0層:解釋執行
- 1層:使用C1即時編譯器編譯執行(不帶profiling)
- 2層:使用C1即時編譯器編譯執行(帶基本的profiling)
- 3層:使用C1即時編譯器編譯執行(帶完全的profiling)
- 4層:使用C2即時編譯器編譯執行
profiling是在運行過程中收集一些程序執行狀態的數據(方法的調用次數、循環次數…)
解釋器:將字節碼解釋成機器碼,下次遇到相同的字節碼,仍然會執行重復的解釋
即時編譯器(JIT):就是把反復執行的代碼編譯成機器碼,存儲在Code Cache,下次再遇到相同的代碼,直接執行,無需編譯。
解釋器是將字節碼解釋為爭對所有平臺都通用的機器碼;JIT會根據平臺類型,生成平臺特定的機器碼。
對于占據大部分不常用的代碼,無需耗費時間將其編譯成機器碼,直接采取解釋執行的方式;對于僅占用小部分的熱點代碼, 可以將其編譯成機器碼。(運行效率:Iterpreter < C1 < C2
)
方法內聯
例子1
private static int square(final int i) {return i * i;
}
System.out.println(square(9));
如果發現square是熱點方法,并且長度不會太長時,就會進行內聯(把方法內的代碼拷貝到調用位置)
System.out.println(9 * 9);
例子2
public class JIT02 {int[] elements = randomInts(1_000);int sum = 0;void doSum(int x) {sum += x;}public void test() {for(int i = 0; i < elements.length(); ++i) {doSum(elements[i]);}}
}
方法內聯也會導致成員變量讀取時的優化操作。
上邊的test()方法,會被優化成:
public void test() {// elements.length首次讀取會緩存起來 ==> int[] localfor(int i = 0; i < elements.length(); ++i) { // 后續999次,求長度(不需要訪問成員變量,直接從loca中取)sum += elements; // 后續1000次,取下標(不需要訪問成員變量,直接從loca中取)}
}
反射優化
1. 初始階段:解釋執行(未優化)
- 前幾次調用(約0~5次):
Method.invoke
會走完整的 Java反射邏輯,包括:- 方法權限檢查(
AccessibleObject
)。 - 參數解包(
Object[]
轉原始類型)。 - 動態方法解析(通過JNI調用底層方法)。
- 方法權限檢查(
- 性能極差:單次調用耗時可能是直接調用的 20~100倍(微秒級 vs 納秒級)。
2. 中間階段:JIT初步優化(方法內聯+膨脹閾值)
- 調用次數達到閾值(約5~15次):
JIT編譯器(C2)開始介入優化:- 方法內聯(Inlining):
- 如果
foo()
是簡單方法(如本例的System.out.println
),JIT會嘗試內聯它。 - 但
Method.invoke
本身 無法直接內聯(因反射調用是動態的)。
- 如果
- 膨脹閾值(Inflation Threshold):
- JVM默認設置
-XX:InflationThreshold=N
(通常N=15),當反射調用超過此閾值時,JVM會生成 動態字節碼存根(Native Method Accessor),替代原始反射邏輯。 - 優化效果:
調用從JNI方式轉為直接調用生成的存根代碼,性能提升約 5~10倍。
- JVM默認設置
- 方法內聯(Inlining):
3. 最終階段:動態字節碼生成(最高效)
- 超過膨脹閾值(如15次后):
JVM為foo.invoke()
生成專用的 字節碼訪問器(GeneratedMethodAccessor):
// 偽代碼:生成的動態類class GeneratedMethodAccessor1 extends MethodAccessor {public Object invoke(Object obj, Object[] args) {Reflect01.foo(); // 直接調用目標方法,繞過反射檢查!return null;}}
- 優化點:
- 完全跳過權限檢查 和 參數解包(因JVM確認方法簽名固定)。
- 通過字節碼直接調用
foo()
,性能接近 直接方法調用(納秒級)。