Java虚拟机9:Java类加载机制
前言
我們知道我們寫(xiě)的程序經(jīng)過(guò)編譯后成為了.class文件,.class文件中描述了類的各種信息,最終都需要加載到虛擬機(jī)之后才能運(yùn)行和使用。而虛擬機(jī)如何加載這些.class文件?.class文件的信息進(jìn)入到虛擬機(jī)后會(huì)發(fā)生什么變化?這些都是本文要講的內(nèi)容,文章將會(huì)講解加載類加載的每個(gè)階段Java虛擬機(jī)需要做什么事(加粗標(biāo)紅)。
?
類使用的7個(gè)階段
類從被加載到虛擬機(jī)內(nèi)存中開(kāi)始,到卸載出內(nèi)存,它的整個(gè)生命周期包括:加載(Loading)、驗(yàn)證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化(Initiallization)、使用(Using)和卸載(Unloading)這7個(gè)階段。其中驗(yàn)證、準(zhǔn)備、解析3個(gè)部分統(tǒng)稱為連接(Linking),這七個(gè)階段的發(fā)生順序如下圖:
圖中,加載、驗(yàn)證、準(zhǔn)備、初始化、卸載這5個(gè)階段的順序是確定的,類的加載過(guò)程必須按照這種順序按部就班地開(kāi)始,而解析階段不一定:它在某些情況下可以初始化階段之后在開(kāi)始,這是為了支持Java語(yǔ)言的運(yùn)行時(shí)綁定(也稱為動(dòng)態(tài)綁定)。接下來(lái)講解加載、驗(yàn)證、準(zhǔn)備、解析、初始化五個(gè)步驟,這五個(gè)步驟組成了一個(gè)完整的類加載過(guò)程。使用沒(méi)什么好說(shuō)的,卸載屬于GC的工作,在之前GC的文章中已經(jīng)有所提及了。
?
加載Loading
加載是類加載的第一個(gè)階段。有兩種時(shí)機(jī)會(huì)觸發(fā)類加載:
1、預(yù)加載。虛擬機(jī)啟動(dòng)時(shí)加載,加載的是JAVA_HOME/lib/下的rt.jar下的.class文件,這個(gè)jar包里面的內(nèi)容是程序運(yùn)行時(shí)非常常常用到的,像java.lang.*、java.util.*、java.io.*等等,因此隨著虛擬機(jī)一起加載。要證明這一點(diǎn)很簡(jiǎn)單,寫(xiě)一個(gè)空的main函數(shù),設(shè)置虛擬機(jī)參數(shù)為"-XX:+TraceClassLoading"來(lái)獲取類加載信息,運(yùn)行一下:
1 [Opened E:\MyEclipse10\Common\binary\com.sun.java.jdk.win32.x86_64_1.6.0.013\jre\lib\rt.jar] 2 [Loaded java.lang.Object from E:\MyEclipse10\Common\binary\com.sun.java.jdk.win32.x86_64_1.6.0.013\jre\lib\rt.jar] 3 [Loaded java.io.Serializable from E:\MyEclipse10\Common\binary\com.sun.java.jdk.win32.x86_64_1.6.0.013\jre\lib\rt.jar] 4 [Loaded java.lang.Comparable from E:\MyEclipse10\Common\binary\com.sun.java.jdk.win32.x86_64_1.6.0.013\jre\lib\rt.jar] 5 [Loaded java.lang.CharSequence from E:\MyEclipse10\Common\binary\com.sun.java.jdk.win32.x86_64_1.6.0.013\jre\lib\rt.jar] 6 [Loaded java.lang.String from E:\MyEclipse10\Common\binary\com.sun.java.jdk.win32.x86_64_1.6.0.013\jre\lib\rt.jar] 7 [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] 8 [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] 9 [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] 10 [Loaded java.lang.Class from E:\MyEclipse10\Common\binary\com.sun.java.jdk.win32.x86_64_1.6.0.013\jre\lib\rt.jar] 11 [Loaded java.lang.Cloneable from E:\MyEclipse10\Common\binary\com.sun.java.jdk.win32.x86_64_1.6.0.013\jre\lib\rt.jar] 12 ...2、運(yùn)行時(shí)加載。虛擬機(jī)在用到一個(gè).class文件的時(shí)候,會(huì)先去內(nèi)存中查看一下這個(gè).class文件有沒(méi)有被加載,如果沒(méi)有就會(huì)按照類的全限定名來(lái)加載這個(gè)類。
那么,加載階段做了什么,其實(shí)加載階段做了有三件事情:
1、獲取.class文件的二進(jìn)制流
2、將類信息、靜態(tài)變量、字節(jié)碼、常量這些.class文件中的內(nèi)容放入方法區(qū)中
3、在內(nèi)存中生成一個(gè)代表這個(gè).class文件的java.lang.Class對(duì)象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問(wèn)入口。一般這個(gè)Class是在堆里的,不過(guò)HotSpot虛擬機(jī)比較特殊,這個(gè)Class對(duì)象是放在方法區(qū)中的
虛擬機(jī)規(guī)范對(duì)這三點(diǎn)的要求并不具體,因此虛擬機(jī)實(shí)現(xiàn)與具體應(yīng)用的靈活度都是相當(dāng)大的。例如第一條,根本沒(méi)有指明二進(jìn)制字節(jié)流要從哪里來(lái)、怎么來(lái),因此單單就這一條,就能變出許多花樣來(lái):
· 從zip包中獲取,這就是以后jar、ear、war格式的基礎(chǔ)
· 從網(wǎng)絡(luò)中獲取,典型應(yīng)用就是Applet
· 運(yùn)行時(shí)計(jì)算生成,典型應(yīng)用就是動(dòng)態(tài)代理技術(shù)
· 由其他文件生成,典型應(yīng)用就是JSP,即由JSP生成對(duì)應(yīng)的.class文件
· 從數(shù)據(jù)庫(kù)中讀取,這種場(chǎng)景比較少見(jiàn)
總而言之,在類加載整個(gè)過(guò)程中,這部分是對(duì)于開(kāi)發(fā)者來(lái)說(shuō)可控性最強(qiáng)的一個(gè)階段。
?
驗(yàn)證
連接階段的第一步,這一階段的目的是為了確保.class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全。?
Java語(yǔ)言本身是相對(duì)安全的語(yǔ)言(相對(duì)C/C++來(lái)說(shuō)),但是前面說(shuō)過(guò),.class文件未必要從Java源碼編譯而來(lái),可以使用任何途徑產(chǎn)生,甚至包括用十六進(jìn)制編輯器直接編寫(xiě)來(lái)產(chǎn)生.class文件。在字節(jié)碼語(yǔ)言層面上,Java代碼至少?gòu)恼Z(yǔ)義上是可以表達(dá)出來(lái)的。虛擬機(jī)如果不檢查輸入的字節(jié)流,對(duì)其完全信任的話,很可能會(huì)因?yàn)檩d入了有害的字節(jié)流而導(dǎo)致系統(tǒng)崩潰,所以驗(yàn)證是虛擬機(jī)對(duì)自身保護(hù)的一項(xiàng)重要工作。
驗(yàn)證階段將做一下幾個(gè)工作,具體就不細(xì)講了,這是虛擬機(jī)實(shí)現(xiàn)層面的問(wèn)題:
1、文件格式驗(yàn)證
這個(gè)地方要說(shuō)一點(diǎn)和開(kāi)發(fā)者相關(guān)的。.class文件的第5~第8個(gè)字節(jié)表示的是該.class文件的主次版本號(hào),驗(yàn)證的時(shí)候會(huì)對(duì)這4個(gè)字節(jié)做一個(gè)驗(yàn)證,高版本的JDK能向下兼容以前版本的.class文件,但不能運(yùn)行以后的class文件,即使文件格式未發(fā)生任何變化,虛擬機(jī)也必須拒絕執(zhí)行超過(guò)其版本號(hào)的.class文件。舉個(gè)具體的例子,如果一段.java代碼是在JDK1.6下編譯的,那么JDK1.6、JDK1.7的環(huán)境能運(yùn)行這個(gè).java代碼生成的.class文件,但是JDK1.5、JDK1.4乃更低的JDK版本是無(wú)法運(yùn)行這個(gè).java代碼生成的.class文件的。如果運(yùn)行,會(huì)拋出java.lang.UnsupportedClassVersionError,這個(gè)小細(xì)節(jié),務(wù)必注意。
2、元數(shù)據(jù)驗(yàn)證
3、字節(jié)碼驗(yàn)證
4、符號(hào)引用驗(yàn)證
?
準(zhǔn)備
準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置其初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中分配。關(guān)于這點(diǎn),有兩個(gè)地方注意一下:
1、這時(shí)候進(jìn)行內(nèi)存分配的僅僅是類變量(被static修飾的變量),而不是實(shí)例變量,實(shí)例變量將會(huì)在對(duì)象實(shí)例化的時(shí)候隨著對(duì)象一起分配在Java堆中
2、這個(gè)階段賦初始值的變量指的是那些不被final修飾的static變量,比如"public static int value = 123;",value在準(zhǔn)備階段過(guò)后是0而不是123,給value賦值為123的動(dòng)作將在初始化階段才進(jìn)行;比如"public static final int value = 123;"就不一樣了,在準(zhǔn)備階段,虛擬機(jī)就會(huì)給value賦值為123。
各個(gè)數(shù)據(jù)類型的零值如下圖:
?
解析
解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過(guò)程。來(lái)了解一下符號(hào)引用和直接引用有什么區(qū)別:
1、符號(hào)引用。
這個(gè)其實(shí)是屬于編譯原理方面的概念,符號(hào)引用包括了下面三類常量:
· 類和接口的全限定名
· 字段的名稱和描述符
· 方法的名稱和描述符
這么說(shuō)可能不太好理解,結(jié)合實(shí)際看一下,寫(xiě)一段很簡(jiǎn)單的代碼:
1 package com.xrq.test6; 2 3 public class TestMain 4 { 5 private static int i; 6 private double d; 7 8 public static void print() 9 { 10 11 } 12 13 private boolean trueOrFalse() 14 { 15 return false; 16 } 17 }用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項(xiàng)內(nèi)容,其中帶"Utf8"的就是符號(hào)引用。比如#2,它的值是"com/xrq/test6/TestMain",表示的是這個(gè)類的全限定名;又比如#5為i,#6為I,它們是一對(duì)的,表示變量時(shí)Integer(int)類型的,名字叫做i;#6為D、#7為d也是一樣,表示一個(gè)Double(double)類型的變量,名字為d;#18、#19表示的都是方法的名字。
那其實(shí)總而言之,符號(hào)引用和我們上面講的是一樣的,是對(duì)于類、變量、方法的描述。符號(hào)引用和虛擬機(jī)的內(nèi)存布局是沒(méi)有關(guān)系的,引用的目標(biāo)未必已經(jīng)加載到內(nèi)存中了。
2、直接引用
直接引用可以是直接指向目標(biāo)的指針、相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄。直接引用是和虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局相關(guān)的,同一個(gè)符號(hào)引用在不同的虛擬機(jī)示例上翻譯出來(lái)的直接引用一般不會(huì)相同。如果有了直接引用,那引用的目標(biāo)必定已經(jīng)存在在內(nèi)存中了。
?
初始化
初始化階段是類加載過(guò)程的最后一步,初始化階段是真正執(zhí)行類中定義的Java程序代碼(或者說(shuō)是字節(jié)碼)的過(guò)程。初始化過(guò)程是一個(gè)執(zhí)行類構(gòu)造器<clinit>()方法的過(guò)程,根據(jù)程序員通過(guò)程序制定的主觀計(jì)劃去初始化類變量和其它資源。把這句話說(shuō)白一點(diǎn),其實(shí)初始化階段做的事就是給static變量賦予用戶指定的值以及執(zhí)行靜態(tài)代碼塊。
注意一下,虛擬機(jī)會(huì)保證類的初始化在多線程環(huán)境中被正確地加鎖、同步,即如果多個(gè)線程同時(shí)去初始化一個(gè)類,那么只會(huì)有一個(gè)類去執(zhí)行這個(gè)類的<clinit>()方法,其他線程都要阻塞等待,直至活動(dòng)線程執(zhí)行<clinit>()方法完畢。因此如果在一個(gè)類的<clinit>()方法中有耗時(shí)很長(zhǎng)的操作,就可能造成多個(gè)進(jìn)程阻塞。不過(guò)其他線程雖然會(huì)阻塞,但是執(zhí)行<clinit>()方法的那條線程退出<clinit>()方法后,其他線程不會(huì)再次進(jìn)入<clinit>()方法了,因?yàn)橥粋€(gè)類加載器下,一個(gè)類只會(huì)初始化一次。實(shí)際應(yīng)用中這種阻塞往往是比較隱蔽的,要小心。
Java虛擬機(jī)規(guī)范嚴(yán)格規(guī)定了有且只有5種場(chǎng)景必須立即對(duì)類進(jìn)行初始化,這4種場(chǎng)景也稱為對(duì)一個(gè)類進(jìn)行主動(dòng)引用(其實(shí)還有一種場(chǎng)景,不過(guò)暫時(shí)我還沒(méi)弄明白這種場(chǎng)景的意思,就先不寫(xiě)了):
1、使用new關(guān)鍵字實(shí)例化對(duì)象、讀取或者設(shè)置一個(gè)類的靜態(tài)字段(被final修飾的靜態(tài)字段除外)、調(diào)用一個(gè)類的靜態(tài)方法的時(shí)候
2、使用java.lang.reflect包中的方法對(duì)類進(jìn)行反射調(diào)用的時(shí)候
3、初始化一個(gè)類,發(fā)現(xiàn)其父類還沒(méi)有初始化過(guò)的時(shí)候
4、虛擬機(jī)啟動(dòng)的時(shí)候,虛擬機(jī)會(huì)先初始化用戶指定的包含main()方法的那個(gè)類
除了上面4種場(chǎng)景外,所有引用類的方式都不會(huì)觸發(fā)類的初始化,稱為被動(dòng)引用,接下來(lái)看下被動(dòng)引用的幾個(gè)例子:
1、子類引用父類靜態(tài)字段,不會(huì)導(dǎo)致子類初始化。至于子類是否被加載、驗(yàn)證了,前者可以通過(guò)"-XX:+TraceClassLoading"來(lái)查看
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);} }運(yùn)行結(jié)果為
SuperClass init 1232、通過(guò)數(shù)組定義引用類,不會(huì)觸發(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];} }運(yùn)行結(jié)果為
3、引用靜態(tài)常量時(shí),常量在編譯階段會(huì)存入類的常量池中,本質(zhì)上并沒(méi)有直接引用到定義常量的類
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ùn)行結(jié)果為
Hello World在編譯階段通過(guò)常量傳播優(yōu)化,常量HELLOWORLD的值"Hello World"實(shí)際上已經(jīng)存儲(chǔ)到了NotInitialization類的常量池中,以后NotInitialization對(duì)常量ConstClass.HELLOWORLD的引用實(shí)際上都被轉(zhuǎn)化為NotInitialization類對(duì)自身常量池的引用了。也就是說(shuō),實(shí)際上的NotInitialization的Class文件中并沒(méi)有ConstClass類的符號(hào)引用入口,這兩個(gè)類在編譯成Class之后就不存在任何聯(lián)系了。
總結(jié)
以上是生活随笔為你收集整理的Java虚拟机9:Java类加载机制的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: vue2.0项目结构和打包发布
- 下一篇: 《Elixir in Action》书评