什么是JVM
JVM是Java Virtual Machine(Java虛擬機)的縮寫,是一個虛構(gòu)出來的計算機,它屏蔽了與具體操作系統(tǒng)平臺相關(guān)的信息,使得Java程序只需生成在Java虛擬機上運行的目標(biāo)代碼(字節(jié)碼,ByteCode), 就可以在多種平臺上不加修改地運行。這背后其實就是JVM把字節(jié)碼翻譯成具體平臺上的機器指令,從而實現(xiàn)“一次編寫,到處運行(Write Once, Run Anywhere)”。
Java為什么能夠跨平臺?
Java引入了字節(jié)碼的概念,jvm 只能認(rèn)識字節(jié)碼,并將它們解釋到系統(tǒng)的API調(diào)用。針對不同的系統(tǒng)有不同的jvm實現(xiàn),有 Linux 版本的 jvm 實現(xiàn),也有 Windows 版本的 jvm 實現(xiàn),但是同一段代碼在編譯后的字節(jié)碼是一樣的。在不同的系統(tǒng)平臺上運行是通過JAVA解釋器將字節(jié)碼解釋為不同平臺的機器碼,在不同的 jvm 實現(xiàn)上會映射到不同系統(tǒng)的 API 調(diào)用,從而實現(xiàn)代碼的不加修改即可跨平臺運行。
JVM、JRE、JDK的關(guān)系
- JRE(Java Runtime Environment,Java運行環(huán)境),面向Java程序的使用者,而不是開發(fā)者。JRE是運行Java程序所必須環(huán)境的集合,包含JVM標(biāo)準(zhǔn)實現(xiàn)及 Java核心類庫。它包括Java虛擬機、Java平臺核心類和支持文件
- JDK(Java Development Kit,Java開發(fā)工具包),包括了Java運行環(huán)境(JRE),并提供了一堆Java工具tools.jar和Java標(biāo)準(zhǔn)類庫 (rt.jar)
三者的關(guān)系是:JDK>JRE>JVM
java虛擬機運行原理
按照階段分為兩個階段:
編譯階段:當(dāng)我們將一個.java的文件進(jìn)行編譯,編譯程序會生成一個相同名字而后綴為.class的文件。
運行階段主要分為以下步驟:

-
加載
- 通過一個類的全限定名來獲取該類的二進(jìn)制字節(jié)流
- 將這個字節(jié)流的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)運行時數(shù)據(jù)結(jié)構(gòu)
- 在內(nèi)存堆中生成一個代表該類的java.lang.Class對象,作為該類數(shù)據(jù)的訪問入口
-
驗證
驗證、準(zhǔn)備、解析這三步可以看做是一個連接的過程,將類的字節(jié)碼連接到JVM的運行狀態(tài)之中
驗證是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機的要求,不會威脅到j(luò)vm的安全,主要包括以下幾個方面的驗證:- 文件格式的驗證,驗證字節(jié)流是否符合Class文件的規(guī)范,是否能被當(dāng)前版本的虛擬機處理
- 元數(shù)據(jù)驗證,對字節(jié)碼描述的信息進(jìn)行語義分析,確保符合java語言規(guī)范
- 字節(jié)碼驗證 通過數(shù)據(jù)流和控制流分析,確定語義是合法的,符合邏輯的
- 符號引用驗證 這個校驗在解析階段發(fā)生
-
準(zhǔn)備
為類的靜態(tài)變量分配內(nèi)存,初始化為系統(tǒng)的初始值。對于final static修飾的變量,直接賦值為用戶的定義值。如下面的例子:這里在準(zhǔn)備階段過后的初始值為0,而不是7public static int a=7 -
解析
解析是將常量池內(nèi)的符號引用轉(zhuǎn)為直接引用(如物理內(nèi)存地址指針)
-
初始化
到了初始化階段,jvm才真正開始執(zhí)行類中定義的java代碼
1)初始化階段是執(zhí)行類構(gòu)造器<clinit>()方法的過程。類構(gòu)造器<clinit>()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊(static塊)中的語句合并產(chǎn)生的。
2)當(dāng)初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒有進(jìn)行過初始化、則需要先觸發(fā)其父類的初始化。
3)虛擬機會保證一個類的<clinit>()方法在多線程環(huán)境中被正確加鎖和同步。
JVM內(nèi)存分區(qū)

