前言
我們知道我們寫的程序經(jīng)過編譯后成為了.class文件,.class文件中描述了類的各種信息,最終都需要加載到虛擬機之后才能運行和使用。而虛擬機如何加載這些.class文件霍狰?.class文件的信息進入到虛擬機后會發(fā)生什么變化?這些都是本文要講的內(nèi)容饰及,文章將會講解加載類加載的每個階段Java虛擬機需要做什么事蔗坯。
類使用的7個階段
類從被加載到虛擬機內(nèi)存中開始,到卸載出內(nèi)存燎含,它的整個生命周期包括:加載(Loading)宾濒、驗證(Verification)、準備(Preparation)屏箍、解析(Resolution)绘梦、初始化(Initiallization)橘忱、使用(Using)和卸載(Unloading)這7個階段。其中驗證卸奉、準備钝诚、解析3個部分統(tǒng)稱為連接(Linking),這七個階段的發(fā)生順序如下圖:
圖中榄棵,加載凝颇、驗證、準備疹鳄、初始化拧略、卸載這5個階段的順序是確定的,類的加載過程必須按照這種順序按部就班地開始尚辑,而解析階段不一定:它在某些情況下可以初始化階段之后在開始辑鲤,這是為了支持Java語言的運行時綁定(也稱為動態(tài)綁定)。接下來講解加載杠茬、驗證月褥、準備、解析瓢喉、初始化五個步驟宁赤,這五個步驟組成了一個完整的類加載過程。使用沒什么好說的栓票,卸載屬于GC的工作决左,在之前GC的文章中已經(jīng)有所提及了。
加載Loading
加載是類加載的第一個階段走贪。有兩種時機會觸發(fā)類加載:
1佛猛、預加載。
虛擬機啟動時加載坠狡,加載的是JAVA_HOME/lib/下的rt.jar下的.class文件继找,這個jar包里面的內(nèi)容是程序運行時非常常常用到的,像java.lang.逃沿、java.util.婴渡、java.io.*等等,因此隨著虛擬機一起加載凯亮。要證明這一點很簡單边臼,寫一個空的main函數(shù),設置虛擬機參數(shù)為"-XX:+TraceClassLoading"來獲取類加載信息假消,運行一下:
[Opened E:\MyEclipse10\Common\binary\com.sun.java.jdk.win32.x86_64_1.6.0.013\jre\lib\rt.jar]
[Loaded java.lang.Object from E:\MyEclipse10\Common\binary\com.sun.java.jdk.win32.x86_64_1.6.0.013\jre\lib\rt.jar]
[Loaded java.io.Serializable from E:\MyEclipse10\Common\binary\com.sun.java.jdk.win32.x86_64_1.6.0.013\jre\lib\rt.jar]
[Loaded java.lang.Comparable from E:\MyEclipse10\Common\binary\com.sun.java.jdk.win32.x86_64_1.6.0.013\jre\lib\rt.jar]
[Loaded java.lang.CharSequence from E:\MyEclipse10\Common\binary\com.sun.java.jdk.win32.x86_64_1.6.0.013\jre\lib\rt.jar]
[Loaded java.lang.String from E:\MyEclipse10\Common\binary\com.sun.java.jdk.win32.x86_64_1.6.0.013\jre\lib\rt.jar]
[Loaded java.lang.reflect.GenericDeclaration from E:\MyEclipse10\Common\binary\com.sun.java.jdk.win32.x86_64_1.6.0.013\jre\lib\rt.jar]
[Loaded java.lang.reflect.Type from E:\MyEclipse10\Common\binary\com.sun.java.jdk.win32.x86_64_1.6.0.013\jre\lib\rt.jar]
[Loaded java.lang.reflect.AnnotatedElement from E:\MyEclipse10\Common\binary\com.sun.java.jdk.win32.x86_64_1.6.0.013\jre\lib\rt.jar]
[Loaded java.lang.Class from E:\MyEclipse10\Common\binary\com.sun.java.jdk.win32.x86_64_1.6.0.013\jre\lib\rt.jar]
[Loaded java.lang.Cloneable from E:\MyEclipse10\Common\binary\com.sun.java.jdk.win32.x86_64_1.6.0.013\jre\lib\rt.jar]
...
2柠并、運行時加載。
虛擬機在用到一個.class文件的時候,會先去內(nèi)存中查看一下這個.class文件有沒有被加載臼予,如果沒有就會按照類的全限定名來加載這個類亿傅。
那么,加載階段做了什么瘟栖,其實加載階段做了有三件事情:
1、獲取.class文件的二進制流
2谅阿、將類信息半哟、靜態(tài)變量、字節(jié)碼签餐、常量這些.class文件中的內(nèi)容放入方法區(qū)中
3寓涨、在內(nèi)存中生成一個代表這個.class文件的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口氯檐。一般這個Class是在堆里的戒良,不過HotSpot虛擬機比較特殊,這個Class對象是放在方法區(qū)中的
虛擬機規(guī)范對這三點的要求并不具體冠摄,因此虛擬機實現(xiàn)與具體應用的靈活度都是相當大的糯崎。例如第一條,根本沒有指明二進制字節(jié)流要從哪里來河泳、怎么來沃呢,因此單單就這一條,就能變出許多花樣來:
- 從zip包中獲取拆挥,這就是以后jar薄霜、ear、war格式的基礎
- 從網(wǎng)絡中獲取纸兔,典型應用就是Applet
- 運行時計算生成惰瓜,典型應用就是動態(tài)代理技術
- 由其他文件生成,典型應用就是JSP汉矿,即由JSP生成對應的.class文件
- 從數(shù)據(jù)庫中讀取崎坊,這種場景比較少見
總而言之,在類加載整個過程中负甸,這部分是對于開發(fā)者來說可控性最強的一個階段流强。
驗證
連接階段的第一步,這一階段的目的是為了確保.class文件的字節(jié)流中包含的信息符合當前虛擬機的要求呻待,并且不會危害虛擬機自身的安全打月。
Java語言本身是相對安全的語言(相對C/C++來說),但是前面說過蚕捉,.class文件未必要從Java源碼編譯而來奏篙,可以使用任何途徑產(chǎn)生,甚至包括用十六進制編輯器直接編寫來產(chǎn)生.class文件。在字節(jié)碼語言層面上秘通,Java代碼至少從語義上是可以表達出來的为严。虛擬機如果不檢查輸入的字節(jié)流,對其完全信任的話肺稀,很可能會因為載入了有害的字節(jié)流而導致系統(tǒng)崩潰第股,所以驗證是虛擬機對自身保護的一項重要工作。
驗證階段將做一下幾個工作话原,具體就不細講了夕吻,這是虛擬機實現(xiàn)層面的問題:
1、文件格式驗證
這個地方要說一點和開發(fā)者相關的繁仁。.class文件的第5~第8個字節(jié)表示的是該.class文件的主次版本號涉馅,驗證的時候會對這4個字節(jié)做一個驗證,高版本的JDK能向下兼容以前版本的.class文件黄虱,但不能運行以后的class文件稚矿,即使文件格式未發(fā)生任何變化,虛擬機也必須拒絕執(zhí)行超過其版本號的.class文件捻浦。舉個具體的例子晤揣,如果一段.java代碼是在JDK1.6下編譯的,那么JDK1.6朱灿、JDK1.7的環(huán)境能運行這個.java代碼生成的.class文件碉渡,但是JDK1.5、JDK1.4乃更低的JDK版本是無法運行這個.java代碼生成的.class文件的母剥。如果運行滞诺,會拋出java.lang.UnsupportedClassVersionError,這個小細節(jié)环疼,務必注意习霹。
2、元數(shù)據(jù)驗證
3炫隶、字節(jié)碼驗證
4淋叶、符號引用驗證
準備
準備階段是正式為類變量分配內(nèi)存并設置其初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中分配伪阶。關于這點煞檩,有兩個地方注意一下:
1、這時候進行內(nèi)存分配的僅僅是類變量(被static修飾的變量)栅贴,而不是實例變量斟湃,實例變量將會在對象實例化的時候隨著對象一起分配在Java堆中
2、這個階段賦初始值的變量指的是那些不被final修飾的static變量檐薯,比如"public static int value = 123;"凝赛,value在準備階段過后是0而不是123注暗,給value賦值為123的動作將在初始化階段才進行;比如"public static final int value = 123;"就不一樣了墓猎,在準備階段捆昏,虛擬機就會給value賦值為123。
各個數(shù)據(jù)類型的零值如下圖:
解析
解析階段是虛擬機將常量池內(nèi)的符號引用替換為直接引用的過程毙沾。來了解一下符號引用和直接引用有什么區(qū)別:
1骗卜、符號引用。
這個其實是屬于編譯原理方面的概念左胞,符號引用包括了下面三類常量:
- 類和接口的全限定名
- 字段的名稱和描述符
- 方法的名稱和描述符
這么說可能不太好理解膨俐,結(jié)合實際看一下,寫一段很簡單的代碼:
package com.xrq.test6;
public class TestMain
{
private static int i;
private double d;
public static void print()
{
}
private boolean trueOrFalse()
{
return false;
}
}
用javap把這段代碼的.class反編譯一下:
Constant pool:
#1 = Class #2 // com/xrq/test6/TestMain
#2 = Utf8 com/xrq/test6/TestMain
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 i
#6 = Utf8 I
#7 = Utf8 d
#8 = Utf8 D
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Methodref #3.#13 // java/lang/Object."<init>":()V
#13 = NameAndType #9:#10 // "<init>":()V
#14 = Utf8 LineNumberTable
#15 = Utf8 LocalVariableTable
#16 = Utf8 this
#17 = Utf8 Lcom/xrq/test6/TestMain;
#18 = Utf8 print
#19 = Utf8 trueOrFalse
#20 = Utf8 ()Z
#21 = Utf8 SourceFile
#22 = Utf8 TestMain.java
看到Constant Pool也就是常量池中有22項內(nèi)容罩句,其中帶"Utf8"的就是符號引用。比如#2敛摘,它的值是"com/xrq/test6/TestMain"门烂,表示的是這個類的全限定名;又比如#5為i兄淫,#6為I屯远,它們是一對的,表示變量時Integer(int)類型的捕虽,名字叫做i慨丐;#6為D、#7為d也是一樣泄私,表示一個Double(double)類型的變量房揭,名字為d;#18晌端、#19表示的都是方法的名字捅暴。
那其實總而言之,符號引用和我們上面講的是一樣的咧纠,是對于類蓬痒、變量、方法的描述漆羔。符號引用和虛擬機的內(nèi)存布局是沒有關系的梧奢,引用的目標未必已經(jīng)加載到內(nèi)存中了。
2演痒、直接引用
直接引用可以是直接指向目標的指針亲轨、相對偏移量或是一個能間接定位到目標的句柄。直接引用是和虛擬機實現(xiàn)的內(nèi)存布局相關的鸟顺,同一個符號引用在不同的虛擬機示例上翻譯出來的直接引用一般不會相同瓶埋。如果有了直接引用,那引用的目標必定已經(jīng)存在在內(nèi)存中了。
初始化
初始化階段是類加載過程的最后一步养筒,初始化階段是真正執(zhí)行類中定義的Java程序代碼(或者說是字節(jié)碼)的過程曾撤。初始化過程是一個執(zhí)行類構(gòu)造器<clinit>()方法的過程,根據(jù)程序員通過程序制定的主觀計劃去初始化類變量和其它資源晕粪。把這句話說白一點挤悉,其實初始化階段做的事就是給static變量賦予用戶指定的值以及執(zhí)行靜態(tài)代碼塊。
注意一下巫湘,虛擬機會保證類的初始化在多線程環(huán)境中被正確地加鎖装悲、同步,即如果多個線程同時去初始化一個類尚氛,那么只會有一個類去執(zhí)行這個類的<clinit>()方法诀诊,其他線程都要阻塞等待,直至活動線程執(zhí)行<clinit>()方法完畢阅嘶。因此如果在一個類的<clinit>()方法中有耗時很長的操作属瓣,就可能造成多個進程阻塞。不過其他線程雖然會阻塞讯柔,但是執(zhí)行<clinit>()方法的那條線程退出<clinit>()方法后抡蛙,其他線程不會再次進入<clinit>()方法了,因為同一個類加載器下魂迄,一個類只會初始化一次粗截。實際應用中這種阻塞往往是比較隱蔽的,要小心捣炬。
Java虛擬機規(guī)范嚴格規(guī)定了有且只有5種場景必須立即對類進行初始化熊昌,這4種場景也稱為對一個類進行主動引用(其實還有一種場景,不過暫時我還沒弄明白這種場景的意思湿酸,就先不寫了):
1浴捆、使用new關鍵字實例化對象、讀取或者設置一個類的靜態(tài)字段(被final修飾的靜態(tài)字段除外)稿械、調(diào)用一個類的靜態(tài)方法的時候
2选泻、使用java.lang.reflect包中的方法對類進行反射調(diào)用的時候
3、初始化一個類美莫,發(fā)現(xiàn)其父類還沒有初始化過的時候
4页眯、虛擬機啟動的時候,虛擬機會先初始化用戶指定的包含main()方法的那個類
除了上面4種場景外厢呵,所有引用類的方式都不會觸發(fā)類的初始化窝撵,稱為被動引用,接下來看下被動引用的幾個例子:
1襟铭、子類引用父類靜態(tài)字段碌奉,不會導致子類初始化短曾。至于子類是否被加載、驗證了赐劣,前者可以通過"-XX:+TraceClassLoading"來查看
public class SuperClass
{
public static int value = 123;
static
{
System.out.println("SuperClass init");
}
}
public class SubClass extends SuperClass
{
static
{
System.out.println("SubClass init");
}
}
public class TestMain
{
public static void main(String[] args)
{
System.out.println(SubClass.value);
}
}
運行結(jié)果為
SuperClass init
123
2嫉拐、通過數(shù)組定義引用類,不會觸發(fā)此類的初始化
public class SuperClass
{
public static int value = 123;
static
{
System.out.println("SuperClass init");
}
}
public class TestMain
{
public static void main(String[] args)
{
SuperClass[] scs = new SuperClass[10];
}
}
3魁兼、引用靜態(tài)常量時婉徘,常量在編譯階段會存入類的常量池中,本質(zhì)上并沒有直接引用到定義常量的類
public class ConstClass
{
public static final String HELLOWORLD = "Hello World";
static
{
System.out.println("ConstCLass init");
}
}
public class TestMain
{
public static void main(String[] args)
{
System.out.println(ConstClass.HELLOWORLD);
}
}
在編譯階段通過常量傳播優(yōu)化咐汞,常量HELLOWORLD的值"Hello World"實際上已經(jīng)存儲到了NotInitialization類的常量池中盖呼,以后NotInitialization對常量ConstClass.HELLOWORLD的引用實際上都被轉(zhuǎn)化為NotInitialization類對自身常量池的引用了。也就是說化撕,實際上的NotInitialization的Class文件中并沒有ConstClass類的符號引用入口几晤,這兩個類在編譯成Class之后就不存在任何聯(lián)系了。