ClassLoader的主要職責就是負責各種class文件到jvm中,ClassLoader是一個抽象的class,給定一個class文件的二進制名,ClassLoader會嘗試加載並且在jvm中生成構建這個類的各個數據結構,然后使其分布在對應的內存區域中。
- 1類的加載過程簡介
類的記載過程一般分為三個比較大的階段,分別是加載階段,連接階段和初始化階段,如下圖所示
加載階段:主要負責查找並且加載類的二進制數據文件,其實就是class文件。
連接階段:連接階段所做的工作比較多,細分的話還可以分為如下三個階段。
- 驗證:主要是確保類文件的正確性,比如class文件的版本,class文件的魔術因子是否正確。
- 准備:為類的靜態變量分配內存,並且為其初始化默認值。
- 解析:把類中的符號引用轉換為直接引用。
初始化階段:為類的靜態變量賦予正確的初始值(代碼編寫階段給定的值)
jvm對類的初始化是一個延遲的機制,即:使用的是lazy的方式,當一個類在首次使用的時候才會被初始化,在同一個運行時包下,一個class只會被初始化一次
(運行時包和類的包時有區別的,下次再說),那么什么是類的主動使用和被動使用呢?
- 2 類的主動使用和被動使用
jvm虛擬機規范規定了,每個類或者接口被java程序首次主動使用時才會對其進行初始化,當然隨着JIT技術越來越成熟,JVM運行期間的編譯也越來越只能,
不排除JVM在運行期間提前預判並且初始化某個類。
JVM同時規范了以下6種主動使用類的場景,具體如下
- 通過new關鍵字會導致類的初始化:這種是大家經常采用的初始化一個類的方式,它肯定會導致類的加載並且最終初始化。
- 訪問類的靜態變量,包括讀取和更新會導致類的初始化,這種情況的示例代碼如下:
public class SimpleOne { static{ System.out.println("我會被初始化"); } public static int x = 10; }
這段代碼中x是一個簡單的靜態變量,其他類即使不對SimpleOne進行new的創建,直接訪問x也會導致類的初始化。
- 訪問類的靜態方法,會導致類的初始化,這種情況的示例代碼如下:
public class SimpleTwo { static{ System.out.println("我會被初始化"); } // 靜態方法 public static void test(){ } }
同樣,在其他類中直接調用test靜態方法也會導致類的初始化。
- 對某個類進行反射操作,會導致類的初始化,這種情況的示例代碼如下:
public class InvokeClass { public static void main(String[] args) { try{ Class.forName("com.lanlei.classLoader.SimpleOne"); }catch(ClassNotFoundException e){ } } }
運行上面的代碼,同樣會看到靜態代碼塊中的輸出語句執行。
- 初始化子類會導致父類的初始化,這種情況的示例代碼如下:
public class Parent { static{ System.out.println("父類初始化了"); } public static int y = 100; }
public class Child extends Parent{ static{ System.out.println("子類會被初始化"); } public static int x = 10; }
public class ActiveLoadTest { public static void main(String[] args) { System.out.println(Child.x); } }
在ActiveLoadTest中,我們調用了Child的靜態變量,根據前面的知識可以得出Chid類被初始化了,Child類又是Parent類的子類,子類的初始化會進一步導致
父類的初始化,當然這里需要注意的一點是,通過子類使用父類的靜態變量只會導致父類的初始化,子類則不會被初始化,示例代碼如下:
public class ActiveLoadTest { public static void main(String[] args) { System.out.println(Child.y); } }
改寫后的ActiveLoadTest,直接用Child訪問子類的靜態變量y,並不會導致Child的初始化,僅僅會導致Parent的初始化。
- 啟動類:也就是執行main函數所在的類會導致該類的初始化,比如使用java命令運行上下文中的ActiveLoadTest類。
除了上述6種情況,其余的都被稱為被動引用,不會導致類的加載和初始化。
- 3 類的加載過程詳解
在正式講解類的各個階段的內容之前,請大家思考下面這段程序的輸出結果,如果你不能准確計算出結果或者感覺有點模棱兩可,那么請認真看完本小節。
public class Singleton { // ① private static int x =0; private static int y; private static Singleton instance = new Singleton(); // ② private Singleton(){ x++; y++; } public static Singleton getInstance(){ return instance; } public static void main(String[] args) { Singleton singleton = Singleton.getInstance(); System.out.println(singleton.x); System.out.println(singleton.y); } }
運行上面的程序代碼輸出將是多少?如果將注釋②的代碼移到注釋①的位置,輸出結果又是什么呢?兩種輸出會產生不一樣的結果,為何會發生這樣的
現象,下面就看下本小節尋找答案。
- 3.1 類的加載階段
簡單來說,類的加載就是將class文件中的二進制數據讀取到內存中,然后將該字節流所代表的靜態存儲結構轉換為方法區中運行時的數據結構,並且在堆內存總
生成一個該類的java.lang.Class對象,作為訪問方法區數據結構的入口,如下圖所示。
類加載的最終產物就是堆內存中的class對象,對同一個ClassLoader來講,不管某個類被加載了多少次,對應到堆內存中的class對象始終是同一個。虛擬機
規范中指出了類的加載是通過一個全限定名(包名+類名)來獲取二進制數據流,但是並沒有限定必須通過某種方式獲得,比如我們常見的二進制文件的形式,
但是除此之外還會有如下的幾種形式。
- 運行時動態生成,比如通過開源的ASM包可以生成一些class,或者通過動態代理java.lang.Proxy也可以生成代理類的二進制字節流。
- 通過網絡獲取,比如很早之前的Applet小程序,以及RMI動態發布等。
- 通過讀取zip文件獲得類的二進制字節流,比如jar、war(其實,jar和war使用的是和zip同樣的壓縮算法)。
- 將類的二進制數據存儲在數據庫的BLOB字段類型中。
- 運行時生成class文件,並且動態加載,比如使用Thrift,AVRO等都是可以在運行時將某個Schema文件生成對應的若干個class文件,然后進行加載。
- 3.2類的連接階段
類的連接階段可以細分為三個小的過程,分別為驗證,准備,解析。
- 驗證
先寫到這,改天補充,有點事