NO IMAGE

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篇文章對你有幫助,請動動你的小指頭點個贊吧。

連結:https://www.jianshu.com/p/8c8d6cba1f8e