虛擬機
何爲虛擬機呢?虛擬機是模擬執行某種指令集體系結構(ISA)的軟件,是對操做系統和硬件的一種抽象。其軟件模型以下圖所示:java
計算機系統的這種抽象相似於面向對象編程(OOP)中的針對接口編程泛型(或者是依賴倒轉原則),經過一層抽象提取底層實現中共性的部分,底層實現這個抽象並完成本身個性的部分。也就是說經過一個抽象層次來隔離底層的不一樣實現。虛擬機規範定義了這個虛擬機要完成的功能(也就是接口),底層的操做系統和硬件利用本身提供的功能來實現虛擬機須要完成的功能(實現)。經過運行在虛擬機之上,Java才具備很好跨平臺特性。python
Java虛擬機
Java虛擬機(JVM)是由Java虛擬機規範定義的,其上運行的是字節碼指令集。這種字節碼指令集包含一個字節的操做碼(opcode),零至多個操做數(oprand),虛擬機規範明肯定義了每種字節碼指令完成的功能是什麼以及須要多少個操做數。Java虛擬機上運行的class文件,這個文件中包含字節碼指令流以及類定義的信息,因此Java虛擬機規範還定義了class文件的格式(精確到每一個字節)。因此實現Java虛擬機的兩個要素是字節碼指令集和class文件格式,Java虛擬機的實現者只要以正確方式讀取class文件中的每一條字節碼指令,並按照要求實現字節碼指令的功能就能夠實現JVM。程序員
目前經常使用的商用JVM主要有:Sun HotSpot,BEA JRocket以及IBM J9。其中因爲BEA和Sun已經被Oracle收購,因此Oracle擁有當今世界上最流行的兩個JVM,並有傳言說Oracle將在Java8時將兩個虛擬機合併,各取所需,取長補短,打造一個更加精湛的JVM。HotSpot會以解釋+即時編譯執行代碼,HotSpot在解釋執行字節碼的時候,會探測熱點(hotspot)代碼,而後將這部分代碼編譯爲本地代碼,以後將直接運行本地代碼,而不是解釋,這樣會有效提升虛擬機性能。JRocket主要是定位於服務器應用,因此不關注虛擬機的啓動速度,它會將全部代碼即時編譯爲本地代碼執行,JRocket的垃圾收集器具備很高的收集效率。J9定位與HotSpot相似,專一於桌面應用和服務器應用,主要是針對IBM的各類Java產品。算法
Java語言與Java虛擬機
咱們知道Java源代碼,即.java文件,經過javac編譯爲.class文件。.class文件能夠運行在JVM上,JVM底層會經過字節碼解釋器或者即時編譯器(JIT Compiler)執行.class文件中的字節碼指令。JVM是運行在操做系統之上的,操做系統又經過指令集調用底層硬件服務執行其上的各類軟件。編程
能夠看到Java是運行在JVM之上的。可是Java語言和JVM沒有必然的聯繫。Java語言並非只能運行在JVM之上,只要實現了相應的編譯器Java語言就能夠運行在任何平臺之上(好比J++),也能夠被編譯爲本地代碼直接運行在操做系統之上,好比,Linux上的GCJ(GNU Compiler for Java)就能夠把Java語言編譯爲本地代碼直接執行。一樣的,JVM上也不是隻能執行Java語言,只要實現了適當的編譯器,將其餘語言編譯爲JVM上的字節碼,就能夠在JVM上運行。好比,JRuby,Jython以及Groovy等其餘JVM語言,都會經過相應的編譯器或是解釋器轉化爲.class,而後再JVM上運行。因爲JVM並不關心.class文件是由Java、JRuby、Jython等轉化而來,只要這個文件結構正確並能經過class文件校驗。所以,因爲.class文件屏蔽了Java、JRuby等上層語言的差別,因此Java、Groovy等能夠相互調用。windows
JVM生命週期
當啓動一個Java程序時,一個虛擬機實例就誕生了,當程序關閉退出時,這個虛擬機實例隨之消亡。JVM實例經過main()方法來運行一個Java程序。而這個main()方法必須是共有的(public)、靜態的(static)、返回void,而且接收一個字符串數組爲參數。Java程序初始類中的main()方法,將做爲改程序初始線程的起點,任何其餘線程都是由這個初試線程啓動的。數組
JVM內部有兩種線程:守護線程與非守護線程。守護線程一般是由虛擬機本身使用的,好比垃圾回收線程。當該程序全部的非守護線程都終止時,JVM實例將自動退出。安全
Java虛擬機體系結構
JVM由類加載器子系統,運行時數據區,執行引擎以及本地方法接口組成。服務器
類加載器子系統數據結構
類加載器子系統主要用於定位類定義的二進制信息,而後將這些信息解析並加載至虛擬機,轉化爲虛擬機內部的類型信息的數據結構。類加載器子系統還承擔着安全性的責任,而且是JVM的動態連接和動態加載的基礎。將二進制信息=>類型信息的數據結構,中間須要通過不少步驟。首先類加載器是JVM安全沙箱的第一道防線,可以防止非信任類破壞虛擬機。每個被加載的class文件須要通過四次校驗才能被加載。校驗經過後,類加載器的命名空間和運行時包的特性可以防止非信任類假裝成信任類來破壞虛擬機。類加載器在方法區構造具備這個類的信息的數據結構後,會在堆上建立一個Class對象做爲訪問這個數據結構的接口。同時,類加載還須要初始化類的靜態數據,也就是調用類的方法。以上就是一個類的加載、連接及初始化的過程。
運行時數據區
運行時數據區是JVM運行時的內存空間的組織,邏輯上又劃分爲多個區,這些區的生命週期和它是否線程共享有關,它們分別是:
堆
用於存放對象或數組實例,也就是運行期間new出來的對象。堆的生命週期與JVM相同,而且在線程之間共享訪問。因爲多線程併發訪問,因此須要考慮線程安全的問題,有兩種方法。第一種是,加鎖進行互斥訪問。第二種是線程本地分配緩衝(Thread Local Allocate Buffer, TLAB),在線程建立時預先給每一個線程分配一塊區域,這塊區域是線程私有的,對其餘線程是不可見,也就不會被共享。JVM規範規定在申請不到足夠的內存時,堆會拋出OutOfMemoryException。
方法區
存放類型信息和運行時常量池(Runtime Constant Pool)。每一個被類加載器加載的類都會在方法區中造成一個與子對應的類型信息的數據結構,包括:這個類的類名、直接超類、實現的接口列表、字段列表、方法列表等。運行時常量池是class文件中的常量池列表(Constant Pool List)在運行時的一種體現,其中存儲各類基本數據類型及String類型的常量以及其餘類、方法、字段的符號引用。方法區的生命週期與JVM相同,被多個線程共享,因此要考慮併發訪問的安全性的問題。JVM規範規定在須要的內存得不到知足的狀況下,方法區會拋出OutOfMemoryException。
PC(Program Counter)
線程私有的,生命週期與線程相同,是對CPU中PC的一種模擬。若是線程正在執行的是Java方法,則該線程的PC中存放的下一條字節碼指令的地址。在進行Java方法的調用和返回時,須要更新PC以保存當前方法(Current Method)正在執行的字節碼指令的地址。PC是JVM規範中惟一沒有規定會拋出異常的存儲區。
JVM棧
線程私有,生命週期與線程相同,是對傳統語言(好比C)中的方法調用棧的一種模擬。JVM棧中存放棧幀(Frame)用於進行方法調用和返回、存儲局部變量以及計算的中間結果。JVM規範規定棧能夠拋出兩種異常:(1)StackOverflowException,在棧的深度大於某個規定值的狀況下拋出。(2)OutOfMemoryException,在爲新棧幀分配內存或者是爲線程分配棧的內存時,申請不到足夠的內存的狀況下拋出。
JVM棧中存放的是棧幀,每一個棧幀對應着一次方法調用。每一時刻,JVM線程只能執行一個方法(Current Method),該方法的棧幀是JVM棧的棧頂的元素(叫作當前棧幀,Current Frame),當調用一個方法時,會初始化一個棧幀壓入JVM棧;當方法調用返回或者拋出異常沒有被處理的狀況下,JVM棧會彈出該方法對應的棧幀。每個棧幀中存放局部變量表(Local Variable Table)、操做數棧(Oprand Stack)以及其餘棧幀信息。棧幀的大小在編譯時就肯定了,編譯器會把局部變量表和操做數棧的大小記錄在class文件中method_info的屬性表中。局部變量表相似於數組存放局部變量和方法參數。因爲JVM採用的是基於棧的指令集體系結構,而不是基於寄存器,因此JVM上的全部計算都是在操做數棧上進行的(好比,算術運算、方法調用、內存訪問等)。
本地方法棧
用於支持本地方法調用,拋出的異常與JVM棧相同。
執行引擎
執行引擎用於執行JVM字節碼指令,主要由兩種實現方式:
(1)將輸入的字節碼指令在加載時或執行時翻譯成另一種虛擬機指令;
(2)將輸入的字節碼指令在加載時或執行時翻譯成宿主主機本地CPU的指令集。這兩種方式對應着字節碼的解釋執行和即時編譯。好比在HotSpot VM中執行引擎的實現是一種解釋-編譯的層次結構:
(1)解釋執行:解釋執行字節碼,並以方法爲單位收集「熱點(HotSpot)代碼」的信息,將「熱點代碼」執行C0編譯。
(2)C0編譯:將收集的「熱點代碼」編譯成本地代碼,並進行一些簡單的優化。繼續收集運行時信息,將一些頻繁執行的本地代碼進行C1編譯。
(3)C1編譯:將C0階段的本地代碼,進行一些比較激進的優化。若是某些優化致使本地代碼執行失敗,此時JVM會退化到解釋執行字節碼階段。
自動內存管理
自動內存管理用於管理運行時數據區的分配和釋放。和C和C++相比,Java不須要程序員主動的管理內存(在new出對象後,不須要顯示的delete),這樣JVM就須要承擔內存管理這個任務。內存管理的重點主要是在申請內存(new對象、類加載和初始化、啓動線程時初始化棧等)得不到知足時,JVM能夠自動回收那些再也不存活的對象所佔用的內存,也就是常常聽到的垃圾收集。在回收過程當中還要保證處理內存空間的碎片,以提升空間利用率。回收過程主要有兩個關鍵點,標記存活對象和回收內存的算法。
標記存活對象主要有引用計算和根搜索法兩種。
(1)引用計數,是一種很廣泛的方法,在python、lua等一些腳本語言中都是使用這種算法。每一個對象持有一個計數器,標記這個對象被引用的次數。進行垃圾收集時,那些引用計數爲0的對象就是「死」對象,須要被收集。引用計數的一個缺點就是它沒有辦法處理循環引用的狀況(A->B, B->A)。
(2)根搜索,HotSpot虛擬機採用這種算法標記存活對象。把方法區、JVM棧中的全部的引用組成的集合做爲搜索的根,從這個集合開始遍歷直到結束。其中被遍歷到的對象是存活對象;那些沒有被遍歷到的對象須要被垃圾收集。這樣能夠有效的避免循環引用的狀況。
回收內存的算法主要有:
(1)複製算法,將內存分紅兩個部分,每一時刻只是用其中的一個。進行回收時,將全部存活的對象依次複製到另外一個部分(依次複製避免了內存碎片的產生),接下來只用這一個部分。複製算法須要在兩個內存區域來回複製,有必定的複製開銷和空間開銷(每一時刻只使用一個區域),可是能夠很好的解決內存碎片的問題,適用於對象頻繁建立而且生命週期短的狀況。
(2)標記清掃,先進行存活對象標記,回收時將「死」對象佔用的內存直接釋放掉,會產生大量的內存碎片。
(3)標記整理,標記階段與標記清掃算法同樣,回收階段釋放「死」對象的內存後,還須要進行對象的移動使得全部對象依次在內存中排列,避免了內存碎片的產生。標記整理與複製算法相反,適用於對象建立不頻繁,生命週期長得狀況。
(4)按代收集,將內存按照對象生命週期的不一樣劃分爲多個部分,每一個部分採用不一樣的收集算法。目前,大部分商業虛擬機都是採用這種算法。好比,在HotSpot中,內存被劃分爲:新生代(New)、老年代(Old)和永久代(Perm)。新生代採用複製算法,老年代和永久代採用標記整理算法。內存分配、回收的策略是,對象首先在新生代分配,若是新生代內存不知足要求,則觸發一次新生代內存的垃圾收集(Young GC,或者是Minor GC)。Young GC會致使部分新生代的對象被移動至老年代,一部分是由於新生代內存不足以放下全部的對象;另外一部分是由於這些對象的年齡(每一個對象都保存着這個對象被垃圾收集的次數,表示它的年齡。存儲在對象頭的age屬性中)大到足以晉升到老年代。當新生代的對象進入老年代,而老年代的內存不知足要求時,則會觸發一次整個新生代和老年代的垃圾收集(Full GC, 或者是Major GC)。
在JVM中有多個後臺線程用於完成自動內存管理,對於CPU來講這些後臺線程和用戶線程是同樣的,都須要佔用系統的資源。在GC線程進行垃圾收集時必須執行「Stop the World」這一操做,也就是暫停全部的用戶線程。這就致使對於實時性要求比較高的系統,JVM的垃圾收集多是一個短板。可是在JDK1.5,Sun提供了CMS(Concurrent Mark and Sweep)垃圾收集器,經過GC線程和用戶線程併發執行減小GC時間,提升了JVM的實時性。在JVM的各類應用中,gc調優是一個關鍵的部分,主要目標是減小GC的次數而且下降每次GC的時間。關於這部份內容,後續的JVM內存管理會詳細討論。
JVM執行程序的流程分析
在命令行執行」java Main」就會開啓一個JVM實例,咱們能夠經過jps,jstat等JVM工具觀察JVM的運行狀態,下面以運行com.ntes.money.Main這個類爲例來描述一下JVM執行一個程序的流程。
當在命令行執行」java -Xmx=12m -Xms=12m -Dname=value com.ntes.money.Main」這個命令時,JVM的執行流程是:
1)加載JVM,主要是加載動態連接庫,windows下是jvm.dll,Linux下是libjvm.so;
2)設置JVM啓動參數,好比命令中的-Xmx=12m -Xms=12m用於設置堆大小。
3)初始化JVM。
4)調用類加載器子系統,加載com.ntes.money.Main。這裏給出的是自定義類,根據類加載器雙親委派鏈,最後是由系統默認類加載器(Classpath類加載器)進行加載。首先,根據全路徑類型轉化爲文件路徑com/ntes/money/Main.class,而後讀取Main.class中的二進制信息、解析、加載,在方法區中造成Main類對應的數據結構。這裏可能拋出ClassNotFoundException,有兩種緣由。一是文件路徑com/ntes/money/Main.class不存在;二是com/ntes/money/Main.class文件路徑存在,可是Main.class文件中存儲的不是Main類的信息,好比是Main1,Main2等其餘類的信息。這種狀況下,會拋出NoClassDefFoundError,而後致使ClassNotFoundException。
5)在方法區com.ntes.money.Main類對應的數據結構中,根據方法描述符及訪問標誌,查找main方法。這裏的描述符,包括了方法的方法名、參數、返回值,也就是public static void main(String[])。若是找不到對應的main方法,會拋出NoSuchMethodError: main異常。
6)經過本地方法(JNI)執行main方法。
更多資料,私信小編「資料」即可領取。
但願你幫小編點贊和轉發哦~