1、類加載器java
首先來看一下java程序的執行過程。程序員
從這個框圖很容易大致上瞭解java程序工做原理。首先,你寫好java代碼,保存到硬盤當中。而後你在命令行中輸入數組
javac YourClassName.java 安全
此時,你的java代碼就被編譯成字節碼(.class).若是你是在Eclipse IDE或者其餘開發工具中,你保存代碼的時候,開發工具已經幫你完成了上述的編譯工做,所以你能夠在對應的目錄下看到class文件。此時的class文 件依然是保存在硬盤中,所以,當你在命令行中運行數據結構
java YourClassName 工具
就完成了上面紅色方框中的工做。JRE的來加載器從硬盤中讀取class文件,載入到系統分配給JVM的內存區域--運行數據區(Runtime Data Areas). 而後執行引擎解釋或者編譯類文件,轉化成特定CPU的機器碼,CPU執行機器碼,至此完成整個過程。性能
接下來就重點研究一下類加載器究竟爲什麼物?又是如何工做的?開發工具
首先看一下來加載器的一些特色,有點抽象,不過總有幫助的。ui
》》層級結構this
類加載器被組織成一種層級結構關係,也就是父子關係。其中,Bootstrap是全部類加載器的父親。以下圖所示:
--Bootstrap class loader:
當運行java虛擬機時,這個類加載器被建立,它加載一些基本的java API,包括Object這個類。須要注意的是,這個類加載器不是用java語言寫的,而是用C/C++寫的。
--Extension class loader:
這個加載器加載出了基本API以外的一些拓展類,包括一些與安全性能相關的類。(目前瞭解得不是很深,只能籠統說,待往後再詳細說明)
--System Class Loader:
它加載應用程序中的類,也就是在你的classpath中配置的類。
--User-Defined Class Loader:
這是開發人員經過拓展ClassLoader類定義的自定義加載器,加載程序員定義的一些類。
》》委派模式(Delegation Mode)
仔細看上面的層次結構,當JVM加載一個類的時候,下層的加載器會將將任務委託給上一層類加載器,上一層加載檢查它的命名空間中是否已經加載這個 類,若是已經加載,直接使用這個類。若是沒有加載,繼續往上委託直到頂部。檢查完了以後,按照相反的順序進行加載,若是Bootstrap加載器找不到這 個類,則往下委託,直到找到類文件。對於某個特定的類加載器來講,一個Java類只能被載入一次,也就是說在Java虛擬機中,類的完整標識是 (classLoader,package,className)。一個雷能夠被不一樣的類加載器加載。
舉個具體的例子來講明,如今加入我有一個本身定義的類MyClass須要加載,若是不指定的話,通常交App(System)加載。接到任務 後,System檢查本身的庫裏是否已經有這個類,發現沒有以後委託給Extension,Extension進行一樣的檢查,發現仍是沒有繼續往上委 託,最頂層的Boots發現本身庫裏也沒有,因而根據它的路徑(Java 核心類庫,如java.lang)嘗試去加載,沒找到這個MaClass類,因而只好(人家看好你,交給你完成,你無能爲力,只好交給別人啦)往下委託給 Extension,Extension到本身的路徑(JAVA_HOME/jre/lib/ext)是找,仍是沒找到,繼續往下,此時System加載 器到classpath路徑尋找,找到了,因而加載到Java虛擬機。
如今假設咱們將這個類放到JAVA_HOME/jre/lib/ext這個路徑中去(至關於交給Extension加載器加載),按照一樣的規則, 最後由Extension加載器加載MyClass類,看到了吧,統一各種被兩次加載到JVM,可是每次都是由不一樣的ClassLoader完成。
》》可見性限制
下層的加載器可以看到上層加載器中的類,反之則不行,也就是是說委託只能從下到上。
》》不容許卸載類
類加載器能夠加載一個類,可是它不能卸載一個類。可是類加載器能夠被刪除或者被建立。
當類加載完畢以後,JVM繼續按照下圖完成其餘工做:
框圖中各個步驟簡單介紹以下:
Loading:文章前面介紹的類加載,將文件系統中的Class文件載入到JVM內存(運行數據區域)
Verifying:檢查載入的類文件是否符合Java規範和虛擬機規範。
Preparing:爲這個類分配所須要的內存,肯定這個類的屬性、方法等所需的數據結構。(Prepare a data structure that assigns the memory required by classes and indicates the fields, methods, and interfaces defined in the class.)
Resolving:將該類常量池中的符號引用都改變爲直接引用。(不是很理解)
Initialing:初始化類的局部變量,爲靜態域賦值,同時執行靜態初始化塊。
那麼,Class Loader在加載類的時候,究竟作了些什麼工做呢?
要了解這其中的細節,必須得先詳細介紹一下運行數據區域。
2、運行數據區域
Runtime Data Areas:當運行一個JVM示例時,系統將分配給它一塊內存區域(這塊內存區域的大小能夠設置的),這一內存區域由JVM本身來管理。從這一塊內存中分 出一塊用來存儲一些運行數據,例如建立的對象,傳遞給方法的參數,局部變量,返回值等等。分出來的這一塊就稱爲運行數據區域。運行數據區域能夠劃分爲6大 塊:Java棧、程序計數寄存器(PC寄存器)、本地方法棧(Native Method Stack)、Java堆、方法區域、運行常量池(Runtime Constant Pool)。運行常量池本應該屬於方法區,可是因爲其重要性,JVM規範將其獨立出來講明。其中,前面3各區域(PC寄存器、Java棧、本地方法棧)是 每一個線程獨自擁有的,後三者則是整個JVM實例中的全部線程共有的。這六大塊以下圖所示:
》PC計數器:
每個線程都擁有一個PC計數器,當線程啓動(start)時,PC計數器被建立,這個計數器存放當前正在被執行的字節碼指令(JVM指令)的地址。
》Java棧:
一樣的,Java棧也是每一個線程單獨擁有,線程啓動時建立。這個棧中存放着一系列的棧幀(Stack Frame),JVM只能進行壓入(Push)和彈出(Pop)棧幀這兩種操做。每當調用一個方法時,JVM就往棧裏壓入一個棧幀,方法結束返回時彈出棧 幀。若是方法執行時出現異常,能夠調用printStackTrace等方法來查看棧的狀況。棧的示意圖以下:
OK。如今咱們再來詳細看看每個棧幀中都放着什麼東西。從示意圖很容易看出,每一個棧幀包含三個部分:本地變量數組,操做數棧,方法所屬類的常量池引用。
》局部(本地)變量數組:
局部(本地)變量數組中,從0開始按順序存放方法所屬對象的引用、傳遞給方法的參數、局部變量。舉個例子:
public void doSomething(int a, double b, Object o) {
...
}
這個方法的棧幀中的局部變量存儲的內容分別是:
0: this
1: a
2,3:b
4:0
看仔細了,其中double類型的b須要兩個連續的索引。取值的時候,取出的是2這個索引中的值。若是是靜態方法,則數組第0個不存放this引用,而是直接存儲傳遞的參數。
》操做數棧:
操做數棧中存放方法執行時的一些中間變量,JVM在執行方法時壓入或者彈出這些變量。其實,操做數棧是方法真正工做的地方,執行方法時,局部變量數組與操做數棧根據方法定義進行數據交換。例如,執行如下代碼時,操做數棧的狀況以下:
int a = 90;
int b = 10;
int c = a + b;
注意在這個圖中,操做數棧的地步是在上邊,因此先壓入的100位於上方。能夠看出,操做數棧實際上是一個數據臨時存儲區,存放一些中間變量,方法結束了,操做數棧也就沒有啦。
》棧幀中數據引用:
除了局部變量數組和操做數棧以外,棧幀還須要一個常量池的引用。當JVM執行到須要常量池的數據時,就是經過這個引用來訪問常量池的。棧幀中的數據 還要負責處理方法的返回和異常。若是經過return返回,則將該方法的棧幀從Java棧中彈出。若是方法有返回值,則將返回值壓入到調用該方法的方法的 操做數棧中。另外,數據區中還保存中該方法可能的異常表的引用。下面的例子用來講明:
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;
}
}
執行上述代碼時,Java棧以下圖所示:
花些時間好好研究上圖。同樣須要注意的是,棧的底部在上方,先押人員addAndPrint方法的棧幀,再壓入addTwoTypes方法的棧幀。上圖最右邊的文字說明有錯誤,應該是addTwoTypes的執行結果存放在addAndPrint的操做數棧中。
》》本地方法棧
當程序經過JNI(Java Native Interface)調用本地方法(如C或者C++代碼)時,就根據本地方法的語言類型創建相應的棧。
》》方法區域
方法區域是一個JVM實例中的全部線程共享的,當啓動一個JVM實例時,方法區域被建立。它用於存運行放常量池、有關域和方法的信息、靜態變量、類 和方法的字節碼。不一樣的JVM實現方式在實現方法區域的時候會有所區別。Oracle的HotSpot稱之爲永久區域(Permanent Area)或者永久代(Permanent Generation)。
》》運行常量池
這個區域存放類和接口的常量,除此以外,它還存放方法和域的全部引用。當一個方法或者域被引用的時候,JVM就經過運行常量池中的這些引用來查找方法和域在內存中的的實際地址。
》》堆(Heap)
堆中存放的是程序建立的對象或者實例。這個區域對JVM的性能影響很大。垃圾回收機制處理的正是這一塊內存區域。
因此,類加載器加載其實就是根據編譯後的Class文件,將java字節碼載入JVM內存,並完成對運行數據處於的初始化工做,供執行引擎執行。
3、 執行引擎(Execution Engine)
類加載器將字節碼載入內存以後,執行引擎以Java 字節碼指令爲希望,讀取Java字節碼。問題是,如今的java字節碼機器是讀不懂的,所以還必須想辦法將字節碼轉化成平臺相關的機器碼。這個過程能夠由 解釋器來執行,也能夠有即時編譯器(JIT Compiler)來完成。