文章目錄
本篇的目的,是通過具體代碼解釋jvm類加載的詳細過程。再結合之前的理論說明,完全可以應對所有類加載的相關問題
類加載的整體說明,請參看 《JVM類加載總結》
1、準備試驗用代碼
父類:
public class ParentClass {
public static final String PARENT_FINAL_P = "父類常量";
public static String parentP = "父類成員變量";
static{
System.out.println("父類靜態代碼塊");
}
{
System.out.println("父類動態代碼塊");
}
public ParentClass() {
System.out.println("父類構造方法");
}
public static void parentStaticMethod(){
System.out.println("父類靜態方法");
}
private String propertityA;
public String getPropertityA() {
return propertityA;
}
public void setPropertityA(String propertityA) {
this.propertityA = propertityA;
}
}
子類:
public class ChildClass extends ParentClass{
public static final String CHILD_FINAL_P = "子類常量";
public static String childP = "子類成員變量";
static{
System.out.println("子類靜態代碼塊");
}
{
System.out.println("子類動態代碼塊");
}
static{
System.out.println("子類靜態代碼塊2");
}
public ChildClass() {
System.out.println("子類構造方法");
}
public static void childStaticMethod(){
System.out.println("子類靜態方法");
}
private String propertityB;
public String getPropertityB() {
return propertityB;
}
public void setPropertityB(String propertityB) {
this.propertityB = propertityB;
}
}
測試類:
(測試用的main()方法不能寫在父類或者子類裏面。原因:jvm會強制加載main()方法所在的類,這樣會影響測試效果)
public class TestClass {
static {
System.out.println("測試類靜態代碼塊");
}
{
System.out.println("測試類動態代碼塊");
}
public static void main(String[] args) throws ClassNotFoundException {
這裏的內容下面分情況來寫
}
}
2、引用常量不會引發類加載
public static void main(String[] args) throws ClassNotFoundException {
System.out.println(ChildClass.PARENT_FINAL_P);
}
測試類靜態代碼塊
父類常量
可以看到,引用的父類的常量(final),也不會觸發子類的任何加載行爲(當然父類也沒有被加載)
3、引用父類的成員變量或是靜態方法會執行初始化
public static void main(String[] args) throws ClassNotFoundException {
System.out.println(ChildClass.parentP);
}
測試類靜態代碼塊
父類靜態代碼塊
父類成員變量
子類引用父類的成員變量時,觸發了父類的初始化(“父類靜態代碼塊”被執行),但是子類本身沒有被初始化
public static void main(String[] args) throws ClassNotFoundException {
System.out.println(ChildClass.childP);
}
測試類靜態代碼塊
父類靜態代碼塊
子類靜態代碼塊
子類靜態代碼塊2
子類成員變量
子類引用自身成員變量時,根據“虛擬機會保證首先執行父類的()方法”的理論,父類先被初始化(“父類靜態代碼塊”),然後再初始化子類(“子類靜態代碼塊1、2”)
另外,關於靜態變量、靜態代碼塊初始化的執行順序,代碼中先定義的先執行(變量、動態代碼塊同理)
4、使用new關鍵字實例化對象,會初始化類,並觸發對象的一系列初始化動作
public static void main(String[] args) throws ClassNotFoundException {
ChildClass child = new ChildClass();
}
測試類靜態代碼塊
父類靜態代碼塊
子類靜態代碼塊
子類靜態代碼塊2
父類動態代碼塊
父類構造方法
子類動態代碼塊
子類構造方法
可以看到,與第3節相比,不但執行了類加載(clinit()方法調用靜態代碼),而且先創建了父類對象(父類動態代碼塊、父類構造方法),然後再創建的子類對象(子類動態代碼塊、子類構造方法 )
所以在一些文章中,給出了這樣的對象初始化順序:
靜態變量/靜態代碼塊(先父類後子類) > main方法 > 變量/動態代碼塊(先父類後子類) > 構造器(先父類後子類)
其實只要弄明白類的加載原理(包含一點最基礎的java內存模型知識),這種順序完全不用死記硬背
5、ClassLoader.loadClass
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
classLoader.loadClass("com.demo.ChildClass");
}
測試類靜態代碼塊
loadClass什麼也沒有做……
因爲這個public的loadClass,調用的是ClassLoader的
protected Class<?> loadClass(String name, boolean resolve)
方法,並且resolve固定傳false,意思是隻進行加載和鏈接,不執行初始化過程。
6、Class.forName
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("com.demo.ChildClass");
}
測試類靜態代碼塊
父類靜態代碼塊
子類靜態代碼塊
子類靜態代碼塊2
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Class.forName("com.demo.ChildClass", false, classLoader);
}
測試類靜態代碼塊
Class.forName的情況與ClassLoader.loadClass不同,有兩個重載方法。
只指定一個參數時,調用的是一個native方法
forName0(className, true, ClassLoader.getClassLoader(caller), caller);
其中第二個參數true,指定了要對類進行初始化;
而使用重載的三個參數的方法時,其方法定義是這樣的
public static Class<?> forName(String name, boolean initialize,ClassLoader loader)
第二個參數指定是否初始化,然後調用了同樣的native方法,將boolean變量傳遞了進去,傳false的話就不執行初始化過程了
7、類的加載過程只執行一次
public static void main(String[] args) throws ClassNotFoundException {
System.out.println(ChildClass.CHILD_FINAL_P);
System.out.println(">>><<<");
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
classLoader.loadClass("com.demo.ChildClass");
System.out.println(">>><<<");
Class.forName("com.demo.ChildClass");
System.out.println(">>><<<");
ChildClass child = new ChildClass();
}
測試類靜態代碼塊
子類常量
>>><<<
>>><<<
父類靜態代碼塊
子類靜態代碼塊
子類靜態代碼塊2
>>><<<
父類動態代碼塊
父類構造方法
子類動態代碼塊
子類構造方法
最開始只引用了一下子類成員常量,因爲常量在方法區的常量池裏面,沒有觸發類的任何動作;
第二步 classLoader.loadClass 只做了類的加載和鏈接,不進行初始化,並沒有任何輸出;
第三步 Class.forName ,前面已經完成了加載和鏈接,所以跳過(輸出中無法清晰顯示出這個結論),只做了類的初始化;
第四步進行對象的創建和初始化,由於類的加載全過程已經完成,所以new關鍵字並沒有觸發各種靜態資源的初始化(因爲已經完事了)
簡單總結一下
-
編譯時把常量都放進常量池,引用時不會涉及到類
-
類加載時,除了非靜態成員變量不會被加載,其它的都會被加載(包括非靜態方法——這一結論無法用示例來說明),加載完畢初始化
-
創建實例時,先執行動態方法塊,再執行構造方法
-
先父類後子類