一、類載入器
首先來看一下java程序的運(yùn)行過程。
從這個框圖非常easy大體上了解java程序工作原理。首先,你寫好java代碼,保存到硬盤其中。然后你在命令行中輸入
javac YourClassName.java
此時,你的java代碼就被編譯成字節(jié)碼(.class).假設(shè)你是在Eclipse IDE或者其它開發(fā)工具中,你保存代碼的時候,開發(fā)工具已經(jīng)幫你完畢了上述的編譯工作,因此你能夠在相應(yīng)的文件夾下看到class文件。此時的class文件依舊是保存在硬盤中,因此,當(dāng)你在命令行中執(zhí)行
java YourClassName
就完畢了上面紅色方框中的工作。JRE的來載入器從硬盤中讀取class文件,載入到系統(tǒng)分配給JVM的內(nèi)存區(qū)域--運(yùn)行數(shù)據(jù)區(qū)(Runtime Data Areas). 然后運(yùn)行引擎解釋或者編譯類文件,轉(zhuǎn)化成特定CPU的機(jī)器碼,CPU運(yùn)行機(jī)器碼,至此完畢整個過程。
接下來就重點(diǎn)研究一下類載入器到底為何物?又是怎樣工作的?
首先看一下來載入器的一些特點(diǎn),有點(diǎn)抽象,只是總有幫助的。
》》層級結(jié)構(gòu)
類載入器被組織成一種層級結(jié)構(gòu)關(guān)系,也就是父子關(guān)系。當(dāng)中,Bootstrap是全部類載入器的父親。例如以下圖所看到的:
--Bootstrap class loader:
當(dāng)執(zhí)行java虛擬機(jī)時,這個類載入器被創(chuàng)建,它載入一些主要的java API,包含Object這個類。須要注意的是,這個類載入器不是用java語言寫的,而是用C/C++寫的。
--Extension class loader:
這個載入器載入出了基本API之外的一些拓展類,包含一些與安全性能相關(guān)的類。(眼下了解得不是非常深,僅僅能籠統(tǒng)說,待日后再具體說明)
--System Class Loader:
它載入應(yīng)用程序中的類,也就是在你的classpath中配置的類。
--User-Defined Class Loader:
這是開發(fā)者通過拓展ClassLoader類定義的自己定義載入器,載入程序猿定義的一些類。
》》委派模式(Delegation Mode)
細(xì)致看上面的層次結(jié)構(gòu),當(dāng)JVM載入一個類的時候,下層的載入器會將將任務(wù)托付給上一層類載入器,上一層載入檢查它的命名空間中是否已經(jīng)載入這個類,假設(shè)已經(jīng)載入,直接使用這個類。假設(shè)沒有載入,繼續(xù)往上托付直到頂部。檢查完了之后,依照相反的順序進(jìn)行載入,假設(shè)Bootstrap載入器找不到這個類,則往下托付,直到找到類文件。對于某個特定的類載入器來說,一個Java類僅僅能被載入一次,也就是說在Java虛擬機(jī)中,類的完整標(biāo)識是(classLoader,package,className)。一個雷能夠被不同的類載入器載入。
舉個詳細(xì)的樣例來說明,如今添?我有一個自定義的類MyClass須要載入,假設(shè)不指定的話,一般交App(System)載入。接到任務(wù)后,System檢查自己的庫里是否已經(jīng)有這個類,發(fā)現(xiàn)沒有之后托付給Extension,Extension進(jìn)行相同的檢查,發(fā)現(xiàn)還是沒有繼續(xù)往上托付,最頂層的Boots發(fā)現(xiàn)自己庫里也沒有,于是依據(jù)它的路徑(Java 核心類庫,如java.lang)嘗試去載入,沒找到這個MaClass類,于是僅僅好(人家看好你,交給你完畢,你無能為力,僅僅好交給別人啦)往下托付給Extension,Extension到自己的路徑(JAVA_HOME/jre/lib/ext)是找,還是沒找到,繼續(xù)往下,此時System載入器到classpath路徑尋找,找到了,于是載入到Java虛擬機(jī)。
如今如果我們將這個類放到JAVA_HOME/jre/lib/ext這個路徑中去(相當(dāng)于交給Extension載入器載入),依照相同的規(guī)則,最后由Extension載入器載入MyClass類,看到了吧,統(tǒng)一各類被兩次載入到JVM,可是每次都是由不同的ClassLoader完畢。
》》可見性限制
下層的載入器可以看到上層載入器中的類,反之則不行,也就是是說托付僅僅能從下到上。
》》不同意卸載類
類載入器能夠載入一個類,可是它不能卸載一個類??墒穷愝d入器能夠被刪除或者被創(chuàng)建。
當(dāng)類載入完畢之后,JVM繼續(xù)依照下圖完畢其它工作:
框圖中各個步驟簡介例如以下:
Loading:文章前面介紹的類載入,將文件系統(tǒng)中的Class文件載入到JVM內(nèi)存(執(zhí)行數(shù)據(jù)區(qū)域)
Verifying:檢查加載的類文件是否符合Java規(guī)范和虛擬機(jī)規(guī)范。
Preparing:為這個類分配所須要的內(nèi)存,確定這個類的屬性、方法等所需的數(shù)據(jù)結(jié)構(gòu)。(Prepare a data structure that assigns the memory required by classes and indicates the fields, methods, and interfaces defined in the class.)
Resolving:將該類常量池中的符號引用都改變?yōu)橹苯右?。(不是非常理解?/p>
Initialing:初始化類的局部變量,為靜態(tài)域賦值,同一時候運(yùn)行靜態(tài)初始化塊。
那么,Class Loader在載入類的時候,到底做了些什么工作呢?
要了解這當(dāng)中的細(xì)節(jié),必須得先具體介紹一下執(zhí)行數(shù)據(jù)區(qū)域。
二、執(zhí)行數(shù)據(jù)區(qū)域
Runtime Data Areas:當(dāng)執(zhí)行一個JVM演示樣例時,系統(tǒng)將分配給它一塊內(nèi)存區(qū)域(這塊內(nèi)存區(qū)域的大小能夠設(shè)置的),這一內(nèi)存區(qū)域由JVM自己來管理。從這一塊內(nèi)存中分出一塊用來存儲一些執(zhí)行數(shù)據(jù),比如創(chuàng)建的對象,傳遞給方法的參數(shù),局部變量,返回值等等。分出來的這一塊就稱為執(zhí)行數(shù)據(jù)區(qū)域。執(zhí)行數(shù)據(jù)區(qū)域能夠劃分為6大塊:Java棧、程序計(jì)數(shù)寄存器(PC寄存器)、本地方法棧(Native Method Stack)、Java堆、方法區(qū)域、執(zhí)行常量池(Runtime Constant Pool)。執(zhí)行常量池本應(yīng)該屬于方法區(qū),可是因?yàn)槠渲匾?,JVM規(guī)范將其獨(dú)立出來說明。當(dāng)中,前面3各區(qū)域(PC寄存器、Java棧、本地方法棧)是每一個線程獨(dú)自擁有的,后三者則是整個JVM實(shí)例中的全部線程共同擁有的。這六大塊例如以下圖所看到的:
》PC計(jì)數(shù)器:
每個線程都擁有一個PC計(jì)數(shù)器,當(dāng)線程啟動(start)時,PC計(jì)數(shù)器被創(chuàng)建,這個計(jì)數(shù)器存放當(dāng)前正在被運(yùn)行的字節(jié)碼指令(JVM指令)的地址。
》Java棧:
相同的,Java棧也是每一個線程單獨(dú)擁有,線程啟動時創(chuàng)建。這個棧中存放著一系列的棧幀(Stack Frame),JVM僅僅能進(jìn)行壓入(Push)和彈出(Pop)棧幀這兩種操作。每當(dāng)調(diào)用一個方法時,JVM就往棧里壓入一個棧幀,方法結(jié)束返回時彈出棧幀。假設(shè)方法運(yùn)行時出現(xiàn)異常,能夠調(diào)用printStackTrace等方法來查看棧的情況。棧的示意圖例如以下:
OK。如今我們再來具體看看每個棧幀中都放著什么東西。從示意圖非常easy看出,每個棧幀包括三個部分:本地變量數(shù)組,操作數(shù)棧,方法所屬類的常量池引用。
》局部(本地)變量數(shù)組:
局部(本地)變量數(shù)組中,從0開始按順序存放方法所屬對象的引用、傳遞給方法的參數(shù)、局部變量。舉個樣例:
- public void doSomething(int a, double b, Object o) {
- ...
- }
- 0: this
- 1: a
- 2,3:b
- 4:0
》操作數(shù)棧:
操作數(shù)棧中存放方法運(yùn)行時的一些中間變量,JVM在運(yùn)行方法時壓入或者彈出這些變量。事實(shí)上,操作數(shù)棧是方法真正工作的地方,運(yùn)行方法時,局部變量數(shù)組與操作數(shù)棧依據(jù)方法定義進(jìn)行數(shù)據(jù)交換。比如,運(yùn)行下面代碼時,操作數(shù)棧的情況例如以下:
- int a = 90;
- int b = 10;
- int c = a + b;
注意在這個圖中,操作數(shù)棧的地步是在上邊,所以先壓入的100位于上方。能夠看出,操作數(shù)棧事實(shí)上是一個數(shù)據(jù)暫時存儲區(qū),存放一些中間變量,方法結(jié)束了,操作數(shù)棧也就沒有啦。
》棧幀中數(shù)據(jù)引用:
除了局部變量數(shù)組和操作數(shù)棧之外,棧幀還須要一個常量池的引用。當(dāng)JVM運(yùn)行到須要常量池的數(shù)據(jù)時,就是通過這個引用來訪問常量池的。棧幀中的數(shù)據(jù)還要負(fù)責(zé)處理方法的返回和異常。假設(shè)通過return返回,則將該方法的棧幀從Java棧中彈出。假設(shè)方法有返回值,則將返回值壓入到調(diào)用該方法的方法的操作數(shù)棧中。另外,數(shù)據(jù)區(qū)中還保存中該方法可能的異常表的引用。以下的樣例用來說明:
- class Example3C{
- public static void addAndPrint(){
- double result = addTwoTypes(1,88.88);
- System.out.println(result);
- }
- public static double addTwoTypes(int i, double d){
- return i+d;
- }
- }
花些時間好好研究上圖。一樣須要注意的是,棧的底部在上方,先押人員addAndPrint方法的棧幀,再壓入addTwoTypes方法的棧幀。上圖最右邊的文字說明有錯誤,應(yīng)該是addTwoTypes的運(yùn)行結(jié)果存放在addAndPrint的操作數(shù)棧中。
》》本地方法棧
當(dāng)程序通過JNI(Java Native Interface)調(diào)用本地方法(如C或者C++代碼)時,就依據(jù)本地方法的語言類型建立對應(yīng)的棧。
》》方法區(qū)域
方法區(qū)域是一個JVM實(shí)例中的全部線程共享的,當(dāng)啟動一個JVM實(shí)例時,方法區(qū)域被創(chuàng)建。它用于存執(zhí)行放常量池、有關(guān)域和方法的信息、靜態(tài)變量、類和方法的字節(jié)碼。不同的JVM實(shí)現(xiàn)方式在實(shí)現(xiàn)方法區(qū)域的時候會有所差別。Oracle的HotSpot稱之為永久區(qū)域(Permanent Area)或者永久代(Permanent Generation)。
》》執(zhí)行常量池
這個區(qū)域存放類和接口的常量,除此之外,它還存放方法和域的全部引用。當(dāng)一個方法或者域被引用的時候,JVM就通過執(zhí)行常量池中的這些引用來查找方法和域在內(nèi)存中的的實(shí)際地址。
》》堆(Heap)
堆中存放的是程序創(chuàng)建的對象或者實(shí)例。這個區(qū)域?qū)VM的性能影響非常大。垃圾回收機(jī)制處理的正是這一塊內(nèi)存區(qū)域。
所以,類載入器載入事實(shí)上就是依據(jù)編譯后的Class文件,將java字節(jié)碼載入JVM內(nèi)存,并完畢對運(yùn)行數(shù)據(jù)處于的初始化工作,供運(yùn)行引擎運(yùn)行。
三、 運(yùn)行引擎(Execution Engine)
類載入器將字節(jié)碼載入內(nèi)存之后,運(yùn)行引擎以Java 字節(jié)碼指令為但愿,讀取Java字節(jié)碼。問題是,如今的java字節(jié)碼機(jī)器是讀不懂的,因此還必須想辦法將字節(jié)碼轉(zhuǎn)化成平臺相關(guān)的機(jī)器碼。這個過程能夠由解釋器來運(yùn)行,也能夠有即時編譯器(JIT Compiler)來完畢。