0x00:JAVA虛擬機的前世此生java
1991年,在Sun公司工做期間,詹姆斯·高斯林和一羣技術人員建立了一個名爲Oak的項目,旨在開發運行於虛擬機的編程語言,容許程序多平臺上運行。後來,這項工做就演變爲Java。隨着互聯網的普及,尤爲是網景開發的網頁瀏覽器的面世,Java[1] 成爲全球流行的開發語言。所以被人稱做Java之父。 1996年1月Sun公司發佈了Java的第一個開發工具包(JDK 1.0),2009 年Oracle收購sun後到今天己經到了JDK9。linux
java.exe是java class文件的執行程序,但實際上java.exe程序只是一個執行的外殼,它會裝載jvm.dll(linux下爲:libjvm.so),這個動態鏈接庫纔是java虛擬機。算法
JVM在哪裏?
在windows平臺上虛擬機的位置在:
%JAVA_HOME%\jre\bin\client\jvm.dll編程
%JAVA_HOME%\jre\bin\server\jvm.dllwindows
0x01:JAVA虛擬機跨平臺瀏覽器
虛擬機是一種抽象化的計算機,經過在實際的計算機上仿真模擬各類計算機功能來實現的。Java虛擬機有本身完善的硬體架構,如處理器、堆棧、寄存器等,還具備相應的指令系統。Java虛擬機屏蔽了與具體操做系統平臺相關的信息,使得Java程序只需生成在Java虛擬機上運行的目標代碼(字節碼),就能夠在多種平臺上不加修改地運行, 來一張從java原碼編譯到執行的大體流程圖吧。以下圖服務器
0x02:class文件格式分析網絡
當咱們寫好的.java 源代碼,最後會被Javac編譯器編譯成後綴爲.class的文件,該類型的文件是由字節組成的文件,又叫字節碼文件。流程圖以下所示:架構
那麼,class字節碼文件裏面究竟是有什麼呢?它又是怎樣組織的呢?讓咱們先來大概瞭解一下他的組成結構吧。併發
1. 魔數(magic):全部的由Java編譯器編譯而成的class文件的前4個字節都是「0xCAFEBABE」 它的做用在於:當JVM在嘗試加載某個文件到內存中來的時候,會首先判斷此class文件有沒有JVM認爲能夠接受的「簽名」,即JVM會首先讀取文件的前4個字節,判斷該4個字節是不是「0xCAFEBABE」,若是是,則JVM會認爲能夠將此文件看成class文件來加載並使用。
2.版本號(minor_version,major_version): JVM在加載class文件的時候,會讀取出主版本號,而後比較這個class文件的主版本號和JVM自己的版本號,若是JVM自己的版本號 < class文件的版本號,JVM會認爲加載不了這個class文件,會拋出咱們常常見到的"java.lang.UnsupportedClassVersionError: Bad version number in .class file " Error 錯誤;反之,JVM會認爲能夠加載此class文件,繼續加載此class文件。
3. 類中的method方法的實現代碼---即機器碼指令存放到這裏,JVM執行時會根據結構定位到具體的方法指令。
4.class文件結構文件在硬盤上表現以下:
上圖是用16進制工具打開時就能夠分析具體的字節碼對應在結構體中的項,JVM在執行時也是經過解析這些字節碼。
0x03:JAVA虛擬機引擎工做原理
前面咱們分析完class文件結,知道里面存放有方法Java字節碼,執行由JVM執行引擎來完成,大體流程圖以下所示:
從源到到執行大體流程圖以下所示:
一個簡單的java程序執行示例:
JVM概念上工做原理圖:
第一代引擎簡單實現:
Java虛擬機引擎和真實的計算機同樣,運行的都是二進制的機器碼;CPU指令運行流程都是經過->取指->譯碼->執行,這三個基本過程完成一個程序的執行 。下面將經過一個很是簡單的例子,帶你感覺一下Java虛擬機運行機器碼的過程和其工做的基本原理。假若有以下java代碼,計算兩個整數之和:
int run(int a, int b){
return a+b;
}
經過編譯後生成的字節碼爲0x88(表明加操做);那麼咱們要模擬java虛擬機來執行這指令時將是以下這樣(jvm主要由C、C++、彙編來開發,這裏我用C來演示):
#include(stdio.h)
int jvm(int a, int b, int opcode);
int main(){
int a = 5;
int b = 3;
int code = 0x88;
int result = jvm(a, b, code);
printf(「result = %d\n」,result);
return;
}
int jvm(int a, int b, int opcode){
if(0x88 == opcode)
return a+b;
}
這個示例程序中的jvm函數能夠當作是執行引擎了,這是世界上最精簡的小巧的虛擬機執行引擎啦,執行引擎接到操做數和指令編碼,而後判斷指令是否爲加操做,若是是就便對入棧的兩個操做數執行加法運算並返回結果。第一代jvm引擎就是這麼簡單,然而這種方式卻有一個比較大的缺陷->運行時效率低下(編譯器會產生一些沒必要要的代碼)。因此第一代java虛擬機常被吐槽。
可移植性:返種方式容易用高級語言實現,因此容易達到高可移植性 。
第二代引擎簡單實現:
第二代經過彙編精確控制 (不是直接用匯編寫的 ,經過內聯機器碼 ),其實就是使用機器碼模板方式先寫好一些功能代碼對應與class字節碼對應起來,稱爲模板解釋器
上面咱們用0x88字節碼在jvm中表示加法,若是要用匯編語言編寫實現兩個數相加時以下所示:
Push %ebp
Mov %esp, %ebp
Mov 0xc(%ebp), %eax //將操做1數從棧中取出放到eax寄存器
Mov 0x8(%ebp),%edx //將操做2數從棧中取出放到edx寄存器
Add %edx,%eax //將兩個數相加後存放在eax中
Pop %ebp
Ret
其它指令也有相似的功能代碼。
第三代引擎介紹:
雖然將字節碼對應着編寫好的彙編模板直接運行其效率相比使用C語言解釋執行,它已經提升了不少,可是一個字節碼須機對應不少的彙編代碼,指令數量增多執行時間成本必然增長,所以運行效率仍然不夠高。
爲了可以進一步提高性能,jvm在運行時將字節碼直接編譯爲對應平臺本地機器指令 ,也就是JIT編譯技術,」及時編譯」,(好比安卓5.0以上的ART模式就使用了AOT技術,安裝app時直接將字節碼編譯成對應平臺本地機器指令,運行速度就要快不少)。
動態編譯具備一些缺點,大量的初始編譯可能直接影響應用程序的啓動時間。目前jvm主要是模板解釋與編譯混合執行。 經過java –version命令能夠查看:
對只執行一次的代碼作JIT編譯再執行,能夠說是得不償失。編譯過程慢,需要詞法分析,語法分析,生成語樹,生成本地機器碼。對只執行少許次數的代碼,JIT編譯帶來的執行速度的提高也未必能抵消掉最初編譯帶來的開銷。只有對頻繁執行的代碼,JIT編譯才能保證有正面的收益。
何況,並非說JIT編譯了的代碼就必定會比解釋執行快。切不可盲目認爲有了JIT就能夠鄙視解釋器了,仍是得看實現細節如何。
缺點
可是,動態編譯確實具備一些缺點,這些缺點使它在某些狀況下算不上一個理想的解決方案。例如,由於識別頻繁執行的方法以及編譯這些方法須要時間,因此應用程序一般要經歷一個準備過程,在這個過程當中性能沒法達到其最高值。在這個準備過程當中出現性能問題有幾個緣由。首先,大量的初始編譯可能直接影響應用程序的啓動時間。
JIT編譯技術,」及時編譯」流程圖以下:
0x04:JAVA關鍵字volatile分析
Java 語言提供了一種稍弱的同步機制,即 volatile 變量.用來確保將變量的更新操做通知到其餘線程,保證了新值能當即同步。
下面來看一個示例:
JVM 能夠-client與-Server模式來運行一個程序,區別:當虛擬機運行在-client模式的時候程序啓動快, -Server模式啓動時,速度較慢,可是一旦運行起來後,性能將會有很大的提高, -Server模式要比-client性能快10%左右,緣由是模式不一樣代碼優化級別不一樣。 -Server通常在服務器使用。咱們看看同一個代碼在不一樣模式下運行狀況:
上述代碼在-client模式下運行正常結束,在-server模式運行上述代碼,永遠不會中止 。
使用jps命令能夠查看到進程一直存在,內存一直在上升。
程序比較簡單,在主線程中啓動一個線程,這個線程不停的對局部變量作自增操做,主線程休眠 1 秒中後改變啓動線程的循環控制變量,想讓它中止循環。這個程序在 client 模式下是能中止線程作自增操做的,可是在 server 模式先將是無限循環。如果改爲private volatile boolean stop;
咱們經過HSDIS反彙編插件將JVM動態生成的本地代碼還原爲彙編代碼輸出,這樣咱們就能夠經過輸出的代碼來分析問題。
HSDIS是由Project Kenai提供並獲得Sun官方推薦的HotSpot VM JIT編譯代碼的反彙編插件,做用是讓HotSpot的-XX:+PrintAssembly指令調用它來把動態生成的本地代碼還原爲彙編代碼輸出,同時還生成了大量很是有價值的註釋,這樣咱們就能夠經過輸出的代碼來分析問題。讀者能夠根據本身的操做系統和CPU類型從Kenai的網站上下載編譯好的插件,直接放到JDK_HOME/jre/bin/client和JDK_HOME/jre/bin/server目錄中便可。若是沒有找到所需操做系統(譬如Windows的就沒有)的成品,那就得本身拿源碼編譯一下,或者去HLLVM圈子中下載也能夠,這裏也有 win32 和 win64 編譯好的。
-server
-Xcomp
-XX:+UnlockDiagnosticVMOptions
-XX:CompileCommand=dontinline,*VisibilityTest.run
-XX:CompileCommand=compileonly,*VisibilityTest.run
-XX:+PrintAssembly
其中
-Xcomp 參數-Xcomp是讓虛擬機以編譯模式執行代碼,這樣代碼能夠偷懶,不須要執行足夠次數來預熱都能觸發JIT編譯。
-XX:CompileCommand=dontinline,*VisibilityTest.run 這個表示不要把 run 方法給內聯了,這是解決內聯問題。
-XX:CompileCommand=compileonly,*VisibilityTest.run 這個表示只編譯 run 方法,這樣的話只會輸出sum方法的ASM碼。
-XX:+UnlockDiagnosticVMOptions 這個參數是和 -XX:+PrintAssembly 一塊兒才能生效答應彙編代碼
看完上面彙編代碼發現Server模式代碼沒有去取stop值一直執行jmp了,Client模式下去取Stop值並作判斷了,因此才能正常結束。
解決辦法就是在變量前加volatile關鍵字,這樣在jit 編譯時就不用代碼優化了,能正常結束。
0x05:JVM虛擬機GC算法
JVM GC回收哪一個區域內的垃圾?
垃圾收集 Garbage Collection 一般被稱爲「GC」,並不是是java獨有,它誕生於1960年 MIT 的 Lisp 語言,通過半個多世紀,目前已經十分紅熟了。
jvm 中,程序計數器、虛擬機棧、本地方法棧都是隨線程而生隨線程而滅,棧幀隨着方法的進入和退出作入棧和出棧操做,實現了自動的內存清理,所以,咱們的內存垃圾回收主要集中於 java 堆和方法區中,在程序運行期間,這兩部份內存的分配和使用都是動態的。
JVM GC怎麼判斷垃圾能夠被回收了?
· 對象沒有了引用
· 發生異常
· 程序在做用域正常執行完畢
· 程序執行了System.exit()
· 程序發生意外終止(被殺線程等)
JVM GC 算法:
.引用計數器算法
.根搜索算法
.複製算法
.標記 - 清除算法
.標記 - 整理算法
.分代收集算法
下面分析算法原理:
引用計數法:
當建立對象的時候,爲這個對象在堆棧空間中分配對象,同時會產生一個引用計數器變量,同時引用計數器變量爲1,當有新的引用的時候,引用計數器繼續+1,而當其中一個引用銷燬的時候,引用計數器-1,當引用計數器被減爲零的時候,標誌着這個對象已經沒有引用了,能夠回收了!當咱們的代碼出現下面的情形時,該算法將沒法適應。
這樣的代碼會產生以下引用情形 objA指向objB,而objB又指向objA,這樣當其餘全部的引用都消失了以後,objA和objB還有一個相互的引用,也就是說兩個對象的引用計數器各爲1,而實際上這兩個對象都已經沒有額外的引用,已是垃圾了。
根搜索算法:
若是把全部的引用關係看做一棵樹,從一個節點GC ROOT開始,尋找對應的引用節點,找到這個節點之後,繼續尋找這個節點的引用節點,當全部的引用節點尋找完畢以後,剩餘的節點則被認爲是沒有被引用到的節點,即無用的節點。
標記 - 清除算法:
標記-清除算法採用從根集合進行掃描,對存活的對象對象標記,標記完畢後,再掃描整個空間中未被標記的對象,進行回收,以下圖所示。
標記-清除算法不須要進行對象的移動,而且僅對不存活的對象進行處理,在存活對象比較多的狀況下極爲高效,但因爲標記-清除算法直接回收不存活的對象, B清楚後就空出來了,致使內存不連續,所以會形成內存碎片!
內存碎片:致使內存不連續,即便空閒的內存足夠多,也不必定能分配出足夠大小的內存,同時還會下降內存分配的效率。
複製算法:
複製算法將內存劃分爲兩個區間,使用此算法時,全部動態分配的對象都只能分配在其中一個區間(活動區間),而另一個區間(備用區間)則是空閒的。
當掃描完畢活動區間後,並將存活對象複製到一塊新的,沒有使用過的空間中(備用空間),此時本來的空閒區間變成了活動區間。下次GC時候又會重複剛纔的操做,以此循環。
複製算法在存活對象比較少的時候,極爲高效,可是帶來的成本是犧牲一半的內存空間用於進行對象的移動。因此複製算法的使用場景,必須是對象的存活率很是低才行,並且最重要的是,咱們須要克服50%內存的浪費。
標記 - 整理算法:
標記-整理算法採用 標記-清除 算法同樣的方式進行對象的標記與清除,但在回收對象佔用的空間後,會將全部存活的對象移動,並更新對應的指針。標記-整理 算法是在標記-清除 算法之上,又進行了對象的移動排序整理,所以成本更高,但卻解決了內存碎片的問題。
分代收集算法:
JVM執行GC時須機中止除GC所需的線程外全部線程的執行(stop-the-world ),(這就比如當有人打掃衛生時咱們需要停下來不要隨意走動產生垃圾),JVM爲了減小GC作了不少優化,因此就有了分代收集算法,分代的垃圾回收策略,是基於這樣一個事實:不一樣的對象的生命週期是不同的。所以,不一樣生命週期的對象能夠採起不一樣的收集方式,以便提升回收效率。
新生代(Young generation)->老年代(Old generation)->持久代(Permanent generation)
對象在剛剛被建立以後,是保存在伊甸園空間的(Eden)。那些新生代Gc後存活的對象會存放倖存者空間(Survivor),倖存者空間Gc 15次後還活着的轉存到老年代空間(Old generation),持久代用於保存類常量以及字符串常量,這個區域不是用於存儲那些從老年代存活下來的對象,這個區域的對象與程序同生共死。
0x06:總結
我不認爲爲了使用好Java必須去了解Java底層的實現。許多沒有深刻理解JVM的開發者也開發出了不少很是好的應用,可是,若是你要定製與優化JVM(好比阿里淘寶jvm深度定製解決高併發)就需要深刻了解JVM原理了。
官方JVM是一個通用產品,一大目標是儘量的兼容各個平臺和知足大部分應用場景的需求。因爲開發和維護資源有限,對於特定平臺和應用場景而言,官方JVM在性能和功能上,都有取捨。好比所使用的平臺是統一的x86平臺,應用也有本身的場景特色。針對平臺和應用場景的極致的優化,能夠作得更多。
除了我以上提到的技術,JVM仍是用了其餘的不少特性和技術。因爲時間和水平有限,我沒有對它們進行講解。若是你對JVM感興趣並想要深刻學習JVM話,能夠去閱讀它的源代碼。(源代碼主要由機器碼、C/C++語言組成,源碼大概50多萬行)
源碼下載地址: http://download.java.net/openjdk/jdk8/
以上是來是網絡的學習資料總結。
歡迎關注公衆號: