Java中final和static修飾的變量是在什么時(shí)候賦值的?

開(kāi)始
一位朋友在群里問(wèn)了這樣一個(gè)問(wèn)題:

本著樂(lè)于助人的想法,我當(dāng)時(shí)給出的回答:

后來(lái)我總覺(jué)得哪里不對(duì)勁。
于是我仔細(xì)翻閱了《Java虛擬機(jī)規(guī)范》和《深入理解Java虛擬機(jī)》關(guān)于這一部分的內(nèi)容。
害!發(fā)現(xiàn)自己理解的有問(wèn)題。
因?yàn)樽约旱睦斫獬鲥e(cuò)而誤導(dǎo)了別人,實(shí)在是讓我萬(wàn)分羞愧!
于是我加了這位朋友的好友,向這位朋友表達(dá)了歉意。
這位朋友也非常隨和,對(duì)此表示理解。
今天討論的問(wèn)題就是從這個(gè)故事開(kāi)始的。
final修飾的實(shí)例變量
我們先分析一下這個(gè)問(wèn)題:
深入Java虛擬機(jī)有一句是ConstantValue屬性的作用是通知虛擬機(jī)自動(dòng)為靜態(tài)變量賦值,只有被static關(guān)鍵字修飾的變量才可以使用這項(xiàng)屬性。但為什么private final a = 10也可以被賦值?
我翻閱了《深入理解Java虛擬機(jī)》第二版,在第191頁(yè),確實(shí)有前面那句話

書(shū)中說(shuō)的很清楚,ConstantValue屬性的作用是通知虛擬機(jī)自動(dòng)為靜態(tài)變量賦值。
那就意味著只有static修飾的類變量才會(huì)在class文件中對(duì)應(yīng)的字段表加上ConstantValue屬性嗎?
答案是否定的。
用final修飾的實(shí)例變量,編譯成class文件的時(shí)候,對(duì)應(yīng)的字段表也有可能會(huì)加上ConstantValue屬性。
注意,我這里用了“可能”這兩個(gè)字,因?yàn)檫@是有條件的。
哪些情況會(huì)有ConstantValue屬性呢?
我們寫(xiě)一段代碼,列舉一下用final修飾的實(shí)例變量的幾種情況。
然后用javap -verbose命令反編譯查看字節(jié)碼:

我們可以看到,在字段表集合里面有四個(gè)字段表:
分表對(duì)應(yīng)著a,b,c,d,e五個(gè)實(shí)例屬性。
他們都帶有ACC_PUBLIC(public)和ACC_FINAL(final)的訪問(wèn)標(biāo)志。
但只有a和b對(duì)應(yīng)的字段表帶有ConstantValue屬性。
不難得出:
用final修飾不是在構(gòu)造方法賦值的String類型或者基本類型成員變量,編譯成字節(jié)碼文件時(shí),對(duì)應(yīng)的字段表也會(huì)帶有ConstantValue屬性。
這個(gè)結(jié)論不和《深入理解Java虛擬機(jī)》沖突嗎?
于是我翻閱了官網(wǎng)的JVM規(guī)范,在4.7.2部分我找到了這樣一句話:

