不知道大家有沒有這么一種經(jīng)驗——無論遇到的是新舊知識租漂,經(jīng)常地會以為自己掌握了理盆,但當換另一種問法俯抖,或者增加一點難度的問題输瓜,往往我們會手足無措,不知所云芬萍。這段時間非常自信自己已經(jīng)掌握了一個知識點尤揣,隔段時間才發(fā)現(xiàn)之前自己的理解其實完全是錯誤的,當初愉悅自信的心情依稀記于心中柬祠,而此刻對比起來卻是啪啪啪把臉打的很疼北戏,心情也變得很低落。現(xiàn)在的我可以肯定——自己太功利浮躁漫蛔,懶于思考嗜愈。意識到這種現(xiàn)象后旧蛾,為了變得更好,與自己和解蠕嫁,腳踏實地花時間死磕吧锨天,一開始進步可能很慢,時間久了剃毒,相信一定會有突破的~
那今天要提的一個現(xiàn)象呢病袄,是我在學習Java類加載機制時候碰到的。關(guān)于詳細的Java類加載知識的輸出赘阀,我還需要一段時間的整合益缠。這篇文章先做個小記錄。
額外插入一個注意事項——靜態(tài)代碼塊和靜態(tài)方法不是同一個東西基公。其實基礎(chǔ)常常不會難幅慌,只是我們太過功利焦急,囫圇吞棗轰豆,把很多概念混淆了欠痴,后來越來越亂。在寫法上秒咨,下圖前3行就是靜態(tài)代碼塊,后3行是靜態(tài)方法掌挚。二者都是在類加載的時候初始化的雨席,區(qū)別就是——靜態(tài)代碼塊是自動執(zhí)行的,而靜態(tài)方法是被調(diào)用的時候才執(zhí)行的吠式,比如Test.function(); 陡厘。
網(wǎng)上看到很多篇文章寫了幾個小小的demo,然后根據(jù)運行結(jié)果總結(jié)了靜態(tài)代碼塊特占、構(gòu)造代碼塊和構(gòu)造方法的執(zhí)行順序——靜態(tài)代碼塊>非靜態(tài)代碼塊>構(gòu)造方法糙置,完事。原本我也以為自己背下這個結(jié)論就萬事大吉了是目。直到我遇到了下面這個例子谤饭,我才開始反思,當我在看別人博客的時候懊纳,我在看什么揉抵。花兩分鐘想想下面代碼的運行結(jié)果會是什么呢嗤疯?
當親自碼代碼并運行代碼后冤今,我對運行結(jié)果充滿了疑惑。
網(wǎng)上的文章不都說執(zhí)行順序是靜態(tài)代碼塊>非靜態(tài)代碼塊>構(gòu)造方法嗎茂缚?雖然這個代碼里沒模擬輸出構(gòu)造方法(大家可以自己加上試試)戏罢,但為什么結(jié)果是三行輸出屋谭,并且第一行結(jié)果是normal?難道網(wǎng)上文章的總結(jié)是錯誤的龟糕?其實不盡然桐磁,我掌握的知識不夠全面,以至于沒法判斷導(dǎo)致此現(xiàn)象的原因翩蘸,更別說舉一反三了所意。
當Java源代碼(.java文件)被編譯器編譯成Java字節(jié)碼(.class文件)時,會自動產(chǎn)生兩個方法催首,一個是類的初始化方法<clinit>扶踊,另一個是對象實例的初始化方法<init>。但需要注意的是郎任,并不是所有的類的.class文件中都擁有一個<clinit>()的——如果類沒有聲明任何類變量(類變量即靜態(tài)變量秧耗,被static修飾的變量。類的成員變量包含靜態(tài)變量和普通成員變量)舶治,也沒有靜態(tài)初始化語句分井,那么就不會有<clinit>();如果類僅包含靜態(tài)final變量的類變量初始化語句霉猛,并且這類類變量初始化語句采用編譯時常量表達式尺锚,則類也不會有<clinit>方法。
public class Extended {
? ? ? static final int age = 18;
? ? ? ? static final int height = age*10;
}
類Extended聲明了兩個常量——age和height惜浅,并賦了初始值瘫辩,但這兩個字段并沒有被當做類變量,而是被Java編譯器特殊處理了坛悉,因為被final修飾了伐厌,被當做常量。JVM在使用了它們的任何類的常量池或者字節(jié)碼流中直接存放的是它們表示的常量的 int 值裸影。
另外需要注意的一點是挣轨,Java編譯器為它編譯的每個類中的構(gòu)造方法都產(chǎn)生一個<init>方法。我們可以想想轩猩,一個類可以有很多構(gòu)造函數(shù)卷扮,所以一個類的.class文件中至少生成這樣一個<init>方法(即無參默認構(gòu)造方法)。
好界轩,就算上面的內(nèi)容你并不能記憶画饥,也不影響接下來內(nèi)容的掌握。只是多了解點片塊知識浊猾,有利于搭建更大的知識模塊抖甘,到時候融會貫通,感受設(shè)計之美『鳎現(xiàn)在我們來談?wù)務(wù)f<clinit>方法和<init>()方法都是什么時候被調(diào)用的衔彻。這是關(guān)鍵點薇宠。
<clinit>():在JVM第一次加載.class文件到內(nèi)存時調(diào)用。包括靜態(tài)變量初始化語句(如第9行和第11行)和靜態(tài)代碼塊(如第16~18行)的執(zhí)行艰额。
<init>():在對象實例創(chuàng)建出來的時候調(diào)用澄港。
對應(yīng)到我們上面的代碼例子要怎么理解呢?別急柄沮,我們再了解一些背景——JVM的其中一個工作內(nèi)容是借助類裝載器子系統(tǒng)將.class文件讀取到內(nèi)存中的回梧。我們知道.class文件是一種8位字節(jié)的二進制流文件,JVM裝載一個.class文件時祖搓,它會從這個二進制數(shù)據(jù)中解析類型信息狱意,然后把這些類型信息放到方法區(qū)中。方法區(qū)中存有類變量(類變量即靜態(tài)變量)和方法等拯欧。而當程序運行時详囤,JVM會把所有該程序在運行時創(chuàng)建的對象都放到堆中猖败。
一般情況下巡球,靜態(tài)隨著類的加載而加載,而且優(yōu)先于對象的存在凉蜂「眉郑回過頭來對應(yīng)我們上面的代碼羔杨。在走19行入口之前會先加載Test這個類。那怎么加載呢杨蛋,按照實際代碼寫的內(nèi)容问畅,并且先執(zhí)行靜態(tài)內(nèi)容,或者我們可以換個說法六荒,<clinit>()中有的內(nèi)容是第9、11矾端、16~18行掏击。而<init>()中是13~15行內(nèi)容。因為在19行入口前先加載Test這個類秩铆,所以先執(zhí)行<clinit>()砚亭,而當加載完畢,開始執(zhí)行20行代碼時殴玛,就調(diào)用<init>()捅膘。
但是照我這么說,還是不明白為什么運行結(jié)果先輸出了“normal”滚粟?那當然了寻仗,我還沒講到這點。
代碼從上到下凡壤,發(fā)現(xiàn)第9行是類變量署尤,放到方法區(qū)去耙替,然后再按順序?qū)ふ移渌o態(tài)內(nèi)容。我們可以看到第11行的時候曹体,創(chuàng)建了一個static的Test類對象test俗扇。咋辦呀咋辦呀,它好像很特別箕别?不按常理出牌铜幽?是static,但是又是新創(chuàng)建的對象串稀?怎么不是一個基本類型除抛?這樣可怎么整呀?其實很簡單厨诸,我們上面說了<init>():在對象實例創(chuàng)建出來的時候調(diào)用镶殷。那淡定地在11行調(diào)起<init>()就好。那<init>()里面有什么內(nèi)容微酬?就是13~15行的代碼了绘趋,輸出“normal”。
我們繼續(xù)看代碼颗管,執(zhí)行完11行代碼后陷遮,再尋找下一部分靜態(tài)內(nèi)容——16~18行,輸出“static 1”垦江。至此帽馋,Test類里沒有靜態(tài)內(nèi)容了。加載結(jié)束比吭,該進入第19行了绽族,到第20行的時候,發(fā)現(xiàn)又創(chuàng)建了一個Test對象衩藤,那怎么做吧慢?調(diào)用<init>()呀,即又跑了一遍13~15行的代碼赏表,輸出“normal”了检诗。
一點小建議:可以在第20行之前再寫一行代碼
System.out.println("==========");
大家還可以試試在Test類中增加構(gòu)造函數(shù)的代碼,并且構(gòu)造函數(shù)里面做類似的輸出瓢剿,看看會是什么樣的結(jié)果逢慌。
至此,我們把文中的代碼運行結(jié)果原因講了一遍间狂。不知道對大家有沒有幫助攻泼。這篇文章看起來不長,但是真的寫了很久很久。我一直在斟酌如何措辭和描述現(xiàn)象坠韩。就連標題我都不確定自己是不是起的準確距潘,很難做到盡善盡美,我大概了解代碼運行涉及到的流程只搁,但有很多細枝末節(jié)的知識點是我暫時沒法確認的音比,盡管我自己會猜測一些原因,也相信自己的猜測是正確的氢惋,但我沒有在書上或者其他地方找到確鑿的證據(jù)洞翩,不敢直接在文章中告訴大家——事實就是blabla。給大家推薦一本書 文納斯的《深入Java虛擬機》焰望,這本書我看了兩遍骚亿,重點章節(jié)翻了好幾次,不過我覺得還可以再多閱讀閱讀熊赖。周志明也寫了一本《深入理解Java虛擬機》来屠,不知道內(nèi)容怎么樣,但豆瓣上的評分也挺高的震鹉。
關(guān)于虛擬機涉及到的相關(guān)內(nèi)容俱笛,我后面還會再做輸出,如有不正確的地方传趾,還請大家批評指正迎膜,我們共同進步,謝謝~