一個 Java 類的加載
該系列文章,主要是為了深入學習Java完成的一條鏈,推薦閱讀的整體順序為:Java的內(nèi)存模型(根源),一個java文件被執(zhí)行的歷程,一個Java類的加載,Java的垃圾回收機制及算法,Linux(六):系統(tǒng)運維常用命令? 和 Java程序運行狀態(tài)的監(jiān)控(實用,定位Java程序問題)
0x01:類加載
我一直認為,不應該把類的加載,單獨當作一個模塊去看,那樣就是單純地去看一個知識點,不利于建立Java全體系的知識架構,更別說實際應用到開發(fā)中(閱讀優(yōu)秀開源項目、寫出高質(zhì)量的代碼或定位問題)。所以這里應該串聯(lián)一整個Java語言編譯的全流程。
下面說一下在Java中類加載的概念及它在整個Java程序得以運行的過程中所處的位置:

類的加載指的是將類的字節(jié)碼文件(.class文件)中數(shù)據(jù)讀入到內(nèi)存中,將其放在運行時數(shù)據(jù)區(qū)的方法區(qū)內(nèi),然后在堆區(qū)創(chuàng)建一個java.lang.Class對象(關于這部分可以看之前的一篇關于Java反射的內(nèi)容:入口),用來封裝類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構。類的加載的最終產(chǎn)品是位于堆區(qū)中的Class對象,Class對象封裝了類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構,并且向Java程序員提供了訪問方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構的接口。
類加載器并不需要等到某個類被“首次主動使用”時再加載它,JVM規(guī)范允許類加載器在預料某個類將要被使用時就預先加載它,如果在預先加載的過程中遇到了.class文件缺失或存在錯誤,類加載器必須在程序首次主動使用該類時才報告錯誤(LinkageError錯誤)如果這個類一直沒有被程序主動使用,那么類加載器就不會報告錯誤。
上面的話感覺很懵?沒事,我給你翻譯翻譯,和那些編譯時需要進行連接工作的語言不同(那些語言都是完成全部代碼的編譯連接全部放到內(nèi)存中才開始運行),在Java里,類的加載、連接和初始化過程都是在程序運行起來以后進行的,或者說是在運行期間完成的(懵逼?沒事,先保留困惑,詳細的解釋會在后面類的加載時機那塊做出解釋)。它的這種設計,會在類加載時增加一定的性能開銷,但是這樣是為了滿足Java的高度靈活性,Java是天生地可以動態(tài)擴展地語言,這一特性就是依賴運行期動態(tài)加載和動態(tài)連接實現(xiàn)的。
0x02:類的生命周期
說類加載的過程之前,我們先來了解一下,類的整個生命周期要經(jīng)歷什么

