前言
了解dubbo的時候火的,因?yàn)镾PI機(jī)制用到了動態(tài)代理的機(jī)制赠法,從而涉及到了類加載機(jī)制相關(guān)的東西麦轰,整個概念也屬于非常底層的邏輯,也好久沒整理了砖织,現(xiàn)整理一下款侵,便于后續(xù)翻閱。
盡可能的關(guān)聯(lián)JVM相關(guān)的知識點(diǎn)侧纯,如果讀者有補(bǔ)充的可以留言補(bǔ)充新锈。
類加載機(jī)制概念
Java虛擬機(jī)把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對數(shù)據(jù)進(jìn)行校驗(yàn)眶熬、轉(zhuǎn)換解析和初始化妹笆,最終形成可以被虛擬機(jī)直接使用的Java類型,這就是虛擬機(jī)的加載機(jī)制聋涨。*
Class文件由類裝載器裝載后晾浴,在JVM中將形成一份描述Class結(jié)構(gòu)的元信息對象,通過該元信息對象可以獲知Class的結(jié)構(gòu)信息:如構(gòu)造函數(shù)牍白,屬性和方法等脊凰,Java允許用戶借由這個Class相關(guān)的元信息對象間接調(diào)用Class對象的功能,這里就是我們經(jīng)常能見到的Class類。
類加載過程
類裝載器就是尋找類的字節(jié)碼文件茂腥,并構(gòu)造出類在JVM內(nèi)部表示的對象組件狸涌。主要要經(jīng)過以下步驟:
- (1) 裝載:查找和導(dǎo)入Class文件;
- (2) 鏈接:把類的二進(jìn)制數(shù)據(jù)合并到JRE中最岗;
- (a)校驗(yàn):檢查載入Class文件數(shù)據(jù)的正確性帕胆;
- (b)準(zhǔn)備:給類的靜態(tài)變量分配存儲空間;
- (c)解析:將符號引用轉(zhuǎn)成直接引用般渡;
- (3) 初始化:對類的靜態(tài)變量懒豹,靜態(tài)代碼塊執(zhí)行初始化操作
1.加載
類的裝載指的是將類的.class文件中的二進(jìn)制數(shù)據(jù)讀入到內(nèi)存中,將其放在運(yùn)行時數(shù)據(jù)區(qū)的方法區(qū)
內(nèi)驯用,然后在堆區(qū)
創(chuàng)建一個java.lang.Class對象脸秽,用來封裝類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)。
加載.class文件的方式有:
- 1). 從本地系統(tǒng)中直接加載
- 2). 通過網(wǎng)絡(luò)下載.class文件
- 3). 從zip蝴乔,jar等歸檔文件中加載.class文件
- 4). 從專有數(shù)據(jù)庫中提取.class文件
- 5). 將Java源文件動態(tài)編譯為.class文件
2.驗(yàn)證
驗(yàn)證的目的是為了確保class文件中的字節(jié)流包含的信息流符合虛擬機(jī)的規(guī)范记餐,因此,不同的虛擬機(jī)可能有不同的實(shí)現(xiàn)薇正,大致可分為以下幾步:
- 1)文件格式的驗(yàn)證:驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范片酝,經(jīng)過該階段的驗(yàn)證后囚衔,字節(jié)流才會進(jìn)入內(nèi)存的
方法區(qū)
中進(jìn)行存儲,后面的三個驗(yàn)證都是基于方法區(qū)
的存儲結(jié)構(gòu)進(jìn)行的雕沿。 - 2)元數(shù)據(jù)驗(yàn)證:對類中的各數(shù)據(jù)類型進(jìn)行語法校驗(yàn)练湿,保證不存在不符合Java語法規(guī)范的元數(shù)據(jù)信息。
- 3)字節(jié)碼驗(yàn)證:該階段驗(yàn)證的主要工作是進(jìn)行數(shù)據(jù)流和控制流分析晦炊,對類的方法體進(jìn)行校驗(yàn)分析鞠鲜,以保證被校驗(yàn)的類的方法在運(yùn)行時不會做出危害虛擬機(jī)安全的行為。
- 4)符號引用驗(yàn)證:這是最后一個階段的驗(yàn)證断国,它發(fā)生在虛擬機(jī)將符號引用轉(zhuǎn)化為直接引用的時候贤姆,主要是對類自身以外的信息(常量池中的各種符號引用)進(jìn)行匹配性的校驗(yàn)。
3.準(zhǔn)備
準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段稳衬,這些內(nèi)存都將在方法區(qū)中進(jìn)行分配
- 1)這時候進(jìn)行內(nèi)存分配的僅包括類變量(static)霞捡,而不包括實(shí)例變量,實(shí)例變量會在對象實(shí)例化時隨著對象一塊分配在Java堆中薄疚。
2)這里所設(shè)置的初始值通常情況下是數(shù)據(jù)類型默認(rèn)的初始值值(如0碧信、0L、null街夭、false等)砰碴,具體的賦值是在初始化過程中,而常量是在這個時候進(jìn)行賦值的板丽。
解析
解析階段是虛擬機(jī)將常量池內(nèi)的符號引用替換為直接引用的過程呈枉。
- 1)、類或接口的解析:判斷所要轉(zhuǎn)化成的直接引用是對數(shù)組類型埃碱,還是普通的對象類型的引用猖辫,從而進(jìn)行不同的解析。
- 2)砚殿、字段解析:對字段進(jìn)行解析時啃憎,會先在本類中查找是否包含有簡單名稱和字段描述符都與目標(biāo)相匹配的字段,如果有似炎,則查找結(jié)束辛萍;如果沒有,則會按照繼承關(guān)系從上往下遞歸搜索該類所實(shí)現(xiàn)的各個接口和它們的父接口羡藐,還沒有叹阔,則按照繼承關(guān)系從上往下遞歸搜索其父類,直至查找結(jié)束传睹。
- 3)、類方法解析:對類方法的解析與對字段解析的搜索步驟差不多岸晦,只是多了判斷該方法所處的是類還是接口的步驟欧啤,而且對類方法的匹配搜索睛藻,是先搜索父類,再搜索接口邢隧。
- 4)店印、接口方法解析:與類方法解析步驟類似,只是接口不會有父類倒慧,因此按摘,只遞歸向上搜索父接口就行了。
初始化
類初始化階段是類加載過程的最后一步纫谅,為類的靜態(tài)變量賦予正確的初始值炫贤,JVM負(fù)責(zé)對類進(jìn)行初始化,主要對類變量進(jìn)行初始化付秕。在Java中對類變量進(jìn)行初始值設(shè)定有兩種方式:
- ①聲明類變量時指定初始值
- ②使用靜態(tài)代碼塊為類變量指定初始值
以上也可知兰珍,方法區(qū)
存儲了靜態(tài)變量
、類信息
堆
中存儲了類變量
(局部變量
)
擴(kuò)展:即時編譯器(JIT Just In Time)
編譯后的一些熱點(diǎn)代碼也會存放在方法區(qū)
常量
數(shù)據(jù)在編譯訪問常量的代碼時才會放入方法區(qū)
中询吴。
由上面介紹可以知道掠河,方法區(qū)
存放著各線程都可用的數(shù)據(jù),因此是線程共享的猛计。
類的實(shí)例化
類實(shí)例化的一般過程是:
父類的類構(gòu)造器<clinit>() -> 子類的類構(gòu)造器<clinit>() -> 父類的成員變量和實(shí)例代碼塊 -> 父類的構(gòu)造函數(shù) -> 子類的成員變量和實(shí)例代碼塊 -> 子類的構(gòu)造函數(shù)->靜態(tài)代碼塊->方法唠摹。
在類的實(shí)例化過程中,首先會在虛擬機(jī)棧
中奉瘤,保存實(shí)例對象的引用勾拉,具體對象的屬性實(shí)例會存放在堆
中。
擴(kuò)展:這里也可以理解垃圾的生成
Test testA = new Test();
Test testB = new Test();
testA.setName("a");
testB.setName("b");
testB = testA;
以上偽代碼可解釋毛好,當(dāng)引用關(guān)系變化時望艺,原有testB.setName("b");
所對應(yīng)的堆中的內(nèi)存就可以當(dāng)做垃圾回收。
堆
除了對象實(shí)例肌访,還包括數(shù)組
找默,所以這也解釋為什么會報java.lang.OutOfMemoryError: Requested array size exceeds VM limit
方法調(diào)用
當(dāng)線程執(zhí)行一個方法時,就會隨之創(chuàng)建一個對應(yīng)的棧幀吼驶,并將建立的棧幀壓棧惩激。
將局部變量表(Local Variables)
、操作數(shù)棧(Operand Stack)
蟹演、指向當(dāng)前方法所屬的類的運(yùn)行時常量池(運(yùn)行時常量池的概念在方法區(qū)部分會談到)的引用(Reference to runtime constant pool)
风钻、方法返回地址(Return Address)和一些額外的附加信息
壓入虛擬機(jī)棧
中。
這也就解釋了為什么遞歸過多會導(dǎo)致StackOverflowError
酒请,因?yàn)闀粩嗟耐鶙V袎喝?code>方法返回地址骡技。
這部分是在線程執(zhí)行方法時生成,故也可以知是線程私有的。
擴(kuò)展:程序計數(shù)器存儲當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器布朦。指向下一條要執(zhí)行的指令囤萤。因此也是線程私有的。
JVM內(nèi)存結(jié)構(gòu)
堆
堆的作用是存放對象實(shí)例
和數(shù)組
是趴。從結(jié)構(gòu)上來分涛舍,可以分為新生代
和老年代
。而新生代
又可以分為Eden 空間
唆途、From Survivor 空間(s0)
富雅、To Survivor 空間(s1)
。 所有新生成的對象首先都是放在新生代的肛搬。需要注意没佑,Survivor
的兩個區(qū)是對稱的,沒先后關(guān)系滚婉,所以同一個區(qū)中可能同時存在從Eden
復(fù)制過來的對象图筹,和從前一個Survivor
復(fù)制過來的對象,而復(fù)制到老年代的只有從第一個Survivor
區(qū)過來的對象让腹。而且远剩,Survivor
區(qū)總有一個是空的。
如圖:-Xms
設(shè)置堆的最小空間大小骇窍。-Xmx
設(shè)置堆的最大空間大小瓜晤。-XX:NewSize
設(shè)置新生代最小空間大小。-XX:MaxNewSize
設(shè)置新生代最小空間大小腹纳。
方法區(qū)
方法區(qū)(Method Area)與Java 堆一樣痢掠,是各個線程共享的內(nèi)存區(qū)域,也有人把方法區(qū)稱為“永久代”(Permanent Generation)嘲恍,在Java8中永生代徹底消失了足画。
如圖:-XX:PermSize
設(shè)置最小空間 -XX:MaxPermSize
設(shè)置最大空間。
方法棧
每個線程會有一個私有的棧佃牛。每個線程中方法的調(diào)用又會在本棧中創(chuàng)建一個棧幀淹辞。
如圖:-Xss
控制每個線程棧的大小。
本地方法棧
本地方法棧(Native Method Stacks)與虛擬機(jī)棧所發(fā)揮的作用是非常相似的俘侠,其區(qū)別不過是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java 方法(也就是字節(jié)碼)服務(wù)象缀,而本地方法棧則是為虛擬機(jī)使用到的Native 方法服務(wù)。
如圖:-Xss
控制每個線程的大小爷速。