其他文章
【Java】深入理解Java虛擬機(jī)1——內(nèi)存區(qū)域以及OOM類型:http://www.itdecent.cn/p/65c91ba4006e
【Java】深入理解Java虛擬機(jī)2——判斷對象是否存活和引用:http://www.itdecent.cn/p/67c24aa93c03
【Java】深入理解Java虛擬機(jī)3——垃圾收集算法:http://www.itdecent.cn/p/362407886236
【Java】深入理解Java虛擬機(jī)4——內(nèi)存分配與回收策略:http://www.itdecent.cn/p/e21f5d5c4f42
【Java】深入理解Java虛擬機(jī)5——類的加載過程:http://www.itdecent.cn/p/931ef115d48e
【Java】深入理解Java虛擬機(jī)6——類的加載器及雙親委派:http://www.itdecent.cn/p/2f33eca93a4f
概述
虛擬機(jī)把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的Java類型,這就是虛擬機(jī)的類加載機(jī)制。
在Java語言里面,類型的加載、連接和初始化過程都是在程序運(yùn)行期間完成的,這種策略雖然會(huì)令類加載時(shí)稍微增加一些性能開銷,但是會(huì)為Java應(yīng)用程序提供高度的靈活性,Java里天生可以動(dòng)態(tài)擴(kuò)展的語言特性就是依賴運(yùn)行期動(dòng)態(tài)加載和動(dòng)態(tài)連接這個(gè)特點(diǎn)實(shí)現(xiàn)的。
類加載的時(shí)機(jī)
類從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個(gè)生命周期包括:加載(Loading)、驗(yàn)證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個(gè)階段。其中驗(yàn)證、準(zhǔn)備、解析3個(gè)部分統(tǒng)稱為連接(Linking),