類從被加載到虛擬機的內(nèi)存中開始,到卸載出內(nèi)存(整個程序\系統(tǒng)運行結(jié)束虛擬機關閉)為止,它的整個生命周期包括:加載、鏈接、初始化、使用、卸載。
因為這里著重說類的加載這一過程,所以類的使用和卸載就不介紹了,后面就默認類的加載這個過程包含:加載、鏈接、初始化
加載(Load)
這里叫做加載,很容易讓人誤會,會覺得類的加載就是指這里,其實不是這個樣子,這里的加載二字和類的加載不是一回事,可以這么理解,加載是類加載過程的一個階段,這一階段,虛擬機主要是做三件事:
1、根據(jù)類的全路徑獲取類的二進制字節(jié)流
2、將這個字節(jié)流對應的結(jié)構轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(把編碼的組織方式變成虛擬機運行時所能解讀的結(jié)構,存放于方法區(qū))
3、在內(nèi)存中生成一個Class對象(java.lang.Class),由這個對象來關聯(lián)方法區(qū)中的數(shù)據(jù)
這里特別注意一下,以上的三點,只是虛擬機規(guī)范定義的,至于具體如何實現(xiàn),是依賴具體的虛擬機來的;例如,第一件事的獲取二進制流,并不一定是從字節(jié)碼文件(Class文件)從進行獲取,它可以是從ZIP中獲取,從網(wǎng)絡中獲取,利用代理在計算過程中生成等等;
還有第三件事中生成的Class對象,也并不一定是在堆區(qū)的,例如HostSpot虛擬機的實現(xiàn)上,Class對象就是放在方法區(qū)的。
鏈接(Link)
鏈接階段又細分為驗證、準備、解析三個步驟:
驗證
作為鏈接的第一步,它的職責就是確保Class文件的字節(jié)流中包含的信息是符合規(guī)定的,并且不會對虛擬機進行破壞;其實說白了就是它主要責任就是保證你寫的代碼是符合Java語法的,是合理可行的。如果不合理,編譯器是拒絕的。驗證主要是針對 文件格式的驗證、元數(shù)據(jù)的驗證,字節(jié)碼的驗證,符號引用的驗證;
文件格式的驗證是對字節(jié)流進行是否符合Class文件格式的驗證,元數(shù)據(jù)的驗證主要是語義語法的驗證,即驗是否符合Java語言規(guī)范,例如:一個類是否有父類(我們知道Java中處理Object,所有的類都應該有個父類),字節(jié)碼的驗證主要是對數(shù)據(jù)流和控制流進行驗證,確保程序語義是合法、合邏輯的,例如:在操作棧先放了一個Int型的數(shù)據(jù),后面某個地方使用的時候卻用Long型來接它。符號引用的驗證是確保解析動作能夠正常執(zhí)行。
整個驗證過程,保證了Java語言的安全性,不會出現(xiàn)不可控的情況。(這里補充一下,這里說的驗證、不可控,包括上面舉的例子,并不是我們編程中寫的類似于a != null這種,它是在我們編寫的程序更下一層的字節(jié)碼的解析上來說的),對于加載的過程來說,驗證階段很重要,但并不一定是必須的,因為它對程序運行期并沒有影響,僅僅旨在保證語言的安全性,如果所運行的全部代碼都已經(jīng)被反復使用和驗證過,那么在實施階段,可以考慮使用-Xverify:none參數(shù)來關閉大部分的驗證過程,以達到縮短虛擬機加載的時間。
準備
準備階段主要作用是正式為類變量分配內(nèi)存并設置類變量初始值的階段,即這些變量所使用的內(nèi)存,都在方法區(qū)中進行分配。這里需要注意,這時候進行內(nèi)存分配的僅僅是類變量,換句話說也就是靜態(tài)變量(static修飾的),并不包括實例變量,實例變量會在實例化時分配在堆內(nèi)存中。初始值也并不是我們的賦值,
例如:
public?Class?A{
??public?String name;
??public?static?int?value?=?987;
}就像剛剛講的,這里在準備階段,只會對value變量進行內(nèi)存分配,并不會對name進行分配,其次,在準備階段,對value分配完內(nèi)存,會同時賦予初始值,但是并不會賦給它987,在準備階段,value的值是0。而賦值為987的指令,是在程序被編譯后,存放于類構造器<clinit>()方法中,所以把value賦值987的操作,會在初始化階段才會進行。(這里補充個特殊情況,如果我們寫成 public static final int ?value = 987,那么變量value 在準備階段就會被賦值為987,這就是為什么很多書在講final字段的時候說它一般用來定義常量,且一經(jīng)使用,就不可以被更改的原因)
解析
解析階段的任務是將常量池中的符號引用替換為直接引用
常量池可以理解為存放我們代碼符號的地方,例如我們代碼中聲明的變量,它僅僅是個符號,并不具備實際內(nèi)存,所有這些符號,都會放在常量池中。例如,一個類的方法為test(),則符號引用即為test,這個方法存在于內(nèi)存中的地址假設為0x123456,則這個地址則為直接引用。
符號引用:
符號引用更多的是以一組符號來描述所引用的內(nèi)存目標,符號和內(nèi)存空間實際并沒有關系,引用的目標也不一定在內(nèi)存里,只是我們在代碼中自己寫的時候區(qū)分的,例如一句 Persion one;其中one就是個’o‘,’n‘,’e‘三個符號的組合,它啥也不是。
直接引用:
直接引用可以是直接指向內(nèi)存空間的指針、相對便宜量或是一個能夠簡潔定位到內(nèi)存目標的句柄。
解析動作主要是針對 類、接口、字段、類方法、方法類型、方法句柄和調(diào)用點限定符號的引用進行。
初始化(Initialize)
在類的加載過程中,加載、連接完全由虛擬機來主導和控制,到了初始化這一階段,才是真正開始執(zhí)行類中定義的Java代碼。初始化其實我個人理解的就是該階段是為類的類變量初始化值的,在準備階段變量已經(jīng)進了一次賦值,只不過那是系統(tǒng)要求的初始值,而在初始化階段的賦值,則是根據(jù)研發(fā)人員編寫的主觀程序去初始化變量和其他資源。在初始化這步,進行賦值的方式有兩種:
1、在聲明類變量時,直接給變量賦值
2、在靜態(tài)初始化塊為類變量賦值
使用
就是對象之間的調(diào)用通信等等
卸載(死亡)
遇到如下幾種情況,即類結(jié)束生命周期:
執(zhí)行了System.exit()方法
程序正常執(zhí)行結(jié)束
程序在執(zhí)行過程中遇到了異常或錯誤而異常終止
由于操作系統(tǒng)出現(xiàn)錯誤而導致Java虛擬機進程終止
0x03:類加載器
之前說了那么多一個類的聲明周期,更多的是一種理論基礎,映射到具體的代碼層面,到底是什么來完成類加載這個過程的就是這里要說的——類加載器。
虛擬機在設計時,把類加載階段的 “通過一個類的全路徑名來獲取該類字節(jié)碼二進制流” 這個動作放到了 Java虛擬機之外去完成,而負責實現(xiàn)這個動作的模塊就叫做類加載器。
類加載器分類

