本文旨在給全部但願瞭解JVM(Java Virtual Machine)的同窗一個概念性的入門,主要介紹了JVM的組成部分以及它們內部工做的機制和原理。固然本文只是一個簡單的入門,不會涉及過多繁雜的參數和配置,感興趣的同窗能夠作更深刻的研究,在研究JVM的過程當中會發現,其實JVM自己就是一個計算機體系結構,不少原理和咱們平時的硬件、微機原理、操做系統都有十分類似的地方,因此學習JVM自己也是加深自我對計算機結構認識的一個很好的途徑。 html
另外須要注意的是,雖然平時咱們用的大可能是Sun(現已被Oracle收購)JDK提供的JVM,可是JVM自己是一個規範,因此能夠有多種實現,除了Hotspot外,還有諸如Oracle的JRockit、IBM的J9也都是很是有名的JVM。 java
下圖展現了JVM的主要結構: 程序員
能夠看出,JVM主要由類加載器子系統、運行時數據區(內存空間)、執行引擎以及與本地方法接口等組成。其中運行時數據區又由方法區、堆、Java棧、PC寄存器、本地方法棧組成。 數組
從上圖中還能夠看出,在內存空間中方法區和堆是全部Java線程共享的,而Java棧、本地方法棧、PC寄存器則由每一個線程私有,這會引出一些問題,後文會進行具體討論。 瀏覽器
衆所周知,Java語言具備跨平臺的特性,這也是由JVM來實現的。更準確地說,是Sun利用JVM在不一樣平臺上的實現幫咱們把平臺相關性的問題給解決了,這就比如是HTML語言能夠在不一樣廠商的瀏覽器上呈現元素(雖然某些瀏覽器在對W3C標準的支持上還有一些問題)。同時,Java語言支持經過JNI(Java Native Interface)來實現本地方法的調用,可是須要注意到,若是你在Java程序用調用了本地方法,那麼你的程序就極可能再也不具備跨平臺性,即本地方法會破壞平臺無關性。 緩存
類加載器子系統負責加載編譯好的.class字節碼文件,並裝入內存,使JVM能夠實例化或以其它方式使用加載後的類。JVM的類加載子系統支持在運行時的動態加載,動態加載的優勢有不少,例如能夠節省內存空間、靈活地從網絡上加載類,動態加載的另外一好處是能夠經過命名空間的分隔來實現類的隔離,加強了整個系統的安全性。 安全
一、ClassLoader的分類:
a.啓動類加載器(BootStrap Class Loader):負責加載rt.jar文件中全部的Java類,即Java的核心類都是由該ClassLoader加載。在Sun JDK中,這個類加載器是由C++實現的,而且在Java語言中沒法得到它的引用。 網絡
b.擴展類加載器(Extension Class Loader):負責加載一些擴展功能的jar包。 oracle
c.系統類加載器(System Class Loader):負責加載啓動參數中指定的Classpath中的jar包及目錄,一般咱們本身寫的Java類也是由該ClassLoader加載。在Sun JDK中,系統類加載器的名字叫AppClassLoader。 jvm
d.用戶自定義類加載器(User Defined Class Loader):由用戶自定義類的加載規則,能夠手動控制加載過程當中的步驟。
二、ClassLoader的工做原理
類加載分爲裝載、連接、初始化三步。
a.裝載
經過類的全限定名和ClassLoader加載類,主要是將指定的.class文件加載至JVM。當類被加載之後,在JVM內部就以「類的全限定名+ClassLoader實例ID」來標明類。
在內存中,ClassLoader實例和類的實例都位於堆中,它們的類信息都位於方法區。
裝載過程採用了一種被稱爲「雙親委派模型(Parent Delegation Model)」的方式,當一個ClassLoader要加載類時,它會先請求它的雙親ClassLoader(其實這裏只有兩個ClassLoader,因此稱爲父ClassLoader可能更容易理解)加載類,而它的雙親ClassLoader會繼續把加載請求提交再上一級的ClassLoader,直到啓動類加載器。只有其雙親ClassLoader沒法加載指定的類時,它纔會本身加載類。
雙親委派模型是JVM的第一道安全防線,它保證了類的安全加載,這裏同時依賴了類加載器隔離的原理:不一樣類加載器加載的類之間是沒法直接交互的,即便是同一個類,被不一樣的ClassLoader加載,它們也沒法感知到彼此的存在。這樣即便有惡意的類冒充本身在覈心包(例如java.lang)下,因爲它沒法被啓動類加載器加載,也形成不了危害。
由此也可見,若是用戶自定義了類加載器,那就必須本身保障類加載過程當中的安全。
b.連接
連接的任務是把二進制的類型信息合併到JVM運行時狀態中去。
連接分爲如下三步:
a.驗證:校驗.class文件的正確性,確保該文件是符合規範定義的,而且適合當前JVM使用。
b.準備:爲類分配內存,同時初始化類中的靜態變量賦值爲默認值。
c.解析(可選):主要是把類的常量池中的符號引用解析爲直接引用,這一步能夠在用到相應的引用時再解析。
c.初始化
初始化類中的靜態變量,並執行類中的static代碼、構造函數。
JVM規範嚴格定義了什麼時候須要對類進行初始化:
a、經過new關鍵字、反射、clone、反序列化機制實例化對象時。
b、調用類的靜態方法時。
c、使用類的靜態字段或對其賦值時。
d、經過反射調用類的方法時。
e、初始化該類的子類時(初始化子類前其父類必須已經被初始化)。
f、JVM啓動時被標記爲啓動類的類(簡單理解爲具備main方法的類)。
Java棧由棧幀組成,一個幀對應一個方法調用。調用方法時壓入棧幀,方法返回時彈出棧幀並拋棄。Java棧的主要任務是存儲方法參數、局部變量、中間運算結果,而且提供部分其它模塊工做須要的數據。前面已經提到Java棧是線程私有的,這就保證了線程安全性,使得程序員無需考慮棧同步訪問的問題,只有線程自己能夠訪問它本身的局部變量區。
它分爲三部分:局部變量區、操做數棧、幀數據區。
一、局部變量區
局部變量區是以字長爲單位的數組,在這裏,byte、short、char類型會被轉換成int類型存儲,除了long和double類型佔兩個字長之外,其他類型都只佔用一個字長。特別地,boolean類型在編譯時會被轉換成int或byte類型,boolean數組會被當作byte類型數組來處理。局部變量區也會包含對象的引用,包括類引用、接口引用以及數組引用。
局部變量區包含了方法參數和局部變量,此外,實例方法隱含第一個局部變量this,它指向調用該方法的對象引用。對於對象,局部變量區中永遠只有指向堆的引用。
二、操做數棧
操做數棧也是以字長爲單位的數組,可是正如其名,它只能進行入棧出棧的基本操做。在進行計算時,操做數被彈出棧,計算完畢後再入棧。
三、幀數據區
幀數據區的任務主要有:
a.記錄指向類的常量池的指針,以便於解析。
b.幫助方法的正常返回,包括恢復調用該方法的棧幀,設置PC寄存器指向調用方法對應的下一條指令,把返回值壓入調用棧幀的操做數棧中。
c.記錄異常表,發生異常時將控制權交由對應異常的catch子句,若是沒有找到對應的catch子句,會恢復調用方法的棧幀並從新拋出異常。
局部變量區和操做數棧的大小依照具體方法在編譯時就已經肯定。調用方法時會從方法區中找到對應類的類型信息,從中獲得具體方法的局部變量區和操做數棧的大小,依此分配棧幀內存,壓入Java棧。
本地方法棧相似於Java棧,主要存儲了本地方法調用的狀態。在Sun JDK中,本地方法棧和Java棧是同一個。
類型信息和類的靜態變量都存儲在方法區中。方法區中對於每一個類存儲瞭如下數據:
a.類及其父類的全限定名(java.lang.Object沒有父類)
b.類的類型(Class or Interface)
c.訪問修飾符(public, abstract, final)
d.實現的接口的全限定名的列表
e.常量池
f.字段信息
g.方法信息
h.靜態變量
i.ClassLoader引用
j.Class引用
可見類的全部信息都存儲在方法區中。因爲方法區是全部線程共享的,因此必須保證線程安全,舉例來講,若是兩個類同時要加載一個還沒有被加載的類,那麼一個類會請求它的ClassLoader去加載須要的類,另外一個類只能等待而不會重複加載。
此外爲了加快調用方法的速度,一般還會爲每一個非抽象類建立私有的方法表,方法表是一個數組,存放了實例可能被調用的實例方法的直接引用。方法表對於多態有很是重要的意義,具體能夠參照《淺談多態機制的意義及實現》一文中「多態的實現」一節。
在Sun JDK中,方法區對應了持久代(Permanent Generation),默認最小值爲16MB,最大值爲64MB。
堆用於存儲對象實例以及數組值。堆中有指向類數據的指針,該指針指向了方法區中對應的類型信息。堆中還可能存放了指向方法表的指針。堆是全部線程共享的,因此在進行實例化對象等操做時,須要解決同步問題。此外,堆中的實例數據中還包含了對象鎖,而且針對不一樣的垃圾收集策略,可能存放了引用計數或清掃標記等數據。
在堆的管理上,Sun JDK從1.2版本開始引入了分代管理的方式。主要分爲新生代、舊生代。分代方式大大改善了垃圾收集的效率。
一、新生代(New Generation)
大多數狀況下新對象都被分配在新生代中,新生代由Eden Space和兩塊相同大小的Survivor Space組成,後二者主要用於Minor GC時的對象複製(Minor GC的過程在此不詳細討論)。
JVM在Eden Space中會開闢一小塊獨立的TLAB(Thread Local Allocation Buffer)區域用於更高效的內存分配,咱們知道在堆上分配內存須要鎖定整個堆,而在TLAB上則不須要,JVM在分配對象時會盡可能在TLAB上分配,以提升效率。
二、舊生代(Old Generation/Tenuring Generation)
在新生代中存活時間較久的對象將會被轉入舊生代,舊生代進行垃圾收集的頻率沒有新生代高。
執行引擎是JVM執行Java字節碼的核心,執行方式主要分爲解釋執行、編譯執行、自適應優化執行、硬件芯片執行方式。
JVM的指令集是基於棧而非寄存器的,這樣作的好處在於可使指令儘量緊湊,便於快速地在網絡上傳輸(別忘了Java最初就是爲網絡設計的),同時也很容易適應通用寄存器較少的平臺,而且有利於代碼優化,因爲Java棧和PC寄存器是線程私有的,線程之間沒法互相干涉彼此的棧。每一個線程擁有獨立的JVM執行引擎實例。
JVM指令由單字節操做碼和若干操做數組成。對於須要操做數的指令,一般是先把操做數壓入操做數棧,即便是對局部變量賦值,也會先入棧再賦值。注意這裏是「一般」狀況,以後會講到因爲優化致使的例外。
一、解釋執行
和一些動態語言相似,JVM能夠解釋執行字節碼。Sun JDK採用了token-threading的方式,感興趣的同窗能夠深刻了解一下。
解釋執行中有幾種優化方式:
a.棧頂緩存
將位於操做數棧頂的值直接緩存在寄存器上,對於大部分只須要一個操做數的指令而言,就無需再入棧,能夠直接在寄存器上進行計算,結果壓入操做數站。這樣便減小了寄存器和內存的交換開銷。
b.部分棧幀共享
被調用方法可將調用方法棧幀中的操做數棧做爲本身的局部變量區,這樣在獲取方法參數時減小了複製參數的開銷。
c.執行機器指令
在一些特殊狀況下,JVM會執行機器指令以提升速度。
二、編譯執行
爲了提高執行速度,Sun JDK提供了將字節碼編譯爲機器指令的支持,主要利用了JIT(Just-In-Time)編譯器在運行時進行編譯,它會在第一次執行時編譯字節碼爲機器碼並緩存,以後就能夠重複利用。Oracle JRockit採用的是徹底的編譯執行。
三、自適應優化執行
自適應優化執行的思想是程序中10%~20%的代碼佔據了80%~90%的執行時間,因此經過將那少部分代碼編譯爲優化過的機器碼就能夠大大提高執行效率。自適應優化的典型表明是Sun的Hotspot VM,正如其名,JVM會監測代碼的執行狀況,當判斷特定方法是瓶頸或熱點時,將會啓動一個後臺線程,把該方法的字節碼編譯爲極度優化的、靜態連接的C++代碼。當方法再也不是熱區時,則會取消編譯過的代碼,從新進行解釋執行。
自適應優化不只經過利用小部分的編譯時間得到大部分的效率提高,並且因爲在執行過程當中時刻監測,對內聯代碼等優化也起到了很大的做用。因爲面向對象的多態性,一個方法可能對應了不少種不一樣實現,自適應優化就能夠經過監測只內聯那些用到的代碼,大大減小了內聯函數的大小。
Sun JDK在編譯上採用了兩種模式:Client和Server模式。前者較爲輕量級,佔用內存較少。後者的優化程序更高,佔用內存更多。
在Server模式中會進行對象的逃逸分析,即方法中的對象是否會在方法外使用,若是被其它方法使用了,則該對象是逃逸的。對於非逃逸對象,JVM會在棧上直接分配對象(因此對象不必定是在堆上分配的),線程獲取對象會更加快速,同時當方法返回時,因爲棧幀被拋棄,也有利於對象的垃圾收集。Server模式還會經過分析去除一些沒必要要的同步,感興趣的同窗能夠研究一下Sun JDK 6引入的Biased Locking機制。
此外,執行引擎也必須保證線程安全性,於是JMM(Java Memory Model)也是由執行引擎確保的。