深入理解jvm的類載入機制
阿新 • • 發佈:2019-02-20
一、類的生命週期
java程式使用某個類時,必須按照以下順序執行:(1)載入:查詢並載入類的二進位制資料;
(2)連線:包括驗證、準備和解析類的二進位制;
- 驗證:確保載入類的正確性;
- 準備:為類的靜態變數分配記憶體,並將其初始化為預設值;
- 解析:把類中的符號引用轉為直接引用
二、類的載入
1、什麼是類的載入?
類的載入指的是將類的.class檔案中的二進位制資料讀入記憶體中,將其放在執行時資料區域的方法區內,然後在堆中建立java.lang.Class物件,用來封裝類在方法區的資料結構.只有java虛擬機器才會建立class物件。2、什麼時候對類進行載入呢?
Java虛擬機器有預載入功能。類載入器並不需要等到某個類被”首次主動使用”時再載入它,JVM規範規定JVM可以預測載入某一個類,如果這個類出錯,但是應用程式沒有呼叫這個類, JVM也不會報錯;如果呼叫這個類的話,JVM才會報錯,(LinkAgeError錯誤)。其實就是一句話,Java虛擬機器有預載入功能。3、類載入器
類的載入是由類載入器完成,可分為兩種: 1、java虛擬機器自帶的載入器,啟動類載入器、擴充套件類載入器和應用載入器。 2、使用者自定義的類載入器,需要繼承java.lang.ClassLoader類。 啟動類載入器(Bootstrap ClassLoader):這個類載入器負責將<JAVA_HOME>\lib目錄下的類庫載入到虛擬機器記憶體中,用來載入java的核心庫,此類載入器並不繼承於java.lang.ClassLoader,不能被java程式直接呼叫,程式碼是使用C++編寫的.是虛擬機器自身的一部分.
擴充套件類載入器(Extendsion ClassLoader):
這個類載入器負責載入<JAVA_HOME>\lib\ext目錄下的類庫,用來載入java的擴充套件庫,開發者可以直接使用這個類載入器.
應用程式類載入器(Application ClassLoader):
這個類載入器負責載入使用者類路徑(CLASSPATH)下的類庫,一般我們編寫的java類都是由這個類載入器載入,這個類載入器是CLassLoader中的getSystemClassLoader()方法的返回值,所以也稱為系統類載入器.一般情況下這就是系統預設的類載入器.
4、類載入的雙親委派機制
類的載入過程採用雙親委派機制,更好的保證了Java平臺的安全(此機制下,使用者自定義的類載入器不可能載入應該由父類載入器載入的可靠類,從而防止不可靠甚至惡意的程式碼代替由父類載入器載入載入的可靠程式碼)。除了java虛擬機器自帶的啟動類載入器以外 ,其餘的類載入器都有且只有一個父載入器。
比如A類的載入器是AppClassLoader(其實我們自己寫的類的載入器都是AppClassLoader),AppClassLoader不會自己去載入類,而會委ExtClassLoader進行載入,那麼到了ExtClassLoader類載入器的時候,它也不會自己去載入,而是委託BootStrap類載入器進行載入,就這樣一層一層往上委託,如果Bootstrap類載入器無法進行載入的話,再一層層往下走,如果載入成功,則層層返回,到底層都不能載入,則丟擲ClassNotFoundException異常。
三、類的連線
1、類的驗證
驗證階段主要做了以下工作-將已經讀入到記憶體類的二進位制資料合併到虛擬機器執行時環境中去。
-類檔案結構檢查:格式符合jvm規範 -語義檢查:符合java語言規範,final類沒有子類,final型別方法沒有被覆蓋
-位元組碼驗證:確保位元組碼可以安全的被java虛擬機器執行.
-二進位制相容性檢查:確保互相引用的類的一致性.如A類的a方法會呼叫B類的b方法.那麼java虛擬機器在驗證A類的時候會檢查B類的b方法是否存在並檢查版本相容性.因為有可能A類是由jdk1.7編譯的,而B類是由1.8編譯的。那根據向下相容的性質,A類引用B類可能會出錯,注意是可能。
2、類的準備
java虛擬機器為類的靜態變數分配記憶體並賦予預設的初始值.如int分配4個位元組並賦值為0,long分配8位元組並賦值為0;[java] view plain copy print?
- publicclass Sample {
- publicstaticint a;
- publicstaticlong b;
- static{
- b=2;
- }
- }
public class Sample {
public static int a;
public static long b;
static{
b=2;
}
}
3、類的解析
解析階段主要是將符號引用轉化為直接引用的過程。比如 A類中的a方法引用了B類中的b方法,那麼它會找到B類的b方法的記憶體地址,將符號引用替換為直接引用(記憶體地址)。四、類的初始化
1、類的初始化時機:
java虛擬機器在每個類或介面被 “首次主動使用”時,才初始化他們。那什麼是“主動使用”呢? 主動初始化的6種方式(1)建立物件的例項:我們new物件的時候,會引發類的初始化,前提是這個類沒有被初始化。
(2)呼叫類的靜態屬性或者為靜態屬性賦值
(3)呼叫類的靜態方法
(4)通過class檔案反射建立物件
(5)初始化一個類的子類:使用子類的時候先初始化父類
(6)java虛擬機器啟動時被標記為啟動類的類:就是我們的main方法所在的類
只有上面6種情況才是主動使用,也只有上面六種情況的發生才會引發類的初始化。
同時我們需要注意下面幾個Tips:
1)在同一個類載入器下面只能初始化類一次,如果已經初始化了就不必要初始化了.
這裡多說一點,為什麼只初始化一次呢?因為我們上面講到過類載入的最終結果就是在堆中存有唯一一個Class物件,我們通過Class物件找到類的相關資訊。唯一一個Class物件說明了類只需要初始化一次即可,如果再次初始化就會出現多個Class物件,這樣和唯一相違背了。
2)在編譯的時候能確定下來的靜態變數(編譯常量),不會對類進行初始化;
3)在編譯時無法確定下來的靜態變數(執行時常量),會對類進行初始化;
4)如果這個類沒有被載入和連線的話,那就需要進行載入和連線
5)如果這個類有父類並且這個父類沒有被初始化,則先初始化父類.
6)如果類中存在初始化語句,依次執行初始化語句.
[java] view plain copy print?
- publicclass Test1 {
- publicstaticvoid main(String args[]){
- System.out.println(FinalTest.x);
- }
- }
- class FinalTest{
- publicstaticfinalint x =6/3;//final關鍵字修飾的是常量
- static {
- System.out.println(”FinalTest static block”);
- }
- }
public class Test1 {
public static void main(String args[]){
System.out.println(FinalTest.x);
}
}
class FinalTest{
public static final int x =6/3;//final關鍵字修飾的是常量
static {
System.out.println("FinalTest static block");
}
}
[java] view plain copy print?- publicclass Test2 {
- publicstaticvoid main(String args[]){
- System.out.println(FinalTest2.x);
- }
- }
- class FinalTest2{
- publicstaticfinalint x =new Random().nextInt(100);//final關鍵字修飾的是常量
- static {
- System.out.println(”FinalTest2 static block”);
- }
- }
public class Test2 {
public static void main(String args[]){
System.out.println(FinalTest2.x);
}
}
class FinalTest2{
public static final int x =new Random().nextInt(100);//final關鍵字修飾的是常量
static {
System.out.println("FinalTest2 static block");
}
}
第一個輸出的是2
第二個輸出的是
FinalTest2 static block
61(隨機數)
為何會出現這樣的結果呢?
參考上面的Tips2和Tips3,第一個能夠在編譯時期確定的,叫做編譯常量;第二個是執行時才能確定下來的,叫做執行時常量。編譯常量不會引起類的初始化,而執行常量就會進行類的初始化,所有會輸出靜態程式碼塊。
那麼將第一個例子的final去掉之後呢?輸出又是什麼呢?
這就是對類的首次主動使用,引用類的靜態變數,輸出的當然是:
FinalTest static block
2
2、類的初始化順序
五、結束JVM程序的幾種方式
(1) 執行System.exit()(2) 程式正常結束
(3) 程式丟擲異常,一直向上丟擲沒處理
(4) 作業系統異常,導致JVM退出
六、練習回顧
[java] view plain copy print?- publicclass Singleton {
- privatestatic Singleton singleton = new Singleton();
- publicstaticint counter1;
- publicstaticint counter2 = 0;
- private Singleton() {
- counter1++;
- counter2++;
- }
- publicstatic Singleton getSingleton() {
- return singleton;
- }
- }
public class Singleton {
private static Singleton singleton = new Singleton();
public static int counter1;
public static int counter2 = 0;
private Singleton() {
counter1++;
counter2++;
}
public static Singleton getSingleton() {
return singleton;
}
}
[java] view plain copy print?- publicclass TestSingleton {
- publicstaticvoid main(String args[]){
- Singleton singleton = Singleton.getSingleton();
- System.out.println(”counter1=”+singleton.counter1);
- System.out.println(”counter2=”+singleton.counter2);
- }
- }
public class TestSingleton {
public static void main(String args[]){
Singleton singleton = Singleton.getSingleton();
System.out.println("counter1="+singleton.counter1);
System.out.println("counter2="+singleton.counter2);
}
}
輸出是:counter1=1
counter2=0
why?我們一步一步分析:
1 執行TestSingleton第一句的時候,因為我們沒有對Singleton類進行載入和連線,所以我們首先需要對它進行載入和連線操作。在連線階-準備階段,我們要講給靜態變數賦予預設初始值。
singleton =null
counter1 =0
counter2 =0
2 載入和連線完畢之後,我們再進行初始化工作。初始化工作是從上往下依次執行的,注意這個時候還沒有呼叫Singleton.getSingleton();
首先 singleton = new Singleton();這樣會執行構造方法內部邏輯,進行++;此時counter1=1,counter2 =1 ;
接下來再看第二個靜態屬性counter1,我們並沒有對它進行初始化,所以它就沒辦法進行初始化工作了;
第三個屬性counter2我們初始化為0,因此此時的counter2 =0 ;
3 初始化完畢之後我們就要呼叫靜態方法Singleton.getSingleton(); 我們知道返回的singleton已經初始化了。
那麼輸出的內容也就理所當然的是1和0了。
那麼我們接下來改變一下程式碼順序,將
[java] view plain copy print?
- publicstaticint counter1;
- publicstaticint counter2 = 0;
- privatestatic Singleton singleton = new Singleton();
public static int counter1;
public static int counter2 = 0;
private static Singleton singleton = new Singleton();
輸出結果是
counter1=1counter2=1
你會分析了嗎?
參考: http://www.jianshu.com/p/b6547abd0706
http://www.jianshu.com/p/8c8d6cba1f8e