首先問一個問題喻奥,Java代碼是如何運行的纺阔?
- 寫好一份.Java代碼
- 被打包成jar包或war包瓷蛙,打包過程中转绷,被編譯成了.class字節(jié)碼文件
- 使用命令”java -jar” 命令伟件,運行這份java代碼(或系統(tǒng)),此時就啟動了一個JVM進程议经。
所以斧账,我們平時部署一個系統(tǒng)并運行的時候,其實就是啟動了一個JVM煞肾,由JVM來運行這臺機器上的這個系統(tǒng)咧织。
JVM要運行系統(tǒng)java代碼,就需要類加載器將class文件中的類加載進JVM內(nèi)存當中籍救,由JVM自己的字節(jié)碼執(zhí)行引擎來執(zhí)行加載進來的類文件习绢,比如,找到并執(zhí)行系統(tǒng)入口函數(shù)main()蝙昙。
那么闪萄,代碼是如何被編譯的呢?類加載器何時加載類呢奇颠?
一個java代碼文件败去,要想被執(zhí)行,主要經(jīng)過的步驟有:
源代碼(SourceCode)-》編譯器(預處理器preprocessor->編譯器compiler->匯編程序assembler->目標代碼object code->鏈接器Linker->) -》可執(zhí)行程序-》加載-》分配內(nèi)存-》執(zhí)行程序
按照JVM工作流程劃分其主要內(nèi)容:
Java源碼編譯機制
類加載機制
類執(zhí)行機制
內(nèi)存分配機制
內(nèi)存回收機制
對于JVM的內(nèi)存中包含以下兩個機制大刊,這里不展開,后面單獨研究三椿。
首先Java源代碼文件(.java后綴)會被Java編譯器編譯為字節(jié)碼文件(.class后綴)缺菌,然后由JVM中的類加載器加載各個類的字節(jié)碼文件,加載完畢之后搜锰,交由JVM執(zhí)行引擎執(zhí)行伴郁。在整個程序執(zhí)行過程中,JVM會用一段空間來存儲程序執(zhí)行期間需要用到的數(shù)據(jù)和相關信息蛋叼,這段空間一般被稱作為Runtime Data Area(運行時數(shù)據(jù)區(qū))焊傅,也就是我們常說的JVM內(nèi)存。
1狈涮、Java代碼編譯
代碼編譯由JAVA源碼編譯器來完成狐胎。主要是將源碼編譯成字節(jié)碼文件(class文件);字節(jié)碼文件格式主要分為兩部分:常量池和方法字節(jié)碼歌馍。
Java代碼編譯是由Java源碼編譯器來完成握巢,流程圖如下所示:
具體步驟詳見javac 編譯與 JIT 編譯
Java 源碼編譯機制
Java 源碼編譯由以下三個過程組成:
- 分析和輸入到符號表
- 注解處理
- 語義分析和生成class文件
流程圖如下所示:
(javac–verbose 輸出有關編譯器正在執(zhí)行的操作的消息)
最后生成的class文件由以下部分組成:
結(jié)構(gòu)信息:包括class文件格式、版本號松却、各部分的數(shù)量與大小的信息
元數(shù)據(jù):對應于Java源碼中聲明與常量的信息暴浦。包含類/繼承的超類/實現(xiàn)的接口的聲明信息溅话、域與方法聲明信息和常量池
方法信息:對應Java源碼中語句和表達式對應的信息。包含字節(jié)碼歌焦、異常處理器表飞几、求值棧與局部變量區(qū)大小、求值棧的類型記錄独撇、調(diào)試符號信息屑墨。
最后生成的 class 文件由以下部分組成:
- 結(jié)構(gòu)信息。包括 class 文件格式版本號及各部分的數(shù)量與大小的信息券勺。
- 元數(shù)據(jù)绪钥。對應于 Java 源碼中聲明與常量的信息。包含類/繼承的超類/實現(xiàn)的接口的聲明信息关炼、域與方法聲明信息和常量池程腹。
- 方法信息。對應 Java 源碼中語句和表達式對應的信息儒拂。包含字節(jié)碼寸潦、異常處理器表、求值棧與局部變量區(qū)大小社痛、求值棧的類型記錄见转、調(diào)試符號信息。
2蒜哀、類加載機制
2.1 類的生命周期
類的生命周期由被加載到虛擬機內(nèi)存中開始斩箫,到卸載出內(nèi)存結(jié)束,共有七個階段撵儿,其中到初始化之前的都是屬于類加載的部分:
** 加載---驗證---準備---解析----初始化---使用---卸載 **
系統(tǒng)可能在第一次使用某個類時加載該類乘客,也可能采用預加載機制來加載某個類,當運行某個java程序時淀歇,會啟動一個java虛擬機進程易核,兩次運行的java程序處于兩個不同的JVM進程中,兩個jvm之間并不會共享數(shù)據(jù)浪默。
1牡直、加載階段
這個流程中的加載是類加載機制中的一個階段,段需要完成的事情有:
1)通過一個類的全限定名來獲取定義此類的二進制字節(jié)流纳决。
2)將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)碰逸。
3)在java堆中生成一個代表這個類的Class對象,作為訪問方法區(qū)中這些數(shù)據(jù)的入口阔加。
由于第一點沒有指明從哪里獲取以及怎樣獲取類的二進制字節(jié)流花竞,所以這一塊區(qū)域留給我開發(fā)者很大的發(fā)揮空間。
2、準備階段
這個階段正式為類變量(被static修飾的變量)分配內(nèi)存并設置類變量初始值约急,這個內(nèi)存分配是發(fā)生在方法區(qū)中零远。
1、注意這里并沒有對實例變量進行內(nèi)存分配厌蔽,實例變量將會在對象實例化時隨著對象一起分配在JAVA堆中牵辣。
2、這里設置的初始值奴饮,通常是指數(shù)據(jù)類型的零值纬向。
private static int a = 3;
這個類變量a在準備階段后的值是0,將3賦值給變量a是發(fā)生在初始化階段戴卜。
3逾条、初始化階段
初始化是類加載機制的最后一步,這個時候才正真開始執(zhí)行類中定義的JAVA程序代碼投剥。在前面準備階段师脂,類變量已經(jīng)賦過一次系統(tǒng)要求的初始值,在初始化階段最重要的事情就是對類變量進行初始化江锨,關注的重點是父子類之間各類資源初始化的順序吃警。
java類中對類變量指定初始值有兩種方式:
1)聲明類變量時指定初始值;
2)使用靜態(tài)初始化塊為類變量指定初始值啄育。
初始化的時機
1)創(chuàng)建類實例的時候酌心,分別有:1、使用new關鍵字創(chuàng)建實例挑豌;2安券、通過反射創(chuàng)建實例;3氓英、通過反序列化方式創(chuàng)建實例侯勉。
new Test();Class.forName(“com.mengdd.Test”);
2)調(diào)用某個類的類方法(靜態(tài)方法) Test.doSomething();
3)訪問某個類或接口的類變量,或為該類變量賦值债蓝。 int b=Test.a; Test.a=b;
4)初始化某個類的子類壳鹤。當初始化子類的時候盛龄,該子類的所有父類都會被初始化饰迹。
5)直接使用java.exe命令來運行某個主類。
除了上面幾種方式會自動初始化一個類余舶,其他訪問類的方式都稱不會觸發(fā)類的初始化啊鸭,稱為被動引用。
被動引用的情況
1匿值、子類引用父類的靜態(tài)變量赠制,不會導致子類初始化。
publicclass SupClass
{
public static int a = 123;
static
{
System.out.println("supclassinit");
}
}
publicclass SubClass extends SupClass
{
static
{
System.out.println("subclassinit");
}
}
publicclass Test
{
public static void main(String[] args)
{
System.out.println(SubClass.a);
}
}
執(zhí)行結(jié)果:
supclass init
123
2、引用常量時钟些,不會觸發(fā)該類的初始化
用final修飾某個類變量時烟号,它的值在編譯時就已經(jīng)確定好放入常量池了,所以在訪問該類變量時政恍,等于直接從常量池中獲取汪拥,并沒有初始化該類。
初始化機制:
1篙耗、如果該類還沒有加載和連接迫筑,則程序先加載該類并連接。
2宗弯、如果該類的直接父類沒有加載脯燃,則先初始化其直接父類。
3蒙保、如果類中有初始化語句辕棚,則系統(tǒng)依次執(zhí)行這些初始化語句。
** 在第二個步驟中追他,如果直接父類又有直接父類坟募,則系統(tǒng)會再次重復這三個步驟來初始化這個父類,依次類推邑狸,JVM最先初始化的總是java.lang.Object類懈糯。當程序主動使用任何一個類時,系統(tǒng)會保證該類以及所有的父類都會被初始化单雾。**
2.2 類加載機制
類加載器結(jié)構(gòu)關系
JVM的類加載是通過ClassLoader及其子類來完成的赚哗,類的層次關系和加載順序可以由下圖來描述:
1)Bootstrap ClassLoader /啟動類加載器
是ClassLoader子類 ,自身也沒有子類硅堆,并且不遵守classLoader加載機制屿储;是JVM內(nèi)核中的加載器,由C++實現(xiàn)渐逃;負責加載$JAVA_HOME中jre/lib/rt.jar里所有的class够掠。
2)Extension ClassLoader/擴展類加載器
是用JAVA編寫,且它的父加載器是Bootstrap茄菊,但是因為BootStrap是用C++寫的疯潭,所以有時候也說ExtClassLoader沒有父加載器,自身也是頂層父類面殖,但是血統(tǒng)不純竖哩,不全是JVM實現(xiàn)的。
負責加載java平臺中擴展功能的一些jar包脊僚,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目錄下的jar包
通過程序來看下系統(tǒng)變量java.ext.dirs所指定的路徑:
public class Test{
public static void main(String[] args) {
System.out.println(System.getProperty("java.ext.dirs"));
}
}
執(zhí)行結(jié)果:
C:\Program Files(x86)\Java\jdk1.6.0_43\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
3)App ClassLoader/ 系統(tǒng)類加載器
也稱為應用程序類加載器相叁,負責加載應用程序classpath目錄下的所有jar和class文件。它的父加載器為Ext ClassLoader。
4)Custom ClassLoader/用戶自定義類加載器
(java.lang.ClassLoader的子類)
屬于應用程序根據(jù)自身需要自定義的ClassLoader增淹,如tomcat椿访、jboss都會根據(jù)j2ee規(guī)范自行實現(xiàn)ClassLoader
這幾種類加載器的層次關系如下圖所示:
類加載機制
類加載機制的特點:
全盤負責,當一個類加載器負責加載某個Class時虑润,該Class所依賴的和引用的其他Class也將由該類加載器負責載入赎离,除非顯示使用另外一個類加載器來載入
父類委托,先讓父類加載器試圖加載該類端辱,只有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類
緩存機制梁剔,緩存機制將會保證所有加載過的Class都會被緩存,當程序中需要使用某個Class時舞蔽,類加載器先從緩存區(qū)尋找該Class荣病,只有緩存區(qū)不存在,系統(tǒng)才會讀取該類對應的二進制數(shù)據(jù)渗柿,并將其轉(zhuǎn)換成Class對象个盆,存入緩存區(qū)。這就是為什么修改了Class后朵栖,必須重啟JVM颊亮,程序的修改才會生效
由上述分析可知,類的加載機制采用的是一種父類委托機制陨溅,也叫作雙親委派機制或者父優(yōu)先等級加載機制:
如果一個類加載器收到了一個類加載請求终惑,它不會自己去嘗試加載這個類,而是首先會自下而上的檢查該類是否已被加載门扇,從Custom ClassLoader到BootStrap ClassLoader逐層檢查雹有,只要某個classloader已加載就視為已加載此類,并將結(jié)果逐層向下反饋臼寄;如果沒有加載霸奕,則繼續(xù)向上層檢查,所有的類加載請求都應該傳遞到最頂層的啟動類加載器中吉拳,只有到父類加載器反饋自己無法完成這個加載請求(在它的搜索范圍沒有找到這個類)時质帅,子類加載器才會嘗試自己去加載,這種委派機制的好處就是保證了一個類不被重復加載留攒。
所以說煤惩,類加載檢查順序是自下而上,而加載的順序是自頂向下稼跳,也就是由上層來逐層嘗試加載類盟庞。
這種類加載機制的實現(xiàn)比較簡單吃沪,源碼如下:
protectedsynchronized Class<?> loadClass(String paramString, boolean paramBoolean)
throws ClassNotFoundException
{
//檢查是否被加載過
Class localClass =findLoadedClass(paramString);
//如果沒有加載汤善,則調(diào)用父類加載器
if (localClass == null) {
try {
//父類加載器不為空
if (this.parent != null)
localClass = this.parent.loadClass(paramString,false);
else {
//父類加載器為空,則使用啟動類加載器,傳統(tǒng)意義上啟動類加載器沒有父類加載器
localClass =findBootstrapClass0(paramString);
}
}
catch (ClassNotFoundExceptionlocalClassNotFoundException)
{
//如果父類加載失敗红淡,則使用自己的findClass方法進行加載
localClass = findClass(paramString);
}
}
if (paramBoolean) {
resolveClass(localClass);
}
return localClass;
}
代碼大意就是先檢查是否已經(jīng)被加載過不狮,若沒有加載則調(diào)用父類加載器的loadClass方法,若父類加載器不存在在旱,則使用啟動類加載器摇零。如果父類加載器加載失敗,則拋出異常之后看桶蝎,再調(diào)用自己定義的的findClass方法進行加載驻仅。
2.3 自定義類加載器
通常情況下,我們都是直接使用系統(tǒng)類加載器登渣。但是噪服,有的時候,我們也需要自定義類加載器胜茧。比如應用是通過網(wǎng)絡來傳輸 Java類的字節(jié)碼粘优,為保證安全性,這些字節(jié)碼經(jīng)過了加密處理呻顽,這時系統(tǒng)類加載器就無法對其進行加載雹顺,這樣則需要自定義類加載器來實現(xiàn)。自定義類加載器一般都是繼承自**** ClassLoader****類廊遍,從上面對 loadClass方法來分析來看嬉愧,我們只需要重寫 findClass方法即可。
下面我們通過一個示例來演示自定義類加載器的流程:
public class MyClassLoader extendsClassLoader {
private String root;
protected Class<?> findClass(String name) throwsClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0,classData.length);
}
}
private byte[] loadClassData(String className) {
String fileName = root + File.separatorChar
+ className.replace('.',File.separatorChar) + ".class";
try {
InputStream ins = new FileInputStream(fileName);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length = 0;
while ((length = ins.read(buffer)) != -1) {
baos.write(buffer, 0, length);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public String getRoot() {
return root;
}
public void setRoot(String root) {
this.root = root;
}
public static void main(String[] args) {
MyClassLoader classLoader = new MyClassLoader();
classLoader.setRoot("E:\\temp");
Class<?> testClass = null;
try {
testClass =classLoader.loadClass("com.neo.classloader.Test2");
Object object = testClass.newInstance();
System.out.println(object.getClass().getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
自定義類加載器的核心在于對字節(jié)碼文件的獲取,如果是加密的字節(jié)碼則需要在該類中對文件進行解密锄贷。由于這里只是演示丢郊,并未對class文件進行加密,因此沒有解密的過程四康。這里有幾點需要注意:
1)這里傳遞的文件名需要是類的全限定性名稱,即com.paddx.test.classloading.Test格式的狭握,因為 defineClass 方法是按這種格式進行處理的闪金。
2)最好不要重寫loadClass方法,因為這樣容易破壞雙親委托模式论颅。
3)這類Test 類本身可以被 AppClassLoader 類加載哎垦,因此我們不能把 com/paddx/test/classloading/Test.class 放在類路徑下。否則恃疯,由于雙親委托機制的存在漏设,會直接導致該類由 AppClassLoader 加載,而不會通過我們自定義類加載器來加載今妄。
3 類執(zhí)行機制
Java字節(jié)碼的執(zhí)行是由JVM執(zhí)行引擎來完成郑口,流程圖如下所示:
JVM是基于棧的體系結(jié)構(gòu)來執(zhí)行class字節(jié)碼的鸳碧。
線程創(chuàng)建后,都會產(chǎn)生一個線程私有的程序計數(shù)器(PC寄存器)和棧(Stack):
** 程序計數(shù)器**存放程序正常執(zhí)行時下一條要執(zhí)行的指令在方法內(nèi)的偏移量地址犬性;
** 棧中存放一個個棧幀瞻离,各個方法每調(diào)用一次就會創(chuàng)建一個自己私有的棧幀,棧幀分為局部變量表乒裆、操作數(shù)棧套利、動態(tài)連接、方法返回地址和一些附加信息**:
1) 局部變量區(qū)是一組變量值存儲空間鹤耍,用于存放方法中的參數(shù)肉迫、局部變量;
局部變量表的容量以變量槽(slot)為最小單位稿黄,一個slot可以存放一個32位以內(nèi)的數(shù)據(jù)類型昂拂,而Java中占32位以內(nèi)的數(shù)據(jù)類型有boolean、byte抛猖、char格侯、short、int财著、float联四、reference(也可以64位)和returnAddress八種類型
Java語句中明確規(guī)定的64位的數(shù)據(jù)類型只有l(wèi)ong和double兩種(reference可能是32位,也可能是64位)故long和double不是原子操作撑教,只是局部變量表建立在線程的堆棧上朝墩,是線程私有的數(shù)據(jù),無論讀寫兩個連續(xù)的slot是否是原子操作伟姐,都不會引起數(shù)據(jù)安全問題
2) 操作數(shù)棧中用于存放方法執(zhí)行過程中產(chǎn)生的中間結(jié)果收苏。
3) 動態(tài)連接
符號引用一部分會在類加載階段或第一次使用的時候轉(zhuǎn)換成為直接引用,這種轉(zhuǎn)換稱為靜態(tài)解析愤兵。另外一部分將在每一次的運行期間轉(zhuǎn)換為直接引用鹿霸,這部分稱為動態(tài)引用
4)方法返回地址
當一個方法被執(zhí)行后,有兩種方式退出這個方法秆乳。
第一種是執(zhí)行引擎懦鼠,遇到一個方法返回的字節(jié)碼指令,這時可能會返回值傳遞給上層的方法調(diào)用者屹堰。這種退出方式為正常完成出口
另一種是遇到異常并且沒有在方法體內(nèi)得到處理(throws不屬于方法體內(nèi)處理)肛冶,這種退出方式是不會給它的上層調(diào)用者產(chǎn)生任何返回值的。
一般來說扯键,方法正常退出時睦袖,調(diào)用者的PC計數(shù)器的值就可以作為返回地址,棧幀中很可能會保存這個計數(shù)器值荣刑。而方法異常退出時馅笙,返回地址是要通過異常處理器表來確定的伦乔,棧幀中一般不會保存這部分信息。
方法退出的實質(zhì)
實際上等同于把當前棧幀出棧延蟹,因此退出時可能執(zhí)行的操作有:恢復上層方法的局部變量表盒操作數(shù)棧,把返回值(如果有的話)壓入調(diào)用者棧幀的操作數(shù)棧中叶堆,調(diào)整PC計數(shù)器的值以指向方法調(diào)用指令后面的一條指令等
JVM工作機制就是執(zhí)行引擎的工作過程--執(zhí)行引擎
1阱飘、最簡單的:一次性解釋字節(jié)碼。
2虱颗、快沥匈,但消耗內(nèi)存的:“即時編譯器”,第一次被執(zhí)行的字節(jié)碼會被編譯成機器代碼忘渔,放入緩存高帖,以后調(diào)用可以重用。
3畦粮、自適應優(yōu)化器散址,虛擬機開始的時候會解釋字節(jié)碼,但是會監(jiān)視運行中程序的活動宣赔,并記錄下使用最頻繁的代碼段预麸。程序運行的時候,虛擬機只把使用最頻繁的代碼編譯成本地代碼儒将,其他的代碼由于使用的并不頻繁吏祸,繼續(xù)保留為字節(jié)碼--由虛擬機繼續(xù)解釋他們。一般可以使java虛擬機80%90%的時間里執(zhí)行被優(yōu)化過的本地代碼钩蚊,只需要編譯10%20%對性能優(yōu)影響的代碼贡翘。
4、由硬件芯片組成砰逻,他用本地方法執(zhí)行java字節(jié)碼鸣驱,這種執(zhí)行引擎實際上是內(nèi)嵌在芯片里的。
JVM為何采用基于棧的結(jié)構(gòu)設計
基于棧的方式:所有的操作數(shù)必須先入棧蝠咆,然后根據(jù)指令集中的操作碼從棧頂彈出若干個元素后再將結(jié)果壓入棧中丐巫。操作數(shù)入棧可以是直接常量入椛酌溃或本地變量集中的變量壓入棧递胧。
JVM是基于棧的虛擬機,為每個新創(chuàng)建的線程都分配一個棧赡茸,也就是說一個Java程序來說缎脾,它的運行就是通過對棧的操作來完成的。棧以幀為單位保存線程的狀態(tài)占卧。JVM對棧只進行兩種操作:以幀為單位的壓棧和出棧操作遗菠。
某個線程正在執(zhí)行的方法稱為此線程的當前方法联喘,當前方法使用的幀稱為當前幀。當線程激活一個Java方法,JVM就會在線程的 Java堆棧里新壓入一個幀辙纬。這個幀自然成為了當前幀.在此方法執(zhí)行期間,這個幀將用來保存參數(shù),局部變量,中間計算過程和其他數(shù)據(jù)豁遭。這個幀在這里和編譯原理中的活動紀錄的概念是差不多的。
從Java的這種分配機制來看,可以這樣理解:棧(Stack)是操作系統(tǒng)在建立某個進程時或者線程(在支持多線程的操作系統(tǒng)中是線程)為這個線程建立的存儲區(qū)域贺拣,該區(qū)域具有先進后出的特性蓖谢。
嵌套方法的出棧和入棧示意圖:
上圖中描述了嵌套方法時,stack的內(nèi)存分配圖譬涡,由上面可以知道闪幽,當嵌套方法調(diào)用時,嵌套越深涡匀,stack的內(nèi)存就越晚才能釋放盯腌,因此,在實際開發(fā)過程中陨瘩,不推薦大家使用遞歸來進行方法的調(diào)用腕够,遞歸很容易導致stack flow。
非嵌套方法的出棧入棧過程
采用基于棧的結(jié)構(gòu)設計原因:
1)JVM要保證設計成的與平臺無關舌劳。屏蔽平臺的差異性燕少,就要求保證在沒有或者很少寄存器的機器上同樣能夠正確的執(zhí)行Java代碼。
2)為了指令的緊湊性蒿囤。為了讓編譯后的class文件更加緊湊客们,降低其大小,提高字節(jié)碼在網(wǎng)絡上傳輸?shù)男省?/p>
3)及時釋放內(nèi)存材诽,提高內(nèi)存的利用率底挫。