玩轉Java虛擬機(一)

從今天開始打卡學習JVM,第一天

本人學習過程中所整理的代碼,源碼地址

- 類加載

在Java代碼中,類型的加載、連接與初始化過程都是在程序運行期間完成的

  • 加載:查找並加載類的二進制數據,具體指將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,然後在內存中創建一個Class對象用來封裝類在方法區內的數據結構
  • 連接:驗證 -> 類被加載後,就進入連接階段,就是將已經讀入到內存的類的二進制數據合併到虛擬機的運行時環境中去,確保被加載的類的正確性;準備 -> 爲類的靜態變量分配內存,並將其初始化爲默認值;解析 -> 把類中的符號引用轉換爲直接引用
  • 初始化:爲類的靜態變量賦予正確的初始值;當Java虛擬機初始化一個類時,要求它的所有父類都已經被初始化,接口不適用,只有當程序首次使用特點接口的靜態變量時,纔會導致該接口的初始化
  • 類實例化:爲新的對象分配內存,爲實例變量賦默認值,爲實例變量賦正確的初始值,java編譯器爲它的每一個類都至少生成一個實例初始化方法,被稱爲<init>,針對源代碼中每一個類的額構造方法,java編譯器都產生一個<init>方法
  • 垃圾回收和對象終結

-XX:+TraceClassLoading:用於追蹤類的加載信息並在控制檯打印出來
-XX:+(option):表示開啓option選擇
-XX:-(option):表示關閉option選擇
-XX:(option)=(value):表示將option選項的值設置爲value

//對於靜態字段來說,只有直接定義了該字段的類纔會被初始化;
//當一個類在初始化時,會先初始化其父類
public class MyTest {
    public static void main(String[] args) {
        System.out.println(MyChild1.str2);
    }
}
class MyParent1 {
    public static String str = "hello world";
    static {
        System.out.println("MyParent1 static block");
    }
}
class MyChild1 extends MyParent1 {
    public static String str2 = "welcome";
    static {
        System.out.println("MyChild1 static block");
    }
}
/*MyParent1 static block
MyChild1 static block
welcome*/

助記符
ldc:表示將int,float或者String類型的常量值從常量池中推送至棧頂
bipush:表示將單字節[-128-127]的常量值推送至棧頂
sipush:表示將一個短整型常量值[-32768-32767]推送至棧頂
iconst_n:表示將int類型n[-1 5]推送至棧頂

//常量在編譯階段會存入到調用這個常量的方法所在類的常量池中,本質上,調用類並沒有直接引用到定義變量的類,因此並不會觸發定義常量的類的初始化
public class MyTest2 {
    public static void main(String[] args) {
        System.out.println(MyParent2.str);
    }
}
class MyParent2{
	//被保存到了MyTest2的常量池中,之後MyTets2與MyParent2就沒有任何關係了,甚至可以將MyParent2的class文件刪除
    public static final String str = "hello world";
    static {
        System.out.println("MyParent2 static block");
    }
}
//hello world
//當一個常量的值在編譯期間無法確定,那麼其值就無法被放到調用類的常量池中,此時程序運行時,會導致主動使用這個常量所在的類,因此這個類將會被初始化
public class MyTest3 {
    public static void main(String[] args) {
        System.out.println(MyParent3.str);
    }
}
class MyParent3{
    public static final String str = UUID.randomUUID().toString();
    static {
        System.out.println("MyParent3 static code");
    }
}
/*MyParent3 static code
df50b36d-15a8-4b9a-8aa5-18de0f3a0a92*/

助記符
anewarray:表示創建一個引用類型的(如類、接口、數組)
數組,並將其引用值壓入棧頂
newarray:表示創建一個基本類型的數組,並將其引用值壓入棧頂

//對於數組實例來說,其類型是由JVM在運行期動態生成的,表示爲class [Lcom.cqupt.jvm.classloader.MyParent4這種形式。動態生成的類型其父類型也是Object。對於數組來說,JavaDoc經常將構成數組的元素爲Component,實際上就是將數組降第一個維度後的類型
public class MyTest4 {
    public static void main(String[] args) {
        //會初始化類
        MyParent4 parent4 = new MyParent4();
        //不會初始化
        MyParent4[] myParent4 = new MyParent4[1];
        MyParent4[][] myParent44 = new MyParent4[1][1];
        System.out.println(myParent4.getClass());
        System.out.println(myParent44.getClass());
        System.out.println(myParent4.getClass().getSuperclass());
        System.out.println(myParent44.getClass().getSuperclass());
    }
}
class MyParent4 {
    static {
        System.out.println("MyParent4 static block");
    }
}

這裏樓主試了好多次,刪除父接口的class文件還是不會報錯,父接口難道沒有被加載嗎?

//當一個接口在初始化時,並不要求其父接口都完成初始化,只有在真正使用到父接口的時候(如引用接口中所定義的常量時)纔會初始化
public class MyTest5 {
    public static void main(String[] args) {
        System.out.println(MyChild5.b);
    }
}
interface MyParent5 {
    public static int a = 5;
}
interface MyChild5 extends MyParent5 {
    public static final int b = new Random().nextInt(3);
}
//6

下面這段代碼中爲什麼counter2會輸出0?
因爲在類加載過程中的準備階段時所有靜態變量都被賦予了默認值,所以在構造函數運行完後counter2等於1,但由於初始化是按照代碼的順序執行的,因此counter2的值又會被顯示式賦予的0所覆蓋掉,因此最終counter2的值爲0;若將counter1一開始就顯示賦爲1,最終counter1的輸出結果爲2,進一步驗證了初始化的順序問題

public class MyTest6 {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        System.out.println("counter1:" + Singleton.counter1);
        System.out.println("counter2:" + Singleton.counter2);
    }
}
class Singleton {
    public static int counter1;
    private static Singleton singleton = new Singleton();
    private Singleton() {
        //準備階段的重要意義
        counter1++;
        counter2++;
        
        System.out.println(counter1);
        System.out.println(counter2);
    }
    public static int counter2 = 0;
    public static Singleton getInstance() {
        return singleton;
    }
}
//1 1 counter1:1 counter2:0

- Java虛擬機結束生命週期的方式

  • 執行了System.exit()方法
  • 程序正常執行結束
  • 程序在執行過程中遇到了異常或錯誤而異常終止
  • 由於操作系統出現錯誤而導致Java虛擬機進程中止

總結:

通過以上實例主要是爲了驗證類加載過程中的各種問題,理解子類父類的初始化問題,理解類加載過程中的準備和初始化階段的重要意義。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章