1 前言
在深入理解Java類加載機制(一)一文中,我們了解了類的加載和連接過程,這篇文章重點講述類的初始化過程,這樣,我們就將類的加載機制弄明白了。
2 初始化時機
在上一篇 類的加載時機5.2中我們提到了“首次主動使用”這個詞語,那什么是“主動使用”呢?
主動初始化的6種方式
(1)創建對象的實例:我們new對象的時候,會引發類的初始化,前提是這個類沒有被初始化。
(2)調用類的靜態屬性或者為靜態屬性賦值
(3)調用類的靜態方法
(4)通過class文件反射創建對象
(5)初始化一個類的子類:使用子類的時候先初始化父類
(6)java虛擬機啟動時被標記為啟動類的類:就是我們的main方法所在的類
只有上面6種情況才是主動使用,也只有上面六種情況的發生才會引發類的初始化。
同時我們需要注意下面幾個Tips:
1)在同一個類加載器下面只能初始化類一次,如果已經初始化了就不必要初始化了.
這里多說一點,為什么只初始化一次呢?因為我們上面講到過類加載的最終結果就是在堆中存有唯一一個Class對象,我們通過Class對象找到
類的相關信息。唯一一個Class對象說明了類只需要初始化一次即可,如果再次初始化就會出現多個Class對象,這樣和唯一相違背了。
2)在編譯的時候能確定下來的靜態變量(編譯常量),不會對類進行初始化;
3)在編譯時無法確定下來的靜態變量(運行時常量),會對類進行初始化;
4)如果這個類沒有被加載和連接的話,那就需要進行加載和連接
5)如果這個類有父類并且這個父類沒有被初始化,則先初始化父類.
6)如果類中存在初始化語句,依次執行初始化語句.
public class Test1 {
public static void main(String args[]){
System.out.println(FinalTest.x);
}
}
class FinalTest{
public static final int x =6/3;
static {
System.out.println("FinalTest 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);
static {
System.out.println("FinalTest2 static block");
}
}
第一個輸出的是
2
第二個輸出的是
FinalTest2 static block
61(隨機數)
為何會出現這樣的結果呢?
參考上面的Tips2和Tips3,第一個能夠在編譯時期確定的,叫做編譯常量;第二個是運行時才能確定下來的,叫做運行時常量。編譯常量不會引起類的初始化,而運行常量就會。
那么將第一個例子的final去掉之后呢?輸出又是什么呢?
這就是對類的首次主動使用,引用類的靜態變量,輸出的當然是:
FinalTest static block
2
那么在第一個例子的輸出語句下面添加
FinalTest.x =3;
又會輸出什么呢?
大家不妨試試!提示(Tips1)
3 類的初始化步驟
講到這里我們應該對類的加載-連接-初始化有一個全局概念了,那么接下來我們看看類具體初始化執行步驟。我們分兩種情況討論,一種是類有父類,一種是類沒有父類。(當然所有類的頂級父類都是Object)
沒有父類的情況:
1)類的靜態屬性
2)類的靜態代碼塊
3)類的非靜態屬性
4)類的非靜態代碼塊
5)構造方法
有父類的情況:
1)父類的靜態屬性
2)父類的靜態代碼塊
3)子類的靜態屬性
4)子類的靜態代碼塊
5)父類的非靜態屬性
6)父類的非靜態代碼塊
7)父類構造方法
8)子類非靜態屬性
9)子類非靜態代碼塊
10)子類構造方法
在這要說明下,靜態代碼塊和靜態屬性是等價的,他們是按照代碼順序執行的。
類的初始化內容這樣看起來還是挺多的,包括“主動使用”大家可以自己去寫一些demo去驗證一下。
4 結束JVM進程的幾種方式
了解完類加載機制之后,接下來我們了解一下結束JVM進程的幾種方式吧。
(1) 執行System.exit()
(2) 程序正常結束
(3) 程序拋出異常,一直向上拋出沒處理
(4) 操作系統異常,導致JVM退出
JVM有上面4種結束的方式,我們一一了解下:
(1)我們先來看看第一種方式,找到源代碼我們發現:
/**
* Terminates the currently running Java Virtual Machine. The
* argument serves as a status code; by convention, a nonzero status
* code indicates abnormal termination.
*/
public static void exit(int status) {
Runtime.getRuntime().exit(status);
}
上面的代碼解釋了System.exit()方法的作用就是:是中斷當前運行的java虛擬機。這是自殺方式。
(2)第二種程序正常結束的方式,我們在運行main方法的時候,運行狀態按鈕由綠色變紅色再變綠色的過程就是程序啟動-運行-結束的過程。 那么,我們來看看Android的程序,同樣,安卓也有自己的啟動方式,也是一個main方法。那么我們的android程序能夠一直運行的前提就是我們的main方法一直被執行著,一旦main方法執行完畢,程序就是kill。我們找找源代碼才能有更好的說服力;我們找到ActivityThread的main方法
public static void main(String[] args) {
SamplingProfilerIntegration.start();
// CloseGuard defaults to true and can be quite spammy. We
// disable it here, but selectively enable it later (via
// StrictMode) on debug builds, but using DropBox, not logs.
CloseGuard.setEnabled(false);
Environment.initForCurrentUser();
// Set the reporter for event logging in libcore
EventLogger.setReporter(new EventLoggingReporter());
Security.addProvider(new AndroidKeyStoreProvider());
Process.setArgV0("<pre-initialized>");
Looper.prepareMainLooper();
ActivityThread thread = new ActivityThread();
thread.attach(false);
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
AsyncTask.init();
if (false) {
Looper.myLooper().setMessageLogging(new
LogPrinter(Log.DEBUG, "ActivityThread"));
}
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
上面的代碼都不用看,直接看最后兩行代碼。執行完Looper.loop()之后,直接拋出了異常。但是我們并沒有見到這個異常,說明我們的Looper一直在執行這樣保證我們的app不被kill掉。Android就是用這種方式來保證我們的app一直運行下去的。
(3)第三種方式不用過多解釋,一直沒有處理被拋出的異常,這樣導致了程序崩潰。
(4)第四種方式是系統異常導致了jvm退出。其實jvm就是一個軟件,如果我們的操作系統都出現了錯誤,那么運行在他上面的軟件(jvm)必然會被kill。
5 結束并回顧
到這里,我們基本都清楚了類的加載機制。那么我們在第一篇文章中開頭提到一個例子,我們這里來講講輸出的是什么,并且為何如此輸出,大家坐穩。
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;
}
}
下面是我們的測試類TestSingleton
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 ;
接下來再看第二個靜態屬性,我們并沒有對它進行初始化,所以它就沒辦法進行初始化工作了;
第三個屬性counter2我們初始化為0 ,而在初始化之前counter2=1,執行完counter2=0之后counter2=0了;
3 初始化完畢之后我們就要調用靜態方法Singleton.getSingleton(); 我們知道返回的singleton已經初始化了。
那么輸出的內容也就理所當然的是1和0了。這樣一步一步去理解程序執行過程是不是讓你清晰的認識了java虛擬機執行程序的邏輯呢。
那么我們接下來改變一下代碼順序,將
public static int counter1;
public static int counter2 = 0;
private static Singleton singleton = new Singleton();
又會輸出什么呢?為什么這樣輸出呢?
這個問題留給大家去思考,主要還是理解為什么這樣輸出才是最重要的。
結合第一篇文章深入理解Java類加載機制(一),我們講完了類的加載機制。大家是否對java又有了不一樣的認識了呢?如果這2篇文章對你有幫助,請動動你的小指頭點個贊吧。