當我們第一次學習Java時這些原理上的東西就會被提到,但是很少有真正去學習。今天開始從頭過一遍Java,打算從JVM開始。
1. JVM是什么
JVM是Java Virtual Mechine的縮寫。它是一種基于計算設備的規(guī)范,是一臺虛擬機,即虛構的計算機。
JVM屏蔽了具體操作系統(tǒng)平臺的信息(顯然,就像是我們在電腦上開了個虛擬機一樣),當然,JVM執(zhí)行字節(jié)碼時實際上還是要解釋成具體操作平臺的機器指令的。
通過JVM,Java實現(xiàn)了平臺無關性,Java語言在不同平臺運行時不需要重新編譯,只需要在該平臺上部署JVM就可以了。因而能實現(xiàn)一次編譯多處運行。(就像是你的虛擬機也可以在任何安了VMWare的系統(tǒng)上運行)
2. JRE和JDK
JRE:Java Runtime Environment,也就是JVM的運行平臺,聯(lián)系平時用的虛擬機,大概可以理解成JRE=虛擬機平臺+虛擬機本體(JVM)。類似于你電腦上的VMWare+適用于VMWare的Ubuntu虛擬機。這樣我們也就明白了JVM到底是個什么。
JDK:Java Develop Kit,Java的開發(fā)工具包,JDK本體也是Java程序,因此運行依賴于JRE,由于需要保持JDK的獨立性與完整性,JDK的安裝目錄下通常也附有JRE。目前Oracle提供的Windows下的JDK安裝工具會同時安裝一個正常的JRE和隸屬于JDK目錄下的JRE。
3. JVM結構
JVM主要包括:程序計數(shù)器(Program Counter),Java堆(Heap),Java虛擬機棧(Stack),本地方法棧(Native Stack),方法區(qū)(Method Area)
詳細的結構如下:

現(xiàn)在我來分別介紹一下每一部分的功能。
3.1. 程序計數(shù)器(PC, Program Counter)
是一個寄存器,可以看作是代碼行號指示器,類似于實際計算機里的PC,用于指示,跳轉下一條需要執(zhí)行的命令。Java的基礎操作以及異常處理等都十分依賴PC。
JVM多線程是通過線程輪流切換并分配處理器執(zhí)行時間的方式來實現(xiàn)的。在一個確定的時刻,一個處理器(或者說多核處理器的一個內(nèi)核)只會執(zhí)行一條線程中的命令。因此,為了正常的切換線程,每個線程都會有一個獨立的PC,各線程的PC不會互相影響。這個私有的PC所占的這塊內(nèi)存即是線程的“私有內(nèi)存”。
如果線程在執(zhí)行的是Java方法,那么PC記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令的地址。如果正在執(zhí)行的不是Java方法即Native方法,那么PC的值為undefined。
PC的內(nèi)存區(qū)域是唯一的沒有規(guī)定任何OutOfMemoryError的Java虛擬機規(guī)范中的區(qū)域。
3.2. Java虛擬機棧(Stack,Java Virtual Mechine Stacks)
同PC一樣(從工作流程圖里我們可以看到,實際上,PC也是存在于JVM Stack上的),也是線程私有的,生命周期與線程相同。虛擬機棧描述Java方法執(zhí)行的內(nèi)存模型,每個方法被執(zhí)行時都會創(chuàng)建一個棧幀(Stack Frame),棧幀會利用局部變量數(shù)組存儲局部變量(Local Variables),操作棧(Operand Stack),方法出口(Return Value),動態(tài)連接(Current Class Constant Pool Reference)等信息。
局部變量數(shù)組存儲了編譯可知的八個基本類型(int, boolean, char, short, byte, long, float, double),對象引用(根據(jù)不同的虛擬機實現(xiàn)可能是引用地址的指針或者一個handle),returnAddress類型。64位的long和double會占用兩個Slot,其余類型會占用一個Slot。在編譯期間,局部變量所需的空間就會完成分配,動態(tài)運行期間不會改變所需的空間。
操作棧在執(zhí)行字節(jié)碼指令時會被用到,這種方式類似于原生的CPU寄存器,大部分JVM把時間花費在操作棧的花費上,操作棧和局部變量數(shù)組會頻繁的交換數(shù)據(jù)。
動態(tài)連接控制著運行時常量池和棧幀的連接。所有方法和類的引用都會被當作符號的引用存在常量池中。符號引用是實際上并不指向物理內(nèi)存地址的邏輯引用。JVM 可以選擇符號引用解析的時機,一種是當類文件加載并校驗通過后,這種解析方式被稱為饑餓方式。另外一種是符號引用在第一次使用的時候被解析,這種解析方式稱為惰性方式。無論如何 ,JVM 必須要在第一次使用符號引用時完成解析并拋出可能發(fā)生的解析錯誤。綁定是將對象域、方法、類的符號引用替換為直接引用的過程。綁定只會發(fā)生一次。一旦綁定,符號引用會被完全替換。如果一個類的符號引用還沒有被解析,那么就會載入這個類。每個直接引用都被存儲為相對于存儲結構(與運行時變量或方法的位置相關聯(lián)的)偏移量。
對Java虛擬機棧這個區(qū)域,Java虛擬機規(guī)范規(guī)定了兩種異常:
- 線程請求的棧深度大于虛擬機所允許的深度,拋出StackOverFlow異常。
- 對于支持動態(tài)擴展的虛擬機,當擴展無法申請到足夠的內(nèi)存時也會拋出StackOverFlow異常。
3.3. 本地方法棧(Native Stack)
本地方法棧如其名字,和Java Virtual Machine Stack其實極為類似,只是執(zhí)行的是Native方法,為Native方法服務。在JVM規(guī)范中,沒有對它的實現(xiàn)做具體規(guī)定。
3.4. Java 堆(Heap, Garbage Collection Heap)

