勿在流沙築高臺,出來混早晚要還的。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 */ class StaticLoad { public static int count1; public static int count2 = 0; private static StaticLoad staticLoad = new StaticLoad(); 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); } }
示例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類型的過程就是加載機制。
類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的生命週期包括了七個階段:
其中驗證、準備、解析三個部分統稱連接!本篇也只會介紹到初始化,後面的週期在後面文章在作介紹。
加載、驗證、準備、初始化和卸載這五個階段順序是肯定的,類的加載過程必須按照這種順序來進行,而解析階段不必定;它在某些狀況下能夠在初始化以後再開始,這是爲了運行時動態綁定特性。值得注意的是:這些階段一般都是互相交叉的混合式進行的,一般會在一個階段執行的過程當中調用或激活另一個階段。
什麼狀況下須要開始類加載的第一個階段:加載。 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的信息:類的定義或者結構)放入方法區 (內存)。
一圖說明:
只有二進制文件載入成功了,才能進行下面的階段!
驗證就是字面意思,以前也提供JVM實際上是有一套本身的規範,因此加載到JVM中數據是須要進行驗證的。
驗證是連接階段的第一步,這一步主要的目的是確保class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身安全。
驗證階段主要包括四個檢驗過程:文件格式驗證、元數據驗證、字節碼驗證和符號引用驗證。
1.文件格式驗證
驗證class文件格式規範,例如: class文件是否已魔術0xCAFEBABE開頭 , 主、次版本號是否在當前虛擬機處理範圍以內等。
2.元數據驗證
這個階段是對字節碼描述的信息進行語義分析,以保證起描述的信息符合java語言規範要求。驗證點可能包括:這個類是否有父類(除了java.lang.Object以外,全部的類都應當有父類)、這個類是否繼承了不容許被繼承的類(被final修飾的)、若是這個類的父類是抽象類,是否實現了起父類或接口中要求實現的全部方法。
3.字節碼驗證
進行數據流和控制流分析,這個階段對類的方法體進行校驗分析,這個階段的任務是保證被校驗類的方法在運行時不會作出危害虛擬機安全的行爲。如:保證訪法體中的類型轉換有效,例如能夠把一個子類對象賦值給父類數據類型,這是安全的,但不能把一個父類對象賦值給子類數據類型、保證跳轉命令不會跳轉到方法體之外的字節碼命令上。
4.符號引用驗證
符號引用中經過字符串描述的全限定名是否能找到對應的類、符號引用類中的類,字段和方法的訪問性(private、protected、public、default)是否可被當前類訪問。
準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中進行分配。這個階段中有兩個容易產生混淆的知識點:
第一:這時候進行內存分配的僅包括類變量(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,則在準備階段就會賦值爲設置的值了,不然只是設置爲零值(也能夠認爲默認值)。
解析階段是虛擬機常量池內的符號引用替換爲直接引用的過程。
符號引用:符號引用是一組符號來描述所引用的目標對象,符號能夠是任何形式的字面量,只要使用時能無歧義地定位到目標便可。符號引用與虛擬機實現的內存佈局無關,引用的目標對象並不必定已經加載到內存中。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 | |
解析的整個階段在虛擬機中仍是比較複雜的,遠比上面介紹的複雜的多,可是不少特別細節的東西咱們能夠暫時先忽略,先有個大概的認識和了解以後有時間在慢慢深刻了。
小總結:
驗證:確保被加載的類的正確性
準備:爲 類的 靜態變量 分配內存,並將其初始化爲默認值
解析:把類中的符號引用轉換爲直接引用
類初始階段是類加載過程的最後一步,在上面提到的類加載過程當中,除了加載階段用戶應用程序能夠經過自定義類加載器參與以外,其他的動做所有由虛擬機主導和控制。初始化階段,是真正開始執行類中定義的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內存模型對比總結(三)
謝謝你的閱讀,若是您以爲這篇博文對你有幫助,請點贊或者喜歡,讓更多的人看到!祝你天天開心愉快!
無論作什麼,只要堅持下去就會看到不同!在路上,不卑不亢!