從 static 關(guān)鍵字深入理解 java對(duì)象初始化順序
前言
最近在閱讀 ThreadLocal 源碼的時(shí)候,發(fā)現(xiàn)一段很有意思的代碼,代碼片段如下:
private?final?int?threadLocalHashCode?=?nextHashCode();?
private?static?AtomicInteger?nextHashCode?=?new?AtomicInteger();
private?static?final?int?HASH_INCREMENT?=?0x61c88647;
private?static?int?nextHashCode()?{
????return?nextHashCode.getAndAdd(HASH_INCREMENT);
}
以上代碼片段主要是 ThradLocal 生成哈希值(threadLocalHashCode)的邏輯,通過靜態(tài)的原子整型變量 nextHashCode 以及靜態(tài)方法 nextHashCode (),為每個(gè)線程持有的 ThreadLocal 本地變量生成唯一 的 hashCode。
注:ThreadLocal 的 hashCode 選擇 HASH_INCREMENT 變量值:0x61c88647 很有意思,里面涉及到斐波那契數(shù)列和黃金分割法,感興趣的同學(xué)可以自行了解下。
當(dāng)然本文的重點(diǎn)不是 ThreadLocal 原理分析上,而是分析 static 關(guān)鍵字修飾的靜態(tài)域(靜態(tài)變量、靜態(tài)塊)順序加載問題。
這段代碼總共四行,除了第一行都是用 static 關(guān)鍵字修飾的,這里我們?cè)O(shè)想一個(gè)問題,當(dāng)類初始化的時(shí)候,這四行代碼是從上往下執(zhí)行的嗎?
答案是:”否“。
靜態(tài)變量
為了方便 debug 調(diào)試,我們把上面的代碼稍微做了下調(diào)整,代碼片段如下:
public?class?Static01?{
????private?final?int?threadLocalHashCode?=?nextHashCode();
????private?static?AtomicInteger?nextHashCode?=?new?AtomicInteger();
????private?static?final?int?HASH_INCREMENT?=?getIncr();
????public?Static01(){
????????System.out.println("threadLocalHashCode::"?+?threadLocalHashCode);
????}
????private?static?int?getIncr()?{
????????return?0x61c88647;
????}
????private?static?int?nextHashCode()?{
????????return?nextHashCode.getAndAdd(HASH_INCREMENT);
????}
????public?static?void?main(String[]?args)?{
????????new?Static01();
????}
}
上面的代碼片段用 debug 模式啟動(dòng),通過為每行代碼打斷點(diǎn),發(fā)現(xiàn)當(dāng)真正實(shí)例化 Static01 類時(shí),代碼運(yùn)行順序并非是按照逐行執(zhí)行,而是如下圖紅色標(biāo)記順序進(jìn)行的。

其執(zhí)行流程是:
第一步、用 new關(guān)鍵字初始化Static01類的構(gòu)造方法第二步、初始化靜態(tài)變量 nextHashCode第三步、初始化靜態(tài)變量 HASH_INCREMENT第四步、初始化成員變量 threadLocalHashCode最后 、在 Static01構(gòu)造方法打印threadLocalHashCode變量的hash值
對(duì)象實(shí)例化
就是執(zhí)行類中構(gòu)造函數(shù)的內(nèi)容,如果該類存在父類,會(huì)通過顯示或者隱示的方式(
super方法)先執(zhí)行父類的構(gòu)造函數(shù),在堆內(nèi)存中為父類的實(shí)例變量開辟空間,并賦予默認(rèn)的初始值,然后在根據(jù)構(gòu)造函數(shù)的代碼內(nèi)容將真正的值賦予實(shí)例變量本身,然后,引用變量獲取對(duì)象的首地址,通過操作對(duì)象來調(diào)用實(shí)例變量和方法
從上面代碼執(zhí)行流程可以看出
在對(duì)象實(shí)例化之前必須先初始化 static修飾的靜態(tài)變量,并且靜態(tài)變量也是有加載順序的;類的成員變量的初始化在構(gòu)造方法里面進(jìn)行,加載順序優(yōu)先于構(gòu)造方法體的執(zhí)行語句。 如果某類繼承了父類,那么必須先初始化父類的構(gòu)造方法以及成員變量以及構(gòu)造方法的執(zhí)行語句,然后才是子類的成員變量以及構(gòu)造方法的執(zhí)行語句。
public?Static01()?{
????super();
????System.out.println("threadLocalHashCode::"?+?threadLocalHashCode);
}
另外,靜態(tài)語句塊中只能訪問到定義在靜態(tài)塊之前的變量,在靜態(tài)塊里可以給該變量賦值,但是不能訪問,否則編譯器會(huì)提示 “Illegal forward reference” 錯(cuò)誤,如下圖