Java堆是被所有線程共享的一塊區(qū)域,在虛擬機啟動時創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內(nèi)存(隨著技術的發(fā)展,已不絕對)。
Java堆是垃圾收集器管理的主要區(qū)域,因而也被稱為GC堆。收集器采用分代回收法,GC堆可以分為新生代(Yong Generation)和老生代(Old Generation)。新生代包括Eden Space和Survivor Space。但無論哪個區(qū)域,如何劃分,存儲的都是Java對象實例,進一步的劃分是為了更好的回收內(nèi)存或快速的分配內(nèi)存。
根據(jù)Java虛擬機規(guī)范,堆所在的物理內(nèi)存區(qū)間可以是不連續(xù)的,只要邏輯連續(xù)就可以。實現(xiàn)時既可以是固定大小,也可以是可擴展的。如果堆無法擴展時,就會拋出OutOfMemoryError。
3.5. 方法區(qū)(Method Area)
方法區(qū)和Java堆類似,也屬于各線程共享的內(nèi)存區(qū)域。用于存儲已被虛擬機加載的類信息,常量,靜態(tài)變量,即時編譯器編譯后的代碼數(shù)據(jù)等。它屬于非堆區(qū)(Non Heap),和Java堆區(qū)分開。對于存在永久代(Permanent)概念的虛擬機(HotSpot)而言,方法區(qū)存在于永久代。Java虛擬機規(guī)范對方法區(qū)的規(guī)定很寬松,甚至可以不實現(xiàn)GC。不過并非進入方法區(qū)的數(shù)據(jù)就會永久存在了,這塊區(qū)域的內(nèi)存回收主要為常量池的回收和類型的卸載。這個區(qū)域的回收處理不善也會導致嚴重的內(nèi)存泄漏。
當方法區(qū)無法滿足內(nèi)存分配需求時也會拋出OutOfMemoryError。
3.6. 代碼緩存(Code Cache)
用于編譯和存儲那些被 JIT 編譯器編譯成原生代碼的方法。
3.7. 類信息(Class Data)
類信息存儲在方法區(qū),其主要構成為運行時常量池(Run-Time Constant Pool)和方法(Method Code)。
一個編譯后的類文件包括以下結構:
| 結構 | 解釋 |
|---|---|
| magic, minor_version, major_version | 類文件的版本信息和用于編譯這個類的 JDK 版本。 |
| constant_pool | 類似于符號表,盡管它包含更多數(shù)據(jù)。下面有更多的詳細描述。 |
| access_flags | 提供這個類的描述符列表。 |
| this_class | 提供這個類全名的常量池(constant_pool)索引,比如org/jamesdbloom/foo/Bar。 |
| super_class | 提供這個類的父類符號引用的常量池索引。 |
| interfaces | 指向常量池的索引數(shù)組,提供那些被實現(xiàn)的接口的符號引用。 |
| fields | 提供每個字段完整描述的常量池索引數(shù)組。 |
| methods | 指向constant_pool的索引數(shù)組,用于表示每個方法簽名的完整描述。如果這個方法不是抽象方法也不是 native 方法,那么就會顯示這個函數(shù)的字節(jié)碼。 |
| attributes | 不同值的數(shù)組,表示這個類的附加信息,包括 RetentionPolicy.CLASS 和 RetentionPolicy.RUNTIME 注解。 |
3.8. 運行時常量池(Run-Time Constant Pool)
運行時常量池是方法區(qū)的一部分。Class文件中有類的版本,字段,方法,接口等描述信息和用于存放編譯期生成的各種字面量和符號引用。這部分內(nèi)容將在類加載后存放到方法區(qū)的運行時常量池中。Java虛擬機規(guī)范對Class的細節(jié)有著嚴苛的要求而對運行時常量池的實現(xiàn)不做要求。一般來說除了翻譯的Class,翻譯出來的直接引用也會存在運行時常量池中。
運行時常量池具備動態(tài)性,即運行時也可將新的常量放入池中。比如String類的intern()方法。
常量池無法申請到足夠的內(nèi)存分配時也會拋出OutOfMemoryError。
3.9. 直接內(nèi)存(Direct Memory)
直接內(nèi)存并不在Java虛擬機規(guī)范中,不是Java的一部分,但是也被頻繁使用并可能導致OutOfMemoryError。Native函數(shù)庫可以直接分配堆外內(nèi)存,通過存儲在Java堆里的DirectDataBuffer對象作為這塊內(nèi)存的引用進行操作。這樣做在一些場景中可以顯著提高性能。
直接內(nèi)存是堆外內(nèi)存,自然不受Java堆大小的限制,但是可能受實體機內(nèi)存大小的限制。如果內(nèi)存各部分總和大于實體機的內(nèi)存時,也會報出OutOfMemoryError。
4. Java垃圾回收
將內(nèi)存中不再被使用的對象進行回收,GC中用于回收的方法稱為收集器,由于GC需要消耗一些資源和時間,Java在對對象的生命周期特征進行分析后,按照新生代、舊生代的方式來對對象進行收集,以盡可能的縮短GC對應用造成的暫停。
不同的對象引用類型, GC會采用不同的方法進行回收,JVM對象的引用分為了四種類型:
- 強引用:默認情況下,對象采用的均為強引用(這個對象的實例沒有其他對象引用,GC時才會被回收)。
- 軟引用:軟引用是Java中提供的一種比較適合于緩存場景的應用(只有在內(nèi)存不夠用的情況下才會被GC)。
- 弱引用:在GC時一定會被GC回收。
- 虛引用:由于虛引用只是用來得知對象是否被GC。
5. JVM線程與原生線程的關系
JVM允許一個程序使用多個并發(fā)線程,Hotspot JVM中Java的線程與原生操作系統(tǒng)的線程是直接映射關系。即當線程本地存儲、緩沖區(qū)分配、同步對象、棧、程序計數(shù)器等準備好以后,就會創(chuàng)建一個操作系統(tǒng)原生線程。Java 線程結束,原生線程隨之被回收。操作系統(tǒng)負責調度所有線程,并把它們分配到任何可用的 CPU 上。當原生線程初始化完畢,就會調用 Java 線程的 run() 方法。run() 返回時,被處理未捕獲異常,原生線程將確認由于它的結束是否要終止 JVM 進程(比如這個線程是最后一個非守護線程)。當線程結束時,會釋放原生線程和 Java 線程的所有資源。