注:加載、驗(yàn)證、準(zhǔn)備、初始化和卸載這5個(gè)階段的順序是確定的,類的加載過程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之后再開始,這是為了支持Java語言的運(yùn)行時(shí)綁定(也稱為動(dòng)態(tài)綁定或晚期綁定)。
類的加載
在加載階段,虛擬機(jī)需要完成以下3件事情:
1.通過一個(gè)類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流。注:Class字節(jié)流可以從zip包中讀取,從網(wǎng)絡(luò)獲取,運(yùn)行時(shí)計(jì)算生成(eg:動(dòng)態(tài)代理),數(shù)據(jù)庫讀取等等...
2.將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。
3.在內(nèi)存中生成一個(gè)代表這個(gè)類的java.lang.Class對象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問入口。
加載階段中獲取類的二進(jìn)制字節(jié)流的動(dòng)作是開發(fā)人員可控性最強(qiáng)的,因?yàn)榧虞d階段既可以使用系統(tǒng)提供的引導(dǎo)類加載器來完成,也可以由用戶自定義的類加載器去完成,開發(fā)人員可以通過定義自己的類加載器去控制字節(jié)流的獲取方式(即重寫一個(gè)類加載器的loadClass()方法)。 對于數(shù)組類而言,情況就有所不同,數(shù)組類本身不通過類加載器創(chuàng)建,它是由Java虛擬機(jī)直接創(chuàng)建的。但數(shù)組類與類加載器仍然有很密切的關(guān)系,因?yàn)閿?shù)組類的元素類型(ElementType,指的是數(shù)組去掉所有維度的類型)最終是要靠類加載器去創(chuàng)建
類的驗(yàn)證
驗(yàn)證是連接階段的第一步,這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全。
Class文件并不一定要求用Java源碼編譯而來,可以使用任何途徑產(chǎn)生,甚至包括用十六進(jìn)制編輯器直接編寫來產(chǎn)生Class文件。虛擬機(jī)如果不檢查輸入的字節(jié)流,對其完全信任的話,很可能會(huì)因?yàn)檩d入了有害的字節(jié)流而導(dǎo)致系統(tǒng)崩潰,所以驗(yàn)證是虛擬機(jī)對自身保護(hù)的一項(xiàng)重要工作。
驗(yàn)證階段大致上會(huì)完成下面4個(gè)階段的檢驗(yàn)動(dòng)作:文件格式驗(yàn)證、元數(shù)據(jù)驗(yàn)證、字節(jié)碼驗(yàn)證、符號引用驗(yàn)證。
- 文件格式驗(yàn)證
第一階段要驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機(jī)處理。 - 元數(shù)據(jù)驗(yàn)證
第二階段是對字節(jié)碼描述的信息進(jìn)行語義分析,以保證其描述的信息符合Java語言規(guī)范的要求,這個(gè)階段可能包括的驗(yàn)證點(diǎn)如下:
- 這個(gè)類是否有父類(除了java.lang.Object之外,所有的類都應(yīng)當(dāng)有父類)。
- 這個(gè)類的父類是否繼承了不允許被繼承的類(被final修飾的類)。
- 如果這個(gè)類不是抽象類,是否實(shí)現(xiàn)了其父類或接口之中要求實(shí)現(xiàn)的所有方法
- 類中的字段、方法是否與父類產(chǎn)生矛盾(例如覆蓋了父類的final字段,或者出現(xiàn)不符合規(guī)則的方法重載,例如方法參數(shù)都一致,但返回值類型卻不同等)。
......
- 字節(jié)碼驗(yàn)證
第三階段是整個(gè)驗(yàn)證過程中最復(fù)雜的一個(gè)階段,主要目的是通過數(shù)據(jù)流和控制流分析,確定程序語義是合法的、符合邏輯的。在第二階段對元數(shù)據(jù)信息中的數(shù)據(jù)類型做完校驗(yàn)后,這個(gè)階段將對類的方法體進(jìn)行校驗(yàn)分析,保證被校驗(yàn)類的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)安全的事件,例如:
- 保證跳轉(zhuǎn)指令不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上
- 保證方法體中的類型轉(zhuǎn)換是有效的,例如可以把一個(gè)子類對象賦值給父類數(shù)據(jù)類型,這是安全的,但是把父類對象賦值給子類數(shù)據(jù)類型,甚至把對象賦值給與它毫無繼承關(guān)系、完全不相干的一個(gè)數(shù)據(jù)類型,則是危險(xiǎn)和不合法的。
.......
- 符號引用驗(yàn)證
最后一個(gè)階段的校驗(yàn)發(fā)生在虛擬機(jī)將符號引用轉(zhuǎn)化為直接引用的時(shí)候,這個(gè)轉(zhuǎn)化動(dòng)作將在連接的第三階段——解析階段中發(fā)生。符號引用驗(yàn)證可以看做是對類自身以外(常量池中的各種符號引用)的信息進(jìn)行匹配性校驗(yàn),通常需要校驗(yàn)下列內(nèi)容:
- 符號引用中通過字符串描述的全限定名是否能找到對應(yīng)的類。
- 符號引用中的類、字段、方法的訪問性(private、protected、public、default)是否可被當(dāng)前類訪問。
......
類的準(zhǔn)備
準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配。這個(gè)階段中有兩個(gè)容易產(chǎn)生混淆的概念需要強(qiáng)調(diào)一下,首先,這時(shí)候進(jìn)行內(nèi)存分配的僅包括類變量(被static修飾的變量),而不包括實(shí)例變量,實(shí)例變量將會(huì)在對象實(shí)例化時(shí)隨著對象一起分配在Java堆中。其次,這里所說的初始值“通常情況”下是數(shù)據(jù)類型的零值,假設(shè)一個(gè)類變量的定義為:
public static int value=123;
那變量value在準(zhǔn)備階段過后的初始值為0而不是123,因?yàn)檫@時(shí)候尚未開始執(zhí)行任何Java方法,而把value賦值為123的putstatic指令是程序被編譯后,存放于類構(gòu)造器<clinit>()方法之中,所以把value賦值為123的動(dòng)作將在初始化階段才會(huì)執(zhí)行。
類的解析
解析階段是虛擬機(jī)將常量池內(nèi)的符號引用替換為直接引用的過程。包括類或接口的解析、字段解析、類方法解析、接口方法解析。
符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標(biāo),符號可以是任何形式的字面量,只要使用時(shí)能無歧義地定位到目標(biāo)即可。符號引用與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局無關(guān),引用的目標(biāo)并不一定已經(jīng)加載到內(nèi)存中。各種虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局可以各不相同,但是它們能接受的符號引用必須都是一致的,因?yàn)榉栆玫淖置媪啃问矫鞔_定義在Java虛擬機(jī)規(guī)范的Class文件格式中。
直接引用(Direct References):直接引用可以是直接指向目標(biāo)的指針、相對偏移量或是一個(gè)能間接定位到目標(biāo)的句柄。直接引用是和虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局相關(guān)的,同一個(gè)符號引用在不同虛擬機(jī)實(shí)例上翻譯出來的直接引用一般不會(huì)相同。如果有了直接引用,那引用的目標(biāo)必定已經(jīng)在內(nèi)存中存在。
類的初始化
在準(zhǔn)備階段,變量已經(jīng)賦過一次系統(tǒng)要求的初始值,而在初始化階段,則根據(jù)程序員通過程序制定的主觀計(jì)劃去初始化類變量和其他資源,或者可以從另外一個(gè)角度來表達(dá):初始化階段是執(zhí)行類構(gòu)造器<clinit>()方法的過程。
- <clinit>()方法是由編譯器自動(dòng)收集類中的所有類變量的賦值動(dòng)作和靜態(tài)語句塊(static{}塊)中的語句合并產(chǎn)生的,編譯器收集的順序是由語句在源文件中出現(xiàn)的順序所決定的,靜態(tài)語句塊中只能訪問到定義在態(tài)語句塊之前的變量,定義在它之后的變量,在前面的靜態(tài)語句塊可以賦值,但是不能訪問
public class Test {
static {
i = 0;//給變量賦值可以正常編譯通過
System.out.print(i);//這句編譯器會(huì)提示"非法向前引用"
}
static int i = 1;
}
- <clinit>()方法與類的構(gòu)造函數(shù)(或者說實(shí)例構(gòu)造器<init>()方法)不同,它不
需要顯式地調(diào)用父類構(gòu)造器,虛擬機(jī)會(huì)保證在子類的<clinit>()方法執(zhí)行之前,父類的<
clinit>()方法已經(jīng)執(zhí)行完畢。因此在虛擬機(jī)中第一個(gè)被執(zhí)行的<clinit>()方法的類肯定
是java.lang.Object。 - 由于父類的<clinit>()方法先執(zhí)行,也就意味著父類中定義的靜態(tài)語句塊要優(yōu)先于子
類的變量賦值操作,如下代碼,字段B的值將會(huì)是2而不是1。
static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(
String[] args)
{
System.out.println(Sub.B);
}
- <clinit>()方法對于類或接口來說并不是必需的,如果一個(gè)類中沒有靜態(tài)語句塊,也沒有對變量的賦值操作,那么編譯器可以不為這個(gè)類生成<clinit>()方法。
- 接口中不能使用靜態(tài)語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會(huì)生成<clinit>()方法。但接口與類不同的是,執(zhí)行接口的<clinit>()方法不需要先執(zhí)行父接口的<clinit>()方法。只有當(dāng)父接口中定義的變量使用時(shí),父接口才會(huì)初始化。另外,接口的實(shí)現(xiàn)類在初始化時(shí)也一樣不會(huì)執(zhí)行接口的<clinit>()方法。
注:當(dāng)一個(gè)類從被JVM裝載開始,各種代碼的執(zhí)行順序大致如下: 被JVM裝載->執(zhí)行父類的相關(guān)代碼->如果有靜態(tài)初始化,先執(zhí)行靜態(tài)初始化,且只執(zhí)行一次,以后即使有該類實(shí)例化,也不會(huì)再執(zhí)行->如果有靜態(tài)代碼塊,以與靜態(tài)初始化一樣的方式執(zhí)行->如果有new語句帶來的實(shí)例化,先為成員變量分配空間,并綁定參數(shù)列表,隱式或顯式執(zhí)行super(),即父類的構(gòu)造方法,->執(zhí)行非靜態(tài)代碼塊-〉執(zhí)行本類的構(gòu)造函數(shù)-〉執(zhí)行其他代碼
類在什么情況下初始化
對于初始化階段,虛擬機(jī)規(guī)范則是嚴(yán)格規(guī)定了有且只有5種情況必須立即對類進(jìn)行“初始化”(而加載、驗(yàn)證、準(zhǔn)備自然需要在此之前開始):
1.遇到new、getstatic、putstatic或invokestatic這4條字節(jié)碼指令時(shí),如果類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關(guān)鍵字實(shí)例化對象的時(shí)候、讀取或設(shè)置一個(gè)類的靜態(tài)字段(被final修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時(shí)候,以及調(diào)用一個(gè)類的靜態(tài)方法的時(shí)候。
2.使用java.lang.reflect包的方法對類進(jìn)行反射調(diào)用的時(shí)候,如果類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。
3.當(dāng)初始化一個(gè)類的時(shí)候,如果發(fā)現(xiàn)其父類還沒有進(jìn)行過初始化,則需要先觸發(fā)其父類的初始化。
4.當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶需要指定一個(gè)要執(zhí)行的主類(包含main()方法的那個(gè)類),虛擬機(jī)會(huì)先初始化這個(gè)主類。
5.當(dāng)使用JDK 1.7的動(dòng)態(tài)語言支持時(shí),如果一個(gè)java.lang.invoke.MethodHandle實(shí)例最后的解析結(jié)果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個(gè)方法句柄所對應(yīng)的類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。
被動(dòng)引用
引用類的方式都不會(huì)觸發(fā)初始化,稱為被動(dòng)引用。下面舉幾個(gè)例子:
例一:
package org.fenixsoft.classloading;
/**
* 被動(dòng)使用類字段演示一:
* 通過子類引用父類的靜態(tài)字段,不會(huì)導(dǎo)致子類初始化
**/
public class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
}
public class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}
/**
* 非主動(dòng)使用類字段演示
**/
public class NotInitialization {
public static void main(
String[] args)
{
System.out.println(SubClass.value);
}
}
上述代碼運(yùn)行之后,只會(huì)輸出“SuperClass init!”,而不會(huì)輸出“SubClass init!”。對于靜態(tài)字段,只有直接定義這個(gè)字段的類才會(huì)被初始化,因此通過其子類來引用父類中定義的靜態(tài)字段,只會(huì)觸發(fā)父類的初始化而不會(huì)觸發(fā)子類的初始化。
例二:
package org.fenixsoft.classloading;
/**
* 被動(dòng)使用類字段演示二:
* 通過數(shù)組定義來引用類,不會(huì)觸發(fā)此類的初始化
**/
public class NotInitialization {
public static void main(
String[] args)
{
SuperClass[] sca = new SuperClass[10];
}
}
運(yùn)行之后發(fā)現(xiàn)沒有輸出“SuperClass init!”,說明并沒有觸發(fā)類org.fenixsoft.classloading.SuperClass的初始化階段。但是這段代碼里面觸發(fā)了另外一個(gè)名為“[Lorg.fenixsoft.classloading.SuperClass”的類的初始化階段,對于用戶代碼來說,這并不是一個(gè)合法的類名稱,它是一個(gè)由虛擬機(jī)自動(dòng)生成的、直接繼承于java.lang.Object的子類,創(chuàng)建動(dòng)作由字節(jié)碼指令newarray觸發(fā)。
例三:
package org.fenixsoft.classloading;
/**
* 被動(dòng)使用類字段演示三:
* 常量在編譯階段會(huì)存入調(diào)用類的常量池中,本質(zhì)上并沒有直接引用到定義常量的類,因此不會(huì)觸發(fā)定義常量的類的初始化。
**/
public class ConstClass {
static {
System.out.println("ConstClass init!");
}
public static final String HELLOWORLD = "hello world";
}
/**
* 非主動(dòng)使用類字段演示
**/
public class NotInitialization {
public static void main(
String[] args)
{
System.out.println(ConstClass.HELLOWORLD);
}
}
代碼運(yùn)行之后,也沒有輸出“ConstClass init!”,這是因?yàn)殡m然在Java源碼中引用了ConstClass類中的常量HELLOWORLD,但其實(shí)在編譯階段通過常量傳播優(yōu)化,已經(jīng)將此常量的值“hello world”存儲(chǔ)到了NotInitialization類的常量池中,以后NotInitialization對常量ConstClass.HELLOWORLD的引用實(shí)際都被轉(zhuǎn)化為NotInitialization類對自身常量池的引用了。也就是說,實(shí)際上NotInitialization的Class文件之中并沒有ConstClass類的符號引用入口,這兩個(gè)類在編譯成Class之后就不存在任何聯(lián)系了。
錯(cuò)誤不足之處或相關(guān)建議歡迎大家評論指出,謝謝!如果覺得內(nèi)容可以的話麻煩喜歡(?)一下。您的支持是我最大的動(dòng)力。