類加載基本介紹
JAVA類加載分為三步: 加載、連接配接 、初始化。
類加載的最終産物是位于heap(堆)中的類對象,Class對象封裝了在方法區内的類的資料結構,提供了通路方法區類資料結構的接口(即反射接口)。
下面看一段代碼:
class Singleton {
private static Singleton singleton=new Singleton();
public static int counter1;
public static int counter2=0;
private Singleton(){
counter1++;
counter2++;
}
public static Singleton getInstance(){
return singleton;
}
}
public class Test {
public static void main(String[] args) {
Singleton singleton=Singleton.getInstance();
System.out.println(singleton.counter1);
System.out.println(singleton.counter2);
}
}
輸出是1和0,看完下面你就知道為什麼了
類加載過程
加載:将位于硬碟中的class檔案加載到記憶體中。
連接配接:将已經加載到記憶體中的二進制資料合并到虛拟機的運作時環境中去。
驗證:
1.確定類檔案遵從java類檔案的固定格式。
2.确定類本身符合java語言的文法規定,比如final修飾的類沒有子類,以及final類型的方法沒有被覆寫(因為可以手動生成class檔案,繞過編譯過程)。
3.位元組碼驗證:確定位元組碼流可以被java虛拟機安全的執行。位元組碼流代表java方法(包括靜态方法和執行個體方法),它是由被稱作操作碼的單位元組指令組成的序列,每一個操作碼後都跟着一個或多個操作數。位元組碼驗證步驟會檢查每個操作碼是否合法,即是否有合法的操作數。
4.二進制相容性的驗證:確定互相引用的類之間協調一緻。(例如Worker類的gotoWork()方法調用了Catr類的run()方法,java虛拟機在驗證Worker類時,會檢查方法區内是否存在Car類的run()方法,假如不存在(兩個類的jdk版本不相容,就會出現這種問題),就會抛出NoSuchMethodError錯誤)。
準備:
将class對象的靜态變量配置設定記憶體空間并賦予預設值,比如int類型的預設值為0,引用對象比如一個自定義的Person對象預設值為null。
解析:
将類的二進制資料中的符号引用轉換為直接引用
例如Worker類的gotoWork()方法調用了Catr類的run()方法,Worker類的二進制資料中包含了一個對run()方法的符号引用。在解析階段将符号引用(符号引用:由方法的全名和相關描述組成)變為直接引用(直接引用:指針),也就是說由描述變為記憶體位址。
初始化:
将類的靜态變量賦予初始值。如果在類中靜态變量沒有被賦予初值,那初始化時将保持他的預設值。
什麼情況下觸發類初始化?
類被主動使用 6種情況
1.靜态變量被使用或靜态變量被指派
2.靜态方法被調用
3.反射 class.forName()
4.當子類被初始化時發現父類未被初始化
5.建立類的執行個體
6.java虛拟機啟動時被辨明為啟動類的類
看到這應該可以明白上面單例的代碼為什麼會輸出1和0了
分析:
首先調用靜态getInstance()方法,類加載
類加載過程:
第一步,加載,将類加載到記憶體中方法區。
第二步,連接配接時進行準備,将靜态變量賦予預設值。
singleton預設值為null
counter1預設值為0
counter2預設值為0,此處的0并不是=0指派的0,指派在之後初始化時進行,此時賦予的int類型的預設值。
第三步,類初始化
singleton指向new Singleton 調用了構造方法此時執行構造方法,counter1++,此時counter1的值為1,counter2++,此時counter2的值為1。此時對counter1進行初始化指派,由于counter1沒有在定義變量時指派,是以counter1的值為1。此時對counter2進行初始化指派,定義變量時counter2=0,是以counter2的值變為0。
初始化語句
靜态變量的聲明語句,以及靜态代碼塊都是類的初始化語句(也就是類初始化的時候會被執行的語句),java虛拟機按照初始化語句在類檔案中的先後順序依次執行它們。
父類初始化
程式中對子類的主動使用會導緻父類初始化;但對父類的主動使用并不會導緻對子類的初始化
class Parent2{
static int a=3;
static{
System.out.println("Parent2 static bolck");
}
}
class child2 extends Parent2{
static int b=4;
static {
System.out.println("child2 static block");
}
}
public class Test5 {
static {
System.out.println("Test5 static block");
}
public static void main(String[] args) {
Parent2 parent;
System.out.println("--------------------");
parent=new Parent2();
System.out.println(Parent2.a);
System.out.println(child2.b);
}
}
類的初始化步驟
1.假如這個類還沒有被加載和連接配接,那就先進行加載和連接配接。
2.假如類存在直接的父類,并且這個父類還沒有被初始化,那麼先初始化父類
3.假如類中存在初始化語句,那就依次執行這些初始化語句
主動使用的陷阱
隻有當程式通路的靜态變量或靜态方法确實在目前類或目前接口中定義時,才可以認為是對類或接口的主動使用
class Parent3{
static int a=3;
static {
System.out.println("Parent3 static block");
}
static void doSomething(){
System.out.println("doSomething");
}
}
class Child3 extends Parent3{
static int b=4;
static {
System.out.println("Child3 static block");
}
}
public class Test6 {
public static void main(String[] args) {
System.out.println(Child3.a);
Child3.doSomething();
}
}
調用常量會導緻類初始化麼
常量如果在編譯期能确定其值,那麼常量被調用并不會導緻類的初始化(類初始化條件之一為:調用類的靜态變量);反之,常量如果在編譯期不能能确定其值,必須在運作時确定值那麼會導緻類的初始化
class FinalTest{
public static final int x=6/3;
static {
System.out.println("Final static block");
}
}
public class Test2 {
public static void main(String[] args) {
System.out.println(FinalTest.x);
}
}
此時并不會輸出 Final static block ,隻會輸出 2。因為x=2在編譯期就被确定了,根據常量傳播優化,已經将此常量x的值“2“,存儲到了Test2類的常量池中,以後Test2對常量FinalTest.x的引用實際都轉化為Test2類對自身常量池的引用了。也就是說,實際上Test2的Class檔案中并沒有FinalTest類的符号引用入口,這兩個類在編譯成class檔案後就不存在任何關系了。
class FinalTest2{
public static final int x=new Random().nextInt(100);
static {
System.out.println("FinalTest2 static block");
}
}
public class Test3 {
public static void main(String[] args) {
System.out.println(FinalTest2.x);
}
}
最後
當java虛拟機初始化一個類時,要求它的所有父類都已經被初始化,但是這條規則并不适用于接口
1.在初始化一個類時,并不會先初始化它實作的接口
2.在初始化一個接口時,并不會先初始化它的父接口
接口,隻有當程式使用接口的靜态變量時,才會導緻該接口的初始化
參考:張龍老師的JAVASE視訊