類載入器 - 類的載入、連線與初始化
類的載入、連線與初始化
概述
在Java程式碼中,型別的載入、連線與初始化過程都是在程式執行期間完成的
- 型別:可以理解為一個class
- 載入:查詢並載入類的二進位制資料,最常見的情況是將已經編譯完成的類的class檔案從磁碟載入到記憶體中
- 連線:確定型別與型別之間的關係,對於位元組碼的相關處理
- 驗證:確保被載入的類的正確性
- 準備:為類的靜態變數分配記憶體,並將其初始化為預設值。但是在到達初始化之前,類變數都沒有初始化為真正的初始值
- 解析:在型別的常量池中尋找類、介面、欄位和方法的符號引用,把這些符號引用轉換為直接引用的過程
- 初始化:為類的靜態變數賦予正確的初始值
- 使用:比如建立物件,呼叫類的方法等
- 解除安裝:類從記憶體中銷燬
理解:public static int number = 666;
上面這段程式碼,在類載入的連線階段,為物件number分配記憶體,並初始化為0;然後再初始化階段在賦予正確的初始值:666
類的使用方式
Java程式對類的使用方式可分為兩種
- 主動使用
- 建立類的例項
- 訪問某個類或介面的靜態變數,或者對靜態變數賦值
- 呼叫類的靜態方法
- 反射
- 初始化類的子類
- Java虛擬機器啟動時被標明為啟動類的類(包含main方法)
- JDK1.7開始提供的動態語言支援(java.lang.invoke.MethodHandle例項的解析結果REF_getStatic,REF_putStatic,REF_invokeStatic控制代碼對應的類沒有初始化,則初始化)
- 被動使用
- 除了主動使用的七種情況之外,其他使用Java類的方法都被看作是對類的被動使用,都不會導致類的初始化
所有的Java虛擬機器實現必須在每個類或介面被Java程式“首次主動使用”時才初始化他們
程式碼理解
示例一:類的載入連線和初始化過程
程式碼一
public class Test01 { public static void main(String[] args) { System.out.println(Child01.str); } } class Father01 { public static String str = "做一個好人!"; static { System.out.println("Father01 static block"); } } class Child01 extends Father01 { static { System.out.println("Child01 static block"); } }
執行結果做一個好人!
Father01 static block
做一個好人!
程式碼二
public class Test01 {
public static void main(String[] args) {
System.out.println(Child01.str2);
}
}
class Father01 {
public static String str = "做一個好人!";
static {
System.out.println("Father01 static block");
}
}
class Child01 extends Father01 {
public static String str2 = "做一個好人!";
static {
System.out.println("Child01 static block");
}
}
執行結果
Father01 static block
Child01 static block
做一個好人!
分析:
- 程式碼一中,我們通過子類呼叫父類中的str,這個str是在父類被定義的,對Father01主動使用,沒有主動使用Child01,故Child01的靜態程式碼塊沒有執行,父類的靜態程式碼塊被執行了。 -> 對於靜態欄位來說,只有直接定義了該欄位的類才會被初始化。
- 程式碼二中,對Child01主動使用;根據主動使用的7種情況,調動類的子類時,其所有的父類都會被先初始化,所以Father01會被初始化。 -> 當一個類初始化時,要求其父類全部都已經初始化完畢了。
以上驗證的是類的初始化情況,那麼如何驗證類的載入情況呢,可以通過在啟動的時候配置虛擬機器引數:-XX:+TraceClassLoading
檢視
執行程式碼一,檢視輸出結果,可以看見控制檯列印了very多的日誌,第一個載入的是java.lang.Object
類(不管載入哪個類,他的父類一定是Object類),後面是載入的一系列jdk的類,他們都位於rt包下。往下檢視,可以看見Loaded classloader.Child01
,說明即使沒有初始化Child01,但是程式依然載入了Child01類。
[Opened /usr/local/jdk1.8/jre/lib/rt.jar]
[Loaded java.lang.Object from /usr/local/jdk1.8/jre/lib/rt.jar]
...
[Loaded java.lang.Void from /usr/local/jdk1.8/jre/lib/rt.jar]
[Loaded classloader.Father01 from file:/home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/]
[Loaded classloader.Child01 from file:/home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/]
Father01 static block
做一個好人!
[Loaded java.lang.Shutdown from /usr/local/jdk1.8/jre/lib/rt.jar]
[Loaded java.lang.Shutdown$Lock from /usr/local/jdk1.8/jre/lib/rt.jar]
拓展:JVM引數介紹
因為前一章節使用了JVM引數,所以對其做一下簡單的介紹
- 所有的JVM引數都是以
-XX:
開頭的 - 如果形式是:
-XX:+<option>
,表示開啟option選項 - 如果形式是:
-XX:-<option>
,表示關閉option選項 - 如果形式是:
-XX:<option>=<value>
,表示將option選項的值設定為value
示例二:常量的本質含義
public class Test02 {
public static void main(String[] args) {
System.out.println(Father02.str);
}
}
class Father02{
public static final String str = "做一個好人!";
static {
System.out.println("Father02 static block");
}
}
執行結果
做一個好人!
分析
可以看見,此段程式碼並沒有初始化Father02類。這是因為final表示的是一個常量,在編譯階段常量就會被存入到呼叫這個常量的方法所在的類的常量池當中,本質上,呼叫類並沒有直接引用到定義常量的類,因此並不會觸發定義常量的類的初始化。在本程式碼中,常量str會被存入到Test02的常量池中,之後Test02與Father02沒有任何關係,甚至可以刪除Father02的class檔案。
我們反編譯一下Test02類
Compiled from "Test02.java"
public class classloader.Test02 {
public classloader.Test02();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String 做一個好人!
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
第一塊是Test02類的構造方法,第二塊是我們要看的main方法。可以看見3: ldc #4 // String 做一個好人!
,此時這個值已經是確定無疑的做一個好人!
了,而不是Father02.str
,證實了上面說的在編譯階段常量就會被存入到呼叫這個常量的方法所在的類的常量池當中。
拓展:助記符
因前一章節涉及到了助記符,所以介紹下本章節涉及到的助記符及擴充套件
- ldc:表示將int、float或String型別的常量值常量池中推送至棧頂
- bipush:表示將單位元組(-128 -至 127)的常量推送至棧頂
- sipush:表示將一個短整形(-32768 至 32767)的常量推送至棧頂
- iconst_1:表示將int型別的
1
推送至棧頂(這類助記符只有iconst_m1 - iconst_5七個)
示例三:編譯期常量與執行期常量的區別
public class Test03 {
public static void main(String[] args) {
System.out.println(Father03.str);
}
}
class Father03 {
public static final String str = UUID.randomUUID().toString();
static {
System.out.println("Father03 static block");
}
}
執行結果
Father03 static block
a60c5db4-2673-4ffc-a9f0-2dbe53fae583
分析
本程式碼與示例二的區別在於str
的值是在執行時確認的,而不是編譯時就確定好的,屬於執行期常量,而不是編譯期常量。當一個常量的值並非編譯期間確定的,那麼其值就不會被放到呼叫類的常量池中,這時在程式執行時,會導致主動使用這個常量所在的類,導致這個類被初始化。
示例四:陣列建立本質
程式碼一
public class Test04 {
public static void main(String[] args) {
Father04 father04_1 = new Father04();
System.out.println("-----------");
Father04 father04_2 = new Father04();
}
}
class Father04 {
static {
System.out.println("Father04 static block");
}
}
執行結果
Father04 static block
-----------
分析
- 建立類的例項時,會初始化類
- 所有的Java虛擬機器實現必須在每個類或介面被Java程式“首次主動使用”時才初始化他們
程式碼二
public class Test04 {
public static void main(String[] args) {
Father04[] father04s = new Father04[1];
System.out.println(father04s.getClass());
}
}
執行結果
class [Lclassloader.Father04;
分析
- 建立陣列物件不再主動使用的7種情況內,屬於被動使用,故不會初始化Father04
- 列印father04s的型別為
[Lclassloader.Father04
,這是虛擬機器在執行期生成的。 -> 對於陣列示例來說,其型別是有JVM在執行期動態生成的,表示為[Lclassloader.Father04
這種形式,動態生成的型別,其父類就是Object。 - 對於陣列來說,JavaDoc經常將構成陣列的元素為Component,實際上就是將陣列降低一個維度後的型別
反編譯一下:
public static void main(java.lang.String[]);
Code:
0: iconst_1
1: anewarray #2 // class classloader/Father04
4: astore_1
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: aload_1
9: invokevirtual #4 // Method java/lang/Object.getClass:()Ljava/lang/Class;
12: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
15: return
- anewarray:表示建立一個引用型別(如類、介面、陣列)的陣列,並將其引用值值壓入棧頂
- newarray:表示建立一個指定的原始型別(如int、float、char等)的陣列,並將其引用值壓入棧頂
示例五:介面的載入與初始化
程式碼一
public class Test05 {
public static void main(String[] args) {
System.out.println(Child05.j);
}
}
interface Father05 {
int i = 5;
}
interface Child05 extends Father05 {
int j = 6;
}
執行結果
6
分析
- 介面中定義的常量本身就是public、static、final的
- 結果顯而易見,這時我們刪除掉Father05.class檔案和Child05.class檔案,程式依然可以正常執行
- 介面中的常量本身就是final常量,會被載入到Test05的常量池中
- 此時,Father05和Child05都不會被載入
程式碼二
public class Test05 {
public static void main(String[] args) {
System.out.println(Child05.j);
}
}
interface Father05 {
int i = 5;
}
interface Child05 extends Father05 {
int j = new Random().nextInt(8);
}
執行結果
6
將Father05.class檔案刪除,執行結果
Exception in thread "main" java.lang.NoClassDefFoundError: classloader/Father05
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:338)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at classloader.Test05.main(Test05.java:15)
Caused by: java.lang.ClassNotFoundException: classloader.Father05
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:338)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 13 more
分析
- 只有在真正使用到父介面的時候(如引用介面中所定義的常量時),才會載入初始化
程式碼三
public class Test05 {
public static void main(String[] args) {
System.out.println(Child05.j);
}
}
interface Father05 {
Thread thread = new Thread() {
{
System.out.println("Father05 code block");
}
};
}
class Child05 implements Father05 {
public static int j = 8;
}
執行結果
8
分析
- 在初始化一個類時,並不會先初始化他所實現的介面
程式碼四
public class Test05 {
public static void main(String[] args) {
System.out.println(Father05.thread);
}
}
interface GrandFather {
Thread thread = new Thread() {
{
System.out.println("GrandFather code block");
}
};
}
interface Father05 extends GrandFather{
Thread thread = new Thread() {
{
System.out.println("Father05 code block");
}
};
}
執行結果
Father05 code block
Thread[Thread-0,5,main]
分析
- 在初始化一個介面時,並不會先初始化他的父介面
示例六:類載入器準備階段和初始化階段
程式碼一
public class Test06 {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println("i:" + Singleton.i);
System.out.println("j:" + Singleton.j);
}
}
class Singleton {
public static int i;
public static int j = 0;
private static Singleton singleton = new Singleton();
private Singleton() {
i ++;
j ++;
}
public static Singleton getInstance() {
return singleton;
}
}
執行結果
i:1
j:1
分析
首先Singleton.getInstance();
進入Singleton
的getInstance
方法,getInstance
會返回Singleton
的例項,Singleton
的例項是new Singleton();
出來的,因此呼叫了自定義的私有構造方法。在呼叫構造方法之前,給靜態變數賦值,i
預設賦值為0,j
顯式的賦值為0,經過建構函式之後,值都為1。
程式碼二
public class Test06 {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println("i:" + Singleton.i);
System.out.println("j:" + Singleton.j);
}
}
class Singleton {
public static int i;
private static Singleton singleton = new Singleton();
private Singleton() {
i ++;
j ++;
}
public static int j = 0;
public static Singleton getInstance() {
return singleton;
}
}
執行結果
i:1
j:0
分析
程式主動使用了Singleton類,準備階段對類的靜態變數分配記憶體,賦予預設值,下面給出類在連線及初始化階段常量的值的變化
- i : 0
- singleton:null
- j : 0
- getInstance:初始化
- i:0
- singleton:呼叫建構函式
- i:1
- j:1
- j:0【覆蓋了之前的1】
故返回的值i為1,j為0
深入解析類的載入、連線與初始化
類的載入
- 將類的.class檔案中的二進位制資料讀入到記憶體中,將其放在執行時資料區的方法區內,然後再記憶體中建立一個java.lang.Class物件(規範並未說明Class物件位於哪裡,HotSpot虛擬機器將其放在了方法區中)用來封裝類在方法區的資料結構
- 載入.class檔案的方式
- 從本地系統直接載入
- 通過網路下載.class檔案
- 從zip等歸檔檔案中載入.class檔案
- 從專有資料庫中提取.class檔案
- 將Java原始碼動態編譯為.class檔案
- 類的載入的最終產品是位於記憶體中的Class物件
- Calss物件封裝了類在方法區內的資料結構,並且向Java程式設計師提供了訪問方法區內的資料結構的介面
- 有兩種型別的類載入器
- Java虛擬機器自帶的類載入器
- 根類載入器(Bootstrap):該載入器沒有父載入器。他負責載入虛擬機器的核心類庫,如java.lang.*等。根類載入器從系統屬性sun.boot.class.path所指定的目錄中載入類庫。根類載入器的實現依賴於底層作業系統,呀沒有繼承java.lang.CalssLoader類
- 擴充套件類載入器(Extension):父載入器為根載入器。從java.ext.dirs系統屬性所指定的目錄中載入類庫,或者從JDK的安裝目錄的jre\lib\ext子目錄(擴充套件目錄)下載入類庫,如果使用者建立的JAR檔案放在這個目錄下,也會自動由擴充套件類載入器載入。擴充套件類載入器是純Java類,是java.lang.ClassLoader類的子類
- 系統(應用)類載入器(System):父載入器為擴充套件載入器。從環境變數classpath或者系統屬性java.class.path所指定的目錄中載入類,是使用者自定義類載入器的預設父載入器。系統類載入器是純Java類,是java.lang.ClassLoader類的子類
- 使用者自定義的類載入器
- java.lang.ClassLoader的子類
- 使用者可以定製類的載入方式
- Java虛擬機器自帶的類載入器
- 類載入器並不需要等到某個類被“首次使用”時再載入他
- JVM規範允許類載入器在預料某個類將要被使用時就預先載入他,如果在預先載入的過程中遇到了.class檔案缺失或存在錯誤,類載入器必須在程式首次主動使用時才報告錯誤(LinkageError錯誤)。如果這個類一直沒有被程式主動使用,那麼類載入器就不會報告錯誤
類的連線
類被載入後,就進入連線階段。連線就是將已經讀入到記憶體中的類的二進位制資料合併到虛擬機器的執行時環境中去
類的驗證
類的驗證的內容
- 類檔案的結構檢查
- 語義檢查
- 位元組碼驗證
- 二進位制相容性驗證
類的準備
在準備階段,Java虛擬機器為類的靜態變數分配記憶體,並設定預設的初始值。例如對於下面的Sample類,在準備階段,將為int型別的靜態變數
i
分配4個位元組的記憶體空間,並且賦預設值0;為long型別的靜態變數j分配8個位元組的記憶體空間,並賦予預設值0
public class Sample {
private static int i = 8;
private static long j = 8L;
......
}
類的初始化
在初始化階段,Java虛擬機器執行類的初始化語句,為類的靜態變數賦予初始值。在程式中,靜態變數的初始化有兩種途徑:
- 在靜態變數的宣告處初始化
- 在靜態程式碼塊中初始化
靜態變數的宣告語句,預計靜態程式碼塊都被看作類的初始化語句,Java虛擬機器會按照初始化語句在類檔案中的先後順序來依次執行他們
類的初始化步驟
- 假如這個類還沒有被載入和連線誒,需要先進行載入和連線
- 假如類存在直接父類,並且這個父類還沒有被初始化,需要先初始化直接父類
- 假如類中存在初始化語句,需要依次執行這些初始化語句
類的初始化時機
當Java虛擬機器初始化一個類時,要求他的所有父類都已經被初始化,但是這條規則並不適用於介面
- 在初始化一個類時,並不會先初始化他所實現的介面
- 在初始化一個介面時,並不會先初始化他的父介面
因此,一個父介面並不會因為他的子介面或實現類的初始化而初始化,只有當程式首次使用特定介面的靜態變數時,才會導致該介面的初始化。程式碼參照程式碼理解-介面的初始化
- 只有當程式訪問的靜態變數或者靜態方法確實在當前類或者當前介面中定義時,才認為是對類或介面的主動使用
呼叫ClassLoader類的loadClass方法載入一個類,並不是對類的主動使用,不會導致類的初始化
拓展:類例項化
類的生命週期除了前文提到的載入、連線、初始化之外,還有類示例化,垃圾回收和物件終結
- 為新的物件分配記憶體
- 為例項變數賦予預設值
- 為例項變數賦予正確的初始值
- Java編譯器為它編譯的每一個類都至少生成一個例項初始化方法,在Java的class檔案中,這個例項初始化方法被稱為
<init>
。針對原始碼中每一個類的構造方法,Java編譯器都產生一個<init>
方法