Java内存管理-掌握虚拟机类加载机制(四)
勿在流沙筑高臺,出來混遲早要還的。
做一個積極的人
編碼、改bug、提升自己
我有一個樂園,面向編程,春暖花開!
上一篇介紹了整個JVM運行時的區域,以及簡單對比了JDK7和JDK8中JVM運行時區域的一些變化,也順便總結了哪些區域會發生異常(內存溢出)問題。前一篇的話還是非常重要,請大家務必要多多閱讀學習和掌握,因為這些基礎的知識點會關聯后續的一系列問題內容,如果前面沒有先有一定的基礎知識儲備,到后面的一些篇章介紹你可能會蒙B的,可能會有一種what the fuck的感覺,這TMD到底在說什么。所以墻裂建議先好好閱讀前面的博文。
本章介紹JVM中類加載的機制,通過類加載的機制的學習我們可以知道類加載的整個流程是什么,讓我們知其然也能知其所以然。
知識地圖:
一、思考:簡單示例
下面代碼是兩個簡單的示例,請先思考30秒,最初回答,輸出的結果到底是什么?
/*** 示例1*/ class StaticLoad {private static StaticLoad staticLoad = new StaticLoad(); public static int count1; public static int count2 = 0; private StaticLoad() { count1++; count2++; } public static StaticLoad getStaticLoadInstance(){ return staticLoad; } } public class TestStaticLoadDemo { public static void main(String[] args) { StaticLoad staticLoad = StaticLoad.getStaticLoadInstance(); System.out.println("count1 = " + staticLoad.count1); System.out.println("count2 = " + staticLoad.count2); } }示例1打印結果:
-
A :1 和 0
-
B :1 和 1
示例2打印結果:
-
A :1 和 0
-
B :1 和 1
兩個例子唯一的區別下面這行代碼的順序!
private static StaticLoad staticLoad = new StaticLoad();如果你能夠選擇出正確結果,并完全知道答案。那今天這一篇文章就不用看了,如果你在兩個答案之間猶豫,那么請繼續往下看,好好閱讀完本篇,我相信你會有答案的。
二、類加載的過程
在來簡單回顧一下JVM運行流程, java源文件程序 使用 javac 進行編譯 ,編譯字節碼 class文件!
JVM 在指定位置讀取class文件然后加載到內存中(字節碼解析成二進制的代碼、指令)。
JVM基本結構:
類加載器、執行引擎、運行時數據區、本地接口。
Class FIle --->?ClassLoader?---> 運行時數據區---->執行引擎,需要調用本地庫接口--->本地方法庫。
本文主要是在ClassLoader 這一個點做做介紹,慢慢的我們會把這一整套都串聯起來。
思考:類加載機制是什么?
JVM把編譯好的class文件加載的內存,并對數據進行校驗、轉換解析和初始化,最終形成JVM可以直接使用的Java類型的過程就是加載機制。
類從被加載到虛擬機內存中開始,到卸載出內存為止,它的生命周期包括了七個階段:
- 加載(Loading)
- 驗證(Verification)
- 準備(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 使用(Using)
- 卸載(Unloading)
其中驗證、準備、解析三個部分統稱鏈接!本篇也只會介紹到初始化,后面的周期在后面文章在做介紹。
加載、驗證、準備、初始化和卸載這五個階段順序是確定的,類的加載過程必須按照這種順序來進行,而解析階段不一定;它在某些情況下可以在初始化之后再開始,這是為了運行時動態綁定特性。值得注意的是:這些階段通常都是互相交叉的混合式進行的,通常會在一個階段執行的過程中調用或激活另外一個階段。
1、加載階段
什么情況下需要開始類加載的第一個階段:加載。 JAVA虛擬機規范并沒有進行強制約束,交給虛擬機的具體實現自由把握。
加載階段是“類加載”過程中的一個階段,這個階段通常也被稱作“裝載”,在加載階段,虛擬機主要完成以下3件事情:
1.通過“類全名”來獲取定義此類的二進制字節流
2.將字節流所代表的靜態存儲結構轉換為方法區的運行時數據結構
3.在java堆中生成一個代表這個類的java.lang.Class對象,作為方法區這些數據的訪問入口(所以我們能夠通過低調用類.getClass() )
注:如果不理解,建議先背下來,記住!
虛擬機規范的這3點要求其實并不規范,比如:通過“類全名”來獲取定義此類的二進制字節流”并沒有指明二進制流必須要從一個本地class文件中獲取,準確地說是根本沒有指明要從哪里獲取及怎樣獲取(記住這個對后面我們實現自定義類加載有幫助)。許多java技術也玩出其他花樣:
-
從Zip包中讀取,這很常見,最終成為日后JAR、EAR、WAR格式的基礎。
-
從網絡獲取(URLClassLoader),下載.class文件
-
運行時計算生成,這種場景使用的最多的就是動態代理技術,在java.lang.reflect.Proxy中,就是用ProxyGenerator.generateProxyClass來為特定接口生成$Prxoy的代理類的二進制字節流。
-
由Java源文件動態編譯為.class,最常用方式!
-
從數據庫中讀取.class文件,這種場景相對少見。
-
……
相對于類加載過程的其他階段,加載階段(準備地說,是加載階段中獲取類的二進制字節流的動作)是開發期可控性最強的階段,因為加載階段可以使用系統提供的類加載器(ClassLoader)來完成,也可以由用戶自定義的類加載器完成,開發人員可以通過定義自己的類加載器去控制字節流的獲取方式。
加載階段完成后,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區之中,方法區中的數據存儲格式有虛擬機實現自行定義,虛擬機并未規定此區域的具體數據結構。然后在java堆中實例化一個java.lang.Class類的對象,這個對象作為程序訪問方法區中的這些類型數據的外部接口。加載階段與鏈接階段的部分內容(如一部分字節碼文件格式驗證動作)是交叉進行的,加載階段尚未完成,鏈接階段可能已經開始,但這些夾在加載階段之中進行的動作,仍然屬于鏈接階段的內容,這兩個階段的開始時間仍然保持著固定的先后順序。
如果上面那么多記不住:?請一定記住這句:?加載階段也就是查找獲取類的二進制數據(磁盤或者網絡)動作,將類的數據(Class的信息:類的定義或者結構)放入方法區 (內存)。
一圖說明:
2、連接階段(驗證、準備、解析)
只有二進制文件載入成功了,才能進行下面的階段!
2.1 驗證
驗證就是字面意思,之前也提供JVM其實是有一套自己的規范,所以加載到JVM中數據是需要進行驗證的。
驗證是鏈接階段的第一步,這一步主要的目的是確保class文件的字節流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身安全。
驗證階段主要包括四個檢驗過程:文件格式驗證、元數據驗證、字節碼驗證和符號引用驗證。
-
1.文件格式驗證
驗證class文件格式規范,例如: class文件是否已魔術0xCAFEBABE開頭 , 主、次版本號是否在當前虛擬機處理范圍之內等。
-
2.元數據驗證
這個階段是對字節碼描述的信息進行語義分析,以保證起描述的信息符合java語言規范要求。驗證點可能包括:這個類是否有父類(除了java.lang.Object之外,所有的類都應當有父類)、這個類是否繼承了不允許被繼承的類(被final修飾的)、如果這個類的父類是抽象類,是否實現了起父類或接口中要求實現的所有方法。
-
3.字節碼驗證
進行數據流和控制流分析,這個階段對類的方法體進行校驗分析,這個階段的任務是保證被校驗類的方法在運行時不會做出危害虛擬機安全的行為。如:保證訪法體中的類型轉換有效,例如可以把一個子類對象賦值給父類數據類型,這是安全的,但不能把一個父類對象賦值給子類數據類型、保證跳轉命令不會跳轉到方法體以外的字節碼命令上。
-
4.符號引用驗證
符號引用中通過字符串描述的全限定名是否能找到對應的類、符號引用類中的類,字段和方法的訪問性(private、protected、public、default)是否可被當前類訪問。
2.2 準備
準備階段是正式為類變量分配內存并設置類變量初始值的階段,這些內存都將在方法區中進行分配。這個階段中有兩個容易產生混淆的知識點:
第一:這時候進行內存分配的僅包括類變量(static 修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨著對象一起分配在java堆中。
第二:這里所說的初始值“通常情況”下是數據類型的零值(默認值),假設一個類變量定義為:
public static int value = 123;首先為int類型的靜態變量value分配4個字節的內存空間,并賦予變量value的初始值為0而不是123。因為這時候尚未開始執行任何Java方法,而把value賦值為123的putstatic指令是程序被編譯后,存放于類構造器()方法之中,所以把value賦值為123的動作將在初始化階段才會被執行。
基本數據類型的零值:
| 數據類型 | 零值 | 數據類型 | 零值 | | -------- | -------- | --------- | ----- | | int | 0 | boolean | false | | long | 0L | float | 0.0f | | short | (short)0 | double | 0.0d | | char | '\u0000' | reference | null | | byte | (byte)0 | | |
上面所說的“通常情況”下初始值是零值,那相對于一些特殊的情況,如果類字段的字段屬性表中存在ConstantValue屬性,那在準備階段變量value就會被初始化為ConstantValue屬性所指定的值,假設上面類變量value定義為:
public static final int value = 123; // 注意 final編譯時javac將會為value生成ConstantValue屬性,在準備階段虛擬機就會根據ConstantValue的設置將value設置為123。
上面說了這么一長串意思是: 如果一個被static 修飾的變量加了final,則在準備階段就會賦值為設置的值了,否則只是設置為零值(也可以認為默認值)。
2.3 解析
解析階段是虛擬機常量池內的符號引用替換為直接引用的過程。
符號引用:符號引用是一組符號來描述所引用的目標對象,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現的內存布局無關,引用的目標對象并不一定已經加載到內存中。Java虛擬機明確在Class文件格式中定義的符號引用的字面量形式。
直接引用:直接引用可以是直接指向目標對象的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機內存布局實現相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同,如果有了直接引用,那引用的目標必定已經在內存中存在。
這里重點理解加粗的兩個名稱,我的理解是由虛指變為實指,舉個不是很恰當的例子,方便理解:
玩斗地主: 每一局輸的人手里的一張牌代表 一塊錢,此時一張牌虛指(符號引用)一塊錢。 等一局游戲結束,將牌兌換為錢(直接引用)的時候,那就是實指了。在解析的階段,解析動作主要針對7類符號引用進行,它們的名稱以及對于常量池中的常量類型和解析報錯信息如下:
| 解析動作 | 符號引用 | 解析可能的報錯 | | ---------- | ------------------------------- | ----------------------------------------------------------- | | 類或接口 | CONSTANTClassInfo | java.land.IllegalAccessError | | 字段 | CONSTANTFieldrefInfo | java.land.IllegalAccessError 或 java.land.NoSuchFieldError | | 類方法 | CONSTANTMethodefInfo | java.land.IllegalAccessError 或 java.land.NoSuchMethodError | | 接口方法 | CONSTANTInterfaceMethoderInfo | java.land.IllegalAccessError 或 java.land.NoSuchMethodError | | 方法類型 | CONSTANTMethodTypeInfo | | | 方法句柄 | CONSTANTMethodhandlerInfo | | | 調用限定符 | CONSTANTInvokeDynamicInfo | |
解析的整個階段在虛擬機中還是比較復雜的,遠比上面介紹的復雜的多,但是很多特別細節的東西我們可以暫時先忽略,先有個大概的認識和了解之后有時間在慢慢深入了。
小總結:
驗證:確保被加載的類的正確性
準備:為 類的?靜態變量?分配內存,并將其初始化為默認值
解析:把類中的符號引用轉換為直接引用
3、初始化階段
類初始階段是類加載過程的最后一步,在上面提到的類加載過程中,除了加載階段用戶應用程序可以通過自定義類加載器參與之外,其余的動作全部由虛擬機主導和控制。初始化階段,是真正開始執行類中定義的Java程序代碼(或者說是字節碼)。
在準備階段,變量已經賦值過一次系統要求的初始值(零值),而在初始化階段,則根據程序員通過程序制定的主觀計劃去初始化類變量和其他資源。(或者從另一個角度表達:初始化階段是執行類構造器<clinit>()方法的過程。)
tips:
類構造器 和 構造方法有什么關系?
類構造器:構造class對象,類對象;構造方法:實例化對象!先要執行類構造器才能執行構造方法!也就是說先要有這個類,才能對類進行實例化。
在類構造器中構造器中先執行static變量,在執行static{}塊,有多個static變量的話按照代碼順序執行。,如下圖例子,順序不對,編譯都不能通過!
在初始化階段,虛擬機規范則是嚴格規定有且只有5種情況必須立即對類進行“初始化”(而加載、驗證、準備、解析要在此之前執行),5種情況分別是:
第一:遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時。如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關鍵字實例化對象、讀取或設置一個類的靜態字段(被final修飾、已在編譯器把結果放入常量池的靜態字段除外)的時候,以及調用類的靜態方法的時候。
備注:靜態屬性和靜態方法,對應的指令為getstatic、putstatic、invokestatic。可能你對這些字節碼指令有點蒙B,沒有關系,可暫時忽略,記住一個new就行。
第二:使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
第三:當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
第四:當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個類。
第五:當使用JDK1.7的動態語言支持時,如果一個java.invoke.MethodHandle 實例最后解析結果REFgetStatic、REFputStatic、REF_invokeStatic 的方法句柄。并且這個方法句柄所對應的類沒有初始化,則需要先觸發其初始化。
<clinit>()方法相關的內容比較多,只需要記住一點:虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確加鎖和同步,如果多個線程同時去初始化一個類,那么只會有一個線程執行這個類的<clinit>()方法,其他線程都需要阻塞等待,直到活動線程執行<clinit>()方法完畢。(一個類在虛擬機中只會被加載一次,是什么機制保證只能被加載一次,后面文章進行講解!)
三、分析示例和簡單總結
上面的內容全部看完之后,我想你應該就知道最開始的簡單示例的答案了。
示例1答案就是: A
示例2答案就是: B
示例1具體分析:首先指定一個要執行的主類(包含main()方法)也就是TestStaticLoadDemo,執行main()方法運行StaticLoad.getStaticLoadInstance(),調用StaticLoad類的靜態方法的時候,開始加載StaticLoad。
第一步:給所有靜態變量分配內存,并賦予零值。如下
public class TestStaticLoadDemo {public static void main(String[] args) { StaticLoad staticLoad = StaticLoad.getStaticLoadInstance(); // ① System.out.println("count1 = " + staticLoad.count1); System.out.println("count2 = " + staticLoad.count2); } } // ② public static StaticLoad getStaticLoadInstance(){ return staticLoad; } // ③ private static StaticLoad staticLoad = null; public static int count1 = 0; public static int count2 = 0; // count2 = 0 并不是代碼中的count2 = 0 的含義,是賦予的默認零值!第二步:賦值完進行初始化,把右邊的值賦左邊,static執行順序從上到下,如下
private static StaticLoad staticLoad = new StaticLoad();// ① public static int count1; // ⑤ public static int count2 = 0; //⑥ private StaticLoad() { // ② count1++; //③ count2++; //④ }第三步:賦值完整,打印結果
public class TestStaticLoadDemo {public static void main(String[] args) { StaticLoad staticLoad = StaticLoad.getStaticLoadInstance(); System.out.println("count1 = " + staticLoad.count1); // ① System.out.println("count2 = " + staticLoad.count2); // ② } }實例2可以按照上面的分析過程自行進行分析,這里就不在分析了。
最后在總結一下本文主要講解的類的生命周期中的三個階段:加載,連接(驗證、準備、解析)、初始化。
參考資料
《深入理解Java虛擬機》
推薦閱讀
Java的線程安全、單例模式、JVM內存結構等知識梳理
Java內存管理-程序運行過程(一)
Java內存管理-初始JVM和JVM啟動流程(二)
Java內存管理-JVM內存模型以及JDK7和JDK內存模型對比總結(三)
謝謝你的閱讀,如果您覺得這篇博文對你有幫助,請點贊或者喜歡,讓更多的人看到!祝你每天開心愉快!
?
不管做什么,只要堅持下去就會看到不一樣!在路上,不卑不亢!
博客首頁 : http://blog.csdn.net/u010648555
轉載于:https://www.cnblogs.com/aflyun/p/10589984.html
總結
以上是生活随笔為你收集整理的Java内存管理-掌握虚拟机类加载机制(四)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Nginx的启动、停止和重启
- 下一篇: [Python]小甲鱼Python视频第