書(shū)中說(shuō)的很清楚:
如果field_info(字段表)表示的非靜態(tài)字段包含了ConstantValue屬性,那么這個(gè)ConstantValue屬性會(huì)被JVM虛擬機(jī)所忽略。
也就是說(shuō),對(duì)于非靜態(tài)字段,就算你編譯器加上了ConstantValue屬性,JVM也會(huì)忽略掉,你加不加結(jié)果是一樣的。
看完JVM規(guī)范里面的說(shuō)明,再回來(lái)看《深入理解Java虛擬機(jī)》里面的這句話:
ConstantValue屬性的作用是通知虛擬機(jī)自動(dòng)為靜態(tài)變量賦值,只有被static關(guān)鍵字修飾的類變量才可以使用這項(xiàng)屬性。
作者的這句話的前半句沒(méi)有什么爭(zhēng)議,但我覺(jué)得后半句的表述的不太明確,容易造成誤解。
以我的理解,應(yīng)該是“只有被static關(guān)鍵字修飾的類變量才可以使用ConstantValue這項(xiàng)屬性來(lái)進(jìn)行初始化,否則使用這項(xiàng)屬性也會(huì)被JVM忽略掉”
好了,我們?cè)倩氐侥俏慌笥褑?wèn)的問(wèn)題:
“為什么private final a = 10也可以被賦值?”
首先,這個(gè)問(wèn)題的本身就問(wèn)的不太準(zhǔn)確。
我理解這位朋友真正想問(wèn)的是“為什么private final a = 10也可以通過(guò)ConstantValue屬性的形式賦值?”
我覺(jué)得這是一個(gè)很好的問(wèn)題。
這位朋友通過(guò)實(shí)驗(yàn)發(fā)現(xiàn)用final修飾的實(shí)例變量對(duì)應(yīng)的字段表有ConstantValue屬性。
結(jié)合《深入理解Java虛擬機(jī)》,他認(rèn)為a是通過(guò)ConstantValue屬性讓虛擬機(jī)知道然后為其賦值的。
最后他發(fā)現(xiàn)和書(shū)中沖突,于是提出了上文的這個(gè)問(wèn)題。
這位朋友的思路有問(wèn)題嗎?我覺(jué)得是沒(méi)有問(wèn)題的。
不過(guò)這樣的理解是對(duì)的嗎?顯然是不對(duì)的。
因?yàn)樘摂M機(jī)規(guī)范是這樣規(guī)范的。對(duì)于非靜態(tài)字段,ConstantValue屬性是不會(huì)生效的。
至于為什么要這樣設(shè)計(jì),功力不夠的我暫時(shí)無(wú)法理解設(shè)計(jì)者的想法。
那單獨(dú)用final修飾的實(shí)例變量到底是在什么時(shí)候賦值的呢?
這個(gè)問(wèn)題也不難回答,看一下字節(jié)碼就清楚了。
圖片可點(diǎn)擊放大
通過(guò)查看字節(jié)碼,我們可以看到生成了一個(gè)
什么是

我們溫習(xí)一下這個(gè)英語(yǔ)四級(jí)短語(yǔ):appear as

然后,我們一起翻譯一下:
在JVM層面上,每一個(gè)用Java寫(xiě)的構(gòu)造方法都表現(xiàn)為實(shí)例初始方法,這個(gè)方法就是
我們?cè)賮?lái)看一下putfield這個(gè)字節(jié)碼指令的含義:

為指定的類的實(shí)例域賦值的,也就是為實(shí)例變量賦值的指令。
知道了
這些用final修飾實(shí)例變量是在實(shí)例構(gòu)造器
static修飾的類變量
上面講到ConstantValue屬性的作用是通知虛擬機(jī)為靜態(tài)變量賦值。
什么是靜態(tài)變量?static修飾的變量!
那static修飾的變量是什么時(shí)候加載的呢?
在這之前,我需要給你把類加載的幾個(gè)過(guò)程大致講一下:
類的生命周期由7個(gè)階段組成,類加載說(shuō)的是前5個(gè)階段:
即加載—>驗(yàn)證—>準(zhǔn)備—>解析—>初始化。
類的生命周期圖
我們簡(jiǎn)單過(guò)一下這幾個(gè)階段:
加載:將字節(jié)碼所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。
驗(yàn)證:驗(yàn)證字節(jié)碼格式,確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全。
準(zhǔn)備:創(chuàng)建類或者接口的靜態(tài)字段,并為靜態(tài)變量設(shè)置初始值。
解析:將常量池內(nèi)的符號(hào)引用替換為直接引用。
初始化:執(zhí)行類構(gòu)造器
方法。
上面出現(xiàn)類構(gòu)造器
JVM 規(guī)范這樣寫(xiě)道:

說(shuō)白了,編譯器會(huì)收集所有靜態(tài)變量的賦值動(dòng)作、所有靜態(tài)代碼塊,合并產(chǎn)生一個(gè)方法,即
回到static修飾的變量(類變量),類變量有兩種賦值方式可以選擇:
使用ConstantValue屬性賦值。
在類構(gòu)造器
方法中賦值。
目前Oracle公司實(shí)現(xiàn)的Javac編譯器的選擇是:
final+static修飾:使用ConstantValue屬性賦值。
僅使用static修飾:在
方法中賦值。這個(gè)方法在類加載的初始化階段執(zhí)行 。
需要注意點(diǎn)的是,用生成ConstantValue屬性來(lái)進(jìn)行初始化,這個(gè)變量必須是基本類型或者java.lang.String類型。
對(duì)于這一點(diǎn),我們也可以通過(guò)javap -verbose命令反編譯驗(yàn)證一下:

為什么呢?
這是因?yàn)镃lass文件格式的常量類型中只有與基本屬性和字符串相對(duì)應(yīng)的字面量,所以就算ConstantValue屬性想支持別的類型也無(wú)能為力。
final+static修飾的常量
上面我們說(shuō)過(guò),
那static+final修飾的常量是在類加載的那一階段進(jìn)行的呢?
我們可以看一下JVM規(guī)范:

我們可以看到在JVM規(guī)范里面,static+final修飾的常量是在初始化階段執(zhí)行
咦?網(wǎng)上的博客不都是在類加載的準(zhǔn)備階段會(huì)對(duì)普通類屬性賦初始值,對(duì)帶有ConstantValue的類屬性直接賦值嗎?
《深入理解Java虛擬機(jī)》也是這樣說(shuō)的啊?

書(shū)上是錯(cuò)的嗎?不是的,因?yàn)椤渡钊肜斫釰ava虛擬機(jī)》里面講的具體實(shí)現(xiàn),是基于HotSpot VM講的。
確確實(shí)實(shí),HotSpot VM就是這么干的,我們也可以在openJdk中找到對(duì)應(yīng)的源碼:


跟蹤源碼可以發(fā)現(xiàn)HotSpot VM對(duì)基本類型或者字符串類型的常量的賦值確實(shí)在準(zhǔn)備階段完成了。
但一個(gè)很關(guān)鍵的點(diǎn)是,雖然沒(méi)在初始化階段賦值,仍然在調(diào)用
外界是不會(huì)觀察到HotSpot VM提前做了這個(gè)初始化賦值的,所以是沒(méi)問(wèn)題的。
不過(guò)要記住的是:
JVM規(guī)范里明確說(shuō)了正確的初始化時(shí)機(jī)是在“初始化(Initialization)”階段。
總結(jié)
單獨(dú)用final修飾的變量也有可能在字節(jié)碼找到對(duì)應(yīng)的ConstantValue屬性,但是會(huì)被JVM忽略掉。
final修飾的實(shí)例屬性,在實(shí)例創(chuàng)建的時(shí)候才會(huì)賦值。
static修飾的類屬性,在類加載的準(zhǔn)備階段賦初值,初始化階段賦值。
static+final修飾的String類型或者基本類型常量,JVM規(guī)范建議在初始化階段賦值,但是HotSpot VM直接在準(zhǔn)備階段就賦值了。
static+final修飾的其他引用類型常量,賦值步驟和第二點(diǎn)的流程是一樣的。
還有一點(diǎn),一定不要把《深入理解Java虛擬機(jī)》和《Java虛擬機(jī)規(guī)范》搞混了。
《Java虛擬機(jī)規(guī)范》是官方JVM規(guī)范文檔翻譯而來(lái)的,所有的JVM實(shí)現(xiàn)都要遵從規(guī)范,但有強(qiáng)制要求的規(guī)范和建議的規(guī)范。
《深入理解Java虛擬機(jī)》是作者根據(jù)自己的理解,結(jié)合HotSpot VM的具體實(shí)現(xiàn),為了讓讀者更容易理解JVM而寫(xiě)的一本書(shū)。
寫(xiě)在最后
本人才疏學(xué)淺,OpenJdk源碼也理解的不夠透徹。
為了說(shuō)清楚文中的一些知識(shí),我翻閱了很多資料。
但是還是有一部分的知識(shí)點(diǎn)無(wú)法找到權(quán)威的資料證明。
不過(guò)我盡量都基于官方文檔展開(kāi)分析,如果有認(rèn)識(shí)有差錯(cuò)的地方,歡迎指出!我定會(huì)在第一時(shí)間修改,不誤導(dǎo)別人!
最后,感謝你的閱讀!
如果可以,點(diǎn)贊、再看加關(guān)注,謝謝你!
—————END—————
推薦閱讀:
最近面試BAT,整理一份面試資料《Java面試BAT通關(guān)手冊(cè)》,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫(kù)、數(shù)據(jù)結(jié)構(gòu)等等。
獲取方式:點(diǎn)“在看”,關(guān)注公眾號(hào)并回復(fù)?666?領(lǐng)取,更多內(nèi)容陸續(xù)奉上。
明天見(jiàn)(??ω??)?
