概述
- Java字節代碼:byte[]
- Java類在JVM的表現形式:Class類的對象;
Java源代碼被編譯成class字節碼 :
Java字節代碼 --> Class類的對象:
- 加載:把Java字節碼byte[]轉換成JVM中的java.lang.Class類的對象;
- 鏈接:Java類的鏈接指的是將Java類的二進制代碼合並到JVM的運行狀態之中的過程。
- 初始化:主要是執行靜態代碼塊和初始化靜態域;
Java類的加載
作用
把Java字節碼轉換成JVM中的java.lang.Class類的對象;
- 通過一個類的全限定名獲取描述此類的二進制字節流;
- 將這個字節流所代表的靜態存儲結構保存為方法區的運行時數據結構;
- 在java堆中生成一個代表這個類的java.lang.Class對象,作為訪問方法區的入口;
類加載器分類
- 啟動類加載器(Bootstrap ClassLoader):負責加載 JAVA_HOME\lib 目錄中的,或通過-Xbootclasspath參數指定路徑中的,且被虛擬機認可(按文件名識別,如rt.jar)的類;
- 擴展類加載器(Extension ClassLoader):負責加載 JAVA_HOME\lib\ext 目錄中的,或通過java.ext.dirs系統變量指定路徑中的類庫;
- 應用程序類加載器(Application ClassLoader):負責加載用戶路徑(classpath)上的類庫;
雙親委派模型工作過程:
當一個類加載器收到類加載任務,優先交給其父類加載器去完成,因此最終加載任務都會傳遞到頂層的啟動類加載器,只有當父類加載器無法完成加載任務時,才會嘗試執行加載任務。
雙親委派模型有什么好處?
比如位於rt.jar包中的類java.lang.Object,無論哪個加載器加載這個類,最終都是委托給頂層的啟動類加載器進行加載,確保了Object類在各種加載器環境中都是同一個類。
重要特征
- 層次組織結構:每個類加載器都有一個父類加載器,形成tree結構;
- 代理模式:一個類加載器既可以自己完成Java類的定義工作,也可以代理給其它的類加載器來完成;
Java類的鏈接
Java類的鏈接指的是將Java類的二進制代碼合並到JVM的運行狀態之中的過程。
包含的步驟:
- 驗證:確保Java類的二進制表示在結構上是完全正確的,主要包括格式驗證、元數據驗證、字節碼驗證和符號引用驗證;
- 准備:創建Java類中的靜態域,並將這些域的值設為默認值;
在准備階段,為類變量(static修飾)在方法區中分配內存並設置初始值。
private static int var = 100;
准備階段完成后,var值為0,而不是100。在初始化階段,才會把100賦值給val,但是有個特殊情況:
private static final int VAL= 100;
在編譯階段會為VAL生成ConstantValue屬性,在准備階段虛擬機會根據ConstantValue屬性將VAL賦值為100。
3. 解析:解析階段是將常量池中的符號引用替換為直接引用的過程,解析過程可能導致其他的Java類被加載;
- 符號引用使用一組符號來描述所引用的目標,可以是任何形式的字面常量,定義在Class文件格式中。
- 直接引用可以是直接指向目標的指針、相對偏移量或則能間接定位到目標的句柄。
Java類的初始化
初始化階段是執行類構造器clinit方法的過程,clinit方法由類變量的賦值動作和靜態語句塊按照在源文件出現的順序合並而成,該合並操作由編譯器完成。
- 方法clinit對於類或接口不是必須的,如果一個類中沒有靜態代碼塊,也沒有靜態變量的賦值操作,那么編譯器不會生成
; - 方法clinit方法與實例構造器不同,不需要顯式的調用父類的
方法,虛擬機會 保證父類的 優先執行 ; - 為了防止多次執行clinit,虛擬機會確保clinit方法在多線程環境下被正確的加鎖同步執行,如果有多個線程同時初始化一個類,那么只有一個線程能夠執行clinit方法,其它線程進行阻塞等待,直到clinit執行完成。
- 注意:執行接口的clinit方法不需要先執行父接口的clinit,只有使用父接口中定義的變量時,才會執行。
- 初始化過程的主要操作是執行靜態代碼塊和初始化靜態域。
- 在一個類被初始化之前,它的直接父類也需要被初始化。
類初始化場景
虛擬機中嚴格規定了有且只有5種情況必須對類進行初始化。
- 執行new、getstatic、putstatic和invokestatic指令;
- 使用reflect對類進行反射調用;
- 初始化一個類的時候,父類還沒有初始化,會事先初始化父類;
- 啟動虛擬機時,需要初始化包含main方法的類;
- 在JDK1.7中,如果java.lang.invoke.MethodHandler實例最后的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄對應的類沒有進行初始化;
不會觸發類初始化的情況
- 通過子類引用父類的靜態字段,只會觸發父類的初始化,而不會觸發子類的初始化。
class Parent {
static int a = 100;
static {
System.out.println("parent init!");
}
}
class Child extends Parent {
static {
System.out.println("child init!");
}
}
public class Init{
public static void main(String[] args){
System.out.println(Child.a); //不會初始化類Child
}
}
輸出結果為:parent init! 不會初始化Child類。
- 定義對象數組,不會觸發該類的初始化。
public class Init{
public static void main(String[] args){
Parent[] parents = new Parent[10]; //不會初始化類Parent
}
}
- 常量在編譯期間會存入調用類的常量池中,本質上並沒有直接引用定義常量的類,不會觸發定義常量所在的類。
class Const {
static final int A = 100; //編譯階段,常量A存儲到Init類的常量池中
static {
System.out.println("Const init");
}
}
public class Init{
public static void main(String[] args){
System.out.println(Const.A);
}
}
在編譯階段,Const類中常量A的值100存儲到Init類的常量池中,這兩個類在編譯成class文件之后就沒有聯系了。
- 通過類名獲取Class對象,不會觸發類的初始化。
public class test {
public static void main(String[] args) throws ClassNotFoundException {
Class c_dog = Dog.class; //不會初始化Dog類
Class clazz = Class.forName("zzzzzz.Cat"); //會初始化Cat類
}
}
class Cat {
private String name;
private int age;
static {
System.out.println("Cat is load");
}
}
class Dog {
private String name;
private int age;
static {
System.out.println("Dog is load");
}
}
-
通過Class.forName加載指定類時,如果指定參數initialize為false時,也不會觸發類初始化,其實這個參數是告訴虛擬機,是否要對類進行初始化。
-
通過ClassLoader默認的loadClass方法,也不會觸發初始化動作;