啟動類加載器
1、它用來加載 Java 的核心庫(JAVA_HOME/jre/lib/rt.jar,sun.boot.class.path路徑下的內(nèi)容),并不是Java代碼完成,而是用原生代碼(C語言或C++)來實現(xiàn)的,并不繼承自 java.lang.ClassLoader。
2、加載擴展類和應用程序類加載器。并指定他們的父類加載器。
擴展類加載器
這一類加載器由sun.misc.Launcher$ExtClassLoader實現(xiàn),用來加載 Java 的擴展庫(JAVA_HOME/jre/ext/*.jar,或java.ext.dirs路徑下的內(nèi)容) 。Java 虛擬機的實現(xiàn)會提供一個擴展庫目錄。該類加載器在此目錄里面查找并加載 Java類。
應用類加載器
這個類加載器由sun.misc.Launcher$AppClassLoader實現(xiàn),由于這個類加載器是ClassLoader中getSystemClassLoader()方法的返回值,所以它也成為系統(tǒng)類加載器。它負責加載用戶類路徑下所指定的類庫,開發(fā)者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是系統(tǒng)默認的類加載器。
自定義加載器
開發(fā)人員可以通過繼承 java.lang.ClassLoader類的方式實現(xiàn)自己的類加載器,以滿足一些特殊的需求。
類加載的代理(雙親委派模式)

如果一個類加載器收到了類加載器的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父加載器去完成。每個層次的類加載器都是如此。因此所有的加載請求最終都會傳送到Bootstrap類加載器(啟動類加載器)中,只有父類加載反饋自己無法加載這個請求(它的搜索范圍中沒有找到所需的類)時子加載器才會嘗試自己去加載。
例如類java.lang.Object,它存放在rt.jart之中,無論哪一個類加載器都要加載這個類.最終都是雙親委派模型最頂端的Bootstrap類加載器去加載,因此Object類在程序的各種類加載器環(huán)境中都是同一個類。相反,如果沒有使用雙親委派模型.由各個類加載器自行去加載的話,如果用戶編寫了一個稱為“java.lang.Object”的類,并存放在程序的ClassPath中,那系統(tǒng)中將會出現(xiàn)多個不同的Object類.java類型體系中最基礎的行為也就無法保證。應用程序也將會一片混亂。
當然也并不是所有的加載機制都是雙親委派的方式,例如tomcat作為一個web服務器,它本身實現(xiàn)了類加載,該類加載器也使用代理模式(不同于前面說的雙親委托機制),所不同的是它是首先嘗試去加載某個類,如果找不到再代理給父類加載器。這與一般類加載器的順序是相反的。但也是為了保證安全,這樣核心庫就不在查詢范圍之內(nèi)。
類的加載時機
最后說一個比較重要也是諸多困惑的地方,就是什么時候才會加載類。
加載、驗證、準備、初始化、卸載這五個步驟是確定的,類的加載過程必須按部就班地開始,但是解析階段就不一定了,它在某些情況下是可以在初始化階段之后再開始,看到這里,肯定滿腦子????,其實不必驚訝,我一開始就說了,它這是為了滿足Java語言地動態(tài)時綁定(泛型、多態(tài)的本質(zhì))這個特性來的,它是按部就班的開始,而不是按部就班的 “進行”或者“結(jié)束”,這些階段其實是相互交叉混合進行的,通常會在一個階段執(zhí)行的過程中調(diào)用、激活另外一個階段。
其實上面的話有些繞,我們從類的使用上來看這個問題,類的使用分為主動引用和被動引用:
1、主動引用類(肯定會初始化)
new一個類的對象。
調(diào)用類的靜態(tài)成員(除了final常量)和靜態(tài)方法。
使用java.lang.reflect包的方法對類進行反射調(diào)用。
當虛擬機啟動,java Hello,則一定會初始化Hello類。說白了就是先啟動main方法所在的類。
當初始化一個類,如果其父類沒有被初始化,則先會初始化他的父類
被動引用
當訪問一個靜態(tài)域時,只有真正聲明這個域的類才會被初始化。例如:通過子類引用父類的靜態(tài)變量,不會導致子類初始化。
通過數(shù)組定義類引用,不會觸發(fā)此類的初始化。
引用常量不會觸發(fā)此類的初始化(常量在編譯階段就存入調(diào)用類的常量池中了)。
首先,Java的編譯不是像其他語言一樣,都加載到內(nèi)存中才開始運行,而且動態(tài)的,也就會出現(xiàn):先運行了一部分,初始化了一些類,但是在這一部分運行的代碼里被動引用了未被初始化的類(例如static變量),這時候就會出現(xiàn)了這種違背順序的情況。總的來說就是,
先加載并連接當前類
父類沒有被加載,則去加載、連接、初始化父類,依舊是先加載并連接,然后再判斷有無父類,如此循環(huán)(所以JVM先將Object加載)
如果類中有初始化語句,包括聲明時賦值與靜態(tài)初始化塊,則按順序進行初始化
source:https://www.cnblogs.com/TheGCC/p/14738123.html

喜歡,在看