靜態(tài)塊
靜態(tài)塊主要用于類的初始化,不是指對(duì)象的實(shí)例化。它只會(huì)執(zhí)行一次,靜態(tài)塊只能訪問類的靜態(tài)成員屬性和方法,不能在靜態(tài)塊使用 this。
我們先把上面的代碼稍加改造下,增加 “靜態(tài)塊1”和“靜態(tài)塊2” 靜態(tài)塊代碼
private?final?int?threadLocalHashCode?=?nextHashCode();
private?static?AtomicInteger?nextHashCode?=?new?AtomicInteger();
static{
????System.out.println("靜態(tài)塊1");
}
private?static?final?int?HASH_INCREMENT?=?getIncr();
static{
????System.out.println("靜態(tài)塊2");
}
運(yùn)行結(jié)果如下:


發(fā)現(xiàn)不管是靜態(tài)塊還是靜態(tài)變量,它們之間都是按順序執(zhí)行的。那為什么是靜態(tài)塊、靜態(tài)變量的初始化是有順序的呢?
通過查看 Static01 類的 class 編譯文件,發(fā)現(xiàn)編譯器會(huì)把 static 塊的代碼放在同一 static 花括號(hào){}內(nèi)。

代碼順序是按照之前編碼的順序整合,這么看來是編譯器在作怪吧。
static?{
????System.out.println("靜態(tài)塊1");
????HASH_INCREMENT?=?getIncr();
????System.out.println("靜態(tài)塊2");
}
類加載中,靜態(tài)域的加載時(shí)機(jī)
從《深入理解Java虛擬機(jī):JVM高級(jí)特性與最佳實(shí)踐(第2版)》這本書講的類加載機(jī)制原理可知:
當(dāng)遇到
new、getstatic和putstatic或invokestatic這4條字節(jié)碼指令時(shí),如果類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。
這就解釋了為什么在對(duì)象未實(shí)例化前,可以通過 “類名.靜態(tài)屬性變量、類名.靜態(tài)方法” 的方式訪問靜態(tài)變量和靜態(tài)方法了。
類加載的時(shí)機(jī)
對(duì)于初始化階段,虛擬機(jī)規(guī)范規(guī)定了有且只有 5 種情況必須立即對(duì)類進(jìn)行“初始化”(而加載、驗(yàn)證、準(zhǔn)備自然需要在此之前開始):
遇到new、getstatic 和 putstatic 或 invokestatic 這4條字節(jié)碼指令時(shí),如果類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。對(duì)應(yīng)場(chǎng)景是:使用 new 實(shí)例化對(duì)象、讀取或設(shè)置一個(gè)類的靜態(tài)字段(被 final 修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)、以及調(diào)用一個(gè)類的靜態(tài)方法。 對(duì)類進(jìn)行反射調(diào)用的時(shí)候,如果類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。 當(dāng)初始化類的父類還沒有進(jìn)行過初始化,則需要先觸發(fā)其父類的初始化。(而一個(gè)接口在初始化時(shí),并不要求其父接口全部都完成了初始化) 虛擬機(jī)啟動(dòng)時(shí),用戶需要指定一個(gè)要執(zhí)行的主類(包含 main() 方法的那個(gè)類),虛擬機(jī)會(huì)先初始化這個(gè)主類。 當(dāng)使用 JDK 1.7 的動(dòng)態(tài)語言支持時(shí),如果一個(gè) java.lang.invoke.MethodHandle 實(shí)例最后的解析結(jié)果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且這個(gè)方法句柄所對(duì)應(yīng)的類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。
總結(jié)
1、靜態(tài)域(靜態(tài)變量、靜態(tài)塊)是按逐行順序加載的,并且靜態(tài)域只會(huì)加載一次。
2、當(dāng)實(shí)例化對(duì)象之前(構(gòu)造方法調(diào)用),會(huì)先去初始化靜態(tài)域,再去調(diào)用構(gòu)造函數(shù)實(shí)例化對(duì)象。
3、一般對(duì)象初始化順序如下:父類的靜態(tài)域順序加載–>子類靜態(tài)域順序加載–>父類非靜態(tài)域初始化->父類構(gòu)造函數(shù)初始化–>子類非靜態(tài)域初始化->子類構(gòu)造函數(shù)初始化。

參考
https://blog.csdn.net/qq_36522306/article/details/80584595 https://www.cnblogs.com/cxiang/p/10082160.html