-
程序計數(shù)器
程序計數(shù)器(Progarm Counter Register)是一塊較小的內(nèi)存空間,它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼行號指示器。在JVM中,通過程序計數(shù)器來記錄某個線程的字節(jié)碼執(zhí)行位置,或者說記錄下一條要運行的指令。程序計數(shù)器是具備線程隔離的特性,也就是說,每個線程工作時都有屬于自己的獨立計數(shù)器,互不影響,是一塊線程私有的內(nèi)存空間。
如果當(dāng)前正在執(zhí)行的是一個java方法,程序計數(shù)器會記錄正在執(zhí)行的java字節(jié)碼地址;如果正在執(zhí)行的是native方法,則程序計數(shù)器為空。 -
Java虛擬機棧
java虛擬機棧是線程私有的內(nèi)存空間,它用來保存方法的局部變量、部分結(jié)果,并參與方法的調(diào)用和返回。
棧幀結(jié)構(gòu).png虛擬機棧在運營師采用棧幀來保存數(shù)據(jù),棧幀中主要有局部變量表、操作數(shù)棧、動態(tài)鏈接地址、返回地址等信息。每一個方法的調(diào)用都伴隨著棧幀的入棧操作,相應(yīng)的,方法的返回則對應(yīng)著棧幀的出戰(zhàn)操作。
和java棧相關(guān)的兩個異常:
- StackOverFlowError
在線程的計算過程中,如果請求的棧的深度大于最大可用的棧深度,則拋出改異常。 - OutOfMemoryError
如果java的??梢詳U展,在程序運行過程中,沒有足夠的內(nèi)存來支撐程序的擴展,則拋出該異常。
- StackOverFlowError
本地方法棧
本地方法棧和java虛擬機棧功能類似,本地方法棧主要管理本地方法棧的調(diào)用,一般是指有C實現(xiàn)的。和java虛擬機棧一樣會拋出StackOverFlowError和OutOfMemoryError異常-
方法區(qū)
方法區(qū)是java內(nèi)存區(qū)域中比較重要的一部分,主要保存的信息是元數(shù)據(jù)。其中最為重要的是類的類型信息、常量池、域信息、方法信息。
-
Java堆
Java堆可以說是Java運行時內(nèi)存中最為重要的一部分,幾乎所有的對象和數(shù)據(jù)都是在堆中分配空間的。Java堆分為新生代和老年代兩個部分,新生代用于存放剛剛產(chǎn)生的對象,如果對象一直沒有被回收,生存的足夠長,老年對象就會被移入老年代。
新生代又可以細(xì)分為eden、surivor space0(s0或者from space)和surivor space1(s1或者To space)。eden存放剛剛創(chuàng)建的對象,s0和s1存放的對象至少經(jīng)歷了一次垃圾回收,等幸存下來。如果幸存去的對象到了指定年齡仍未被回收,就會進(jìn)入老年代。
持久代:Permanent Generation。在Sun的JVM中就是方法區(qū)的意思,盡管有些JVM大多沒有這一代。主要存放常量及類的一些信息默認(rèn)最小值為16MB,最大值為64MB
堆內(nèi)存結(jié)構(gòu).jpg
垃圾收集算法
-
Mark-Sweep(標(biāo)記-清除)算法
分為“標(biāo)記”和“清除”兩個階段:首先標(biāo)記出所有需要回收的對象,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對象。
標(biāo)記清除算法.png- 缺點:空間問題,標(biāo)記清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會導(dǎo)致以后在程序運行過程中需要分配較大對象時,無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動作;
- 優(yōu)點:簡單快速
-
Copying(復(fù)制)算法
復(fù)制算法.png它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對象復(fù)制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。
- 缺點:內(nèi)存使用率只有一半
- 優(yōu)點:不會產(chǎn)生碎片
-
Mark-Compact(標(biāo)記-整理)算法
標(biāo)記過程仍然與“標(biāo)記-清除”算法一樣,但后續(xù)步驟不是直接對可回收對象進(jìn)行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內(nèi)存,
標(biāo)記整理算法.png 分代收集算法
當(dāng)前商業(yè)虛擬機的垃圾收集都采用“分代收集”(Generational Collection)算法,根據(jù)對象存活周期的不同將內(nèi)存劃分為幾塊并采用不用的垃圾收集算法。
一般是把 Java 堆分為新生代和老年代,這樣就可以根據(jù)各個年代的特點采用最適當(dāng)?shù)氖占惴?。在新生代中,每次垃圾收集時都發(fā)現(xiàn)有大批對象死去,只有少量存活,那就選用復(fù)制算法,只需要付出少量存活對象的復(fù)制成本就可以完成收集。而老年代中因為對象存活率高、沒有額外空間對它進(jìn)行分配擔(dān)保,就必須使用“標(biāo)記—清理”或者“標(biāo)記—整理”算法來進(jìn)行回收。
垃圾收集器
-
Serial收集器
串行收集器.png
新生代收集器,使用停止復(fù)制算法,使用一個線程進(jìn)行GC,串行,其它工作線程暫停。
-
ParNew收集器
ParNew收集器.png新生代收集器,使用停止復(fù)制算法,Serial收集器的多線程版,用多個線程進(jìn)行GC,并行,其它工作線程暫停,關(guān)注縮短垃圾收集時間。
Parallel Scavenge 收集器
新生代收集器,使用停止復(fù)制算法,關(guān)注CPU吞吐量,即運行用戶代碼的時間/總時間,比如:JVM運行100分鐘,其中運行用戶代碼99分鐘,垃 圾收集1分鐘,則吞吐量是99%,這種收集器能最高效率的利用CPU,適合運行后臺運算(關(guān)注縮短垃圾收集時間的收集器,如CMS,等待時間很少,所以適 合用戶交互,提高用戶體驗)。-
Serial Old收集器
serial old.png
老年代收集器,單線程收集器,串行,使用標(biāo)記整理(整理的方法是Sweep(清理)和Compact(壓縮),清理是將廢棄的對象干掉,只留幸存的對象,壓縮是將移動對象,將空間填滿保證內(nèi)存分為2塊,一塊全是對象,一塊空閑)算法,使用單線程進(jìn)行GC,其它工作線程暫停(注意,在老年代中進(jìn)行標(biāo)記整理算法清理,也需要暫停其它線程),在JDK1.5之前,Serial Old收集器與ParallelScavenge搭配使用。
-
Parallel Old收集器
Parallel Old收集器.png老年代收集器,多線程,并行,多線程機制與Parallel Scavenge差不錯,使用標(biāo)記整理(與Serial Old不同,這里的整理是Summary(匯總)和Compact(壓縮),匯總的意思就是將幸存的對象復(fù)制到預(yù)先準(zhǔn)備好的區(qū)域,而不是像Sweep(清理)那樣清理廢棄的對象)算法,在Parallel Old執(zhí)行時,仍然需要暫停其它線程。Parallel Old在多核計算中很有用。Parallel Old出現(xiàn)后(JDK 1.6),與Parallel Scavenge配合有很好的效果,充分體現(xiàn)Parallel Scavenge收集器吞吐量優(yōu)先的效果。
-
cms(concurrent mark sweep)收集器
CMS收集器.png老年代收集器,致力于獲取最短回收停頓時間(即縮短垃圾回收的時間),使用標(biāo)記清除算法,多線程,優(yōu)點是并發(fā)收集(用戶線程可以和GC線程同時工作),停頓小。使用-XX:+UseConcMarkSweepGC進(jìn)行ParNew+CMS+Serial Old進(jìn)行內(nèi)存回收,優(yōu)先使用ParNew+CMS(原因見后面),當(dāng)用戶線程內(nèi)存不足時,采用備用方案Serial Old收集。
-
G1收集器
G1收集器.png初始標(biāo)記階段僅僅只是標(biāo)記一下 GC Roots 能直接關(guān)聯(lián)到的對象,并且修改 TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序并發(fā)運行時,能在正確可用的 Region 中創(chuàng)建新對象,這階段需要停頓線程,但耗時很短。
并發(fā)標(biāo)記階段是從 GC Root 開始對堆中對象進(jìn)行可達(dá)性分析,找出存活的對象,這階段耗時較長,但可與用戶程序并發(fā)執(zhí)行。
而最終標(biāo)記階段則是為了修正在并發(fā)標(biāo)記期間因用戶程序繼續(xù)運作而導(dǎo)致標(biāo)記產(chǎn)生變動的那一部分標(biāo)記記錄,虛擬機將這段時間對象變化記錄在線程 Remembered Set Logs 里面,最終標(biāo)記階段需要把 Remembered Set Logs 的數(shù)據(jù)合并到 Remembered Set 中,這階段需要停頓線程,但是可并行執(zhí)行。
最后在篩選回收階段首先對各個 Region 的回收價值和成本進(jìn)行排序,根據(jù)用戶所期望的 GC 停頓時間來制定回收計劃,從Sun公司透露出來的信息來看,這個階段其實也可以做到與用戶程序一起并發(fā)執(zhí)行,但是因為只回收一部分 Region,時間是用戶可控制的,而且停頓用戶線程將大幅提高收集效率。通過下圖可以比較清楚地看到G1收集器的運作步驟中并發(fā)和需要停頓的階段。










