讀書筆記,如需轉載,請註明做者:Yuloran (t.cn/EGU6c76)html
《Java 虛擬機規範》讀書筆記,部份內容摘自 The Java® Virtual Machine Specification Java SE 11 Edition,Java Garbage Collection Basics ,部份內容摘自《深刻理解Java虛擬機_JVM高級特性與最佳實踐 第2版》(周志朋著)。java
首先,我們要明確一個概念:《Java 虛擬機規範》是獨立於具體的編程語言以及具體的虛擬機實現的,Java 只是你們最爲熟悉的一種 JVM 編程語言而已,目前比較火的其它 JVM 語言 還有:web
實現這種語言無關性的基石就是平臺無關的程序存儲格式 - 字節碼(ByteCode),即二進制文件 *.Class:算法
另外,《Java 虛擬機規範》也沒有說明 GC 該如何實現,因此在闡述 Java 虛擬機的 GC 算法、GC 收集器時,應當指明具體的虛擬機。原文:編程
THIS document specifies an abstract machine. It does not describe any particular implementation of the Java Virtual Machine. To implement the Java Virtual Machine correctly, you need only be able to read the class file format and correctly perform the operations specified therein. Implementation details that are not part of the Java Virtual Machine's specification would unnecessarily constrain the creativity of implementors. For example, the memory layout of run-time data areas, the garbage-collection algorithm used, and any internal optimization of the Java Virtual Machine instructions (for example, translating them into machine code) are left to the discretion of the implementor.數組
譯文:數據結構
本文闡述了一個抽象機器。並未闡述任何 Java 虛擬機的特定實現。爲了正確實現虛擬機,你只須要可以讀取 Class 文件格式並執行其中的指定操做便可。實現細節並非 Java 虛擬機規範的一部分,由於這可能會約束實現者的創造力。好比,運行時數據區的內存佈局、GC 算法以及任何 Java 虛擬機指令集的內部優化(好比,翻譯成機器碼),都由實現者自行決定。多線程
收購 Sun 使 Oracle 有了兩種主要的 Java 虛擬機 (JVM) 實現,即 Java HotSpot VM 和 Oracle JRockit JVM。未做特殊說明,本文皆以 HotSpot JVM 爲例來闡述《Java 虛擬機規範》的具體實現。架構
HotSpot 虛擬機是 Java SE 平臺的一個核心組件,是 Java 虛擬機規範的實現之一,並做爲 JRE 的一個共享庫來提供。做爲 Java 字節碼執行引擎,它在多種操做系統和架構上提供 Java 運行時設施,如線程和對象同步。它包括自適應將 Java 字節碼編譯成優化機器指令的動態編譯器,並使用爲下降暫停時間和吞吐量而優化的垃圾收集器來高效管理 Java 堆。
HotSpot 虛擬機可根據平臺配置,選擇合適的編譯器、Java 堆配置和垃圾收集器,以保證爲大多數應用程序提供優良性能。下圖爲 HotSpot 虛擬機的架構:
其主要組件包括:Class Loader, Runtime Data Areas 和 Execution Engine.
注:上圖 Run-Time Data Areas
的 Java Threads
指的是 Java Virtual Machine Stacks
,Native Internal Threads
指的是 Native Method Stacks
。
爲了便於理解,本人結合 JVM 規範,將上圖的 Runtime Data Areas 從新繪製以下:
上圖爲 JVM 規範的闡述,無關具體的虛擬機。好比,Native Method Stacks 可能不存在,HotSopt JVM 就將 JVM Stacks 與 Native Method Stacks 合二爲一了。
Java 虛擬機支持多線程併發執行,每一個線程都有本身的程序計數器(Program Counter Register)。任何肯定的時刻,JVM 線程只能執行一個方法,稱爲當前方法。若是當前方法是 Java 方法,那麼程序計數器裏存儲的就是 JVM 當前正在執行的指令的地址。若是當前方法是本地方法,程序計數器的值則爲空(Undefined)。程序計數器是一塊較小的內存空間,能夠看做是當前線程所執行的字節碼的行號指示器,也是惟一一個不會產生 OutOfMemoryError 的區域。
每一個 JVM 線程都用於一個私有的 Java 虛擬機棧,它隨着進程的建立而分配,隨着進程的退出而銷燬。Java 虛擬機棧,描述的是 Java 方法執行的內存模型,因此也能夠稱之爲 Java 方法棧。每次 Java 方法執行時都會建立一個棧幀,棧幀是描述虛擬機進行方法調用和方法執行的數據結構,用於存儲方法的局部變量表、操做數棧、動態鏈接、方法返回地址和一些附加信息。方法從調用至完成的過程,對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。
在編譯程序代碼的時候,棧幀中須要多大的局部變量表,多深的操做數棧都已經徹底肯定,並寫入到方法表的 Code 屬性之中。所以一個棧幀須要分配多少內存,不會受到程序運行期變量數據的影響,僅取決於具體的虛擬機實現。
用於存儲方法參數和方法內定義的局部變量,以 Variable Slot 爲最小單位,能夠存放一個 boolean、byte、char、short、int、float、reference 或 returnAddress 類型(可按照 Java 語言對應的類型理解,但本質上是不同的)的數據。
在 Java 程序編譯爲 Class 文件時,就在方法的 Code 屬性的 max_locals 數據項中肯定了該方法所須要分配的局部變量表的最大容量。
也常稱爲操做棧,其最大深度在編譯期寫入 Code 屬性的 max_stacks 中。棧元素能夠是 Java 語言的任意數據類型。在方法剛執行時,該棧是空的。在方法執行過程當中,會有各類字節碼指令對操做棧進行讀寫操做,好比算術運算是經過操做棧來完成的,或者調用其它方法時,使用操做棧來傳遞參數。
字節碼中的方法調用指令以常量池中指向該方法的符號引用做爲參數,這些符號引用有一部分會在類加載階段或第一次使用時轉爲直接引用,還有一部分在運行期才轉爲直接引用,這稱爲動態鏈接。每一個棧幀中都包含一個指向運行時常量池中該棧幀所屬方法的引用,以實現動態鏈接。
方法退出後,須要返回方法被調用的位置。方法正常退出時,調用者的 PC 值可做爲返回地址,棧幀中極可能保存了這個值。方法異常退出時,返回地址須要經過異常處理器表來肯定,棧幀中通常不會保存這部分信息。
虛擬機規範裏沒有描述的信息,好比調試信息等。
可能產生的異常:
與 Java 虛擬機棧很是類似,只不過是描述 Java 本地方法執行的內存模型,因此稱之爲本地方法棧。雖然虛擬機規範沒有規定本地方法棧中方法使用的語言、使用方式和與數據結構,不過通常指 C 語言。說到這兒,就不得不說,其實從進程角度來看,不管什麼編程語言編寫的程序,其內存模型或者說內存佈局都差很少。下圖爲 Linux 進程中,C 程序的內存佈局:
Java 本地方法棧在功能上就相似於 C 程序的 C Stack。一樣,Java 本地方法棧也會拋出 StackOverflowError 和 OutOfMemoryError。
Java 方法區是全部線程共享的,在功能上相似於 C 程序的 Text Segment。該區域存儲了運行時常量池、已加載的 Class、靜態變量的引用、成員變量和成員方法的引用,以及即時編譯器(JIT Compiler)編譯後的代碼等數據。
方法區在虛擬機啓動時建立。雖然邏輯上屬於 Java Heap(GC重點區域),可是能夠不實現垃圾回收或者壓縮整理。方法區的大小能夠是固定的、動態擴展的和可壓縮的(無需這麼大的方法區時),並且內存不要求連續。
HotSpot 虛擬機將 GC 分代收集擴展到了方法區,或者說他們用永久(Permanent Generation)代實現了方法區。這會出現一些問題,一是方法區的 GC 沒什麼效果,二是永久代內存有上限,容易出現 OOM,三是極少數方法會由於這個緣由在不一樣虛擬機下有不一樣表現,好比String.intern():
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
String str1 = new StringBuilder("計算機").append("軟件").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}
}
複製代碼
這段代碼在 JDK 1.6 中運行會獲得兩個 false,在 JDK 1.7 中運行,會獲得一個 true,一個 false。緣由是:JDK 1.6 中會將首次遇到的字符串實例複製到永久代,返回的也是永久代中這個字符串實例的引用,而 StringBuilder 建立的字符串實例位於 Java 堆,因此必然不是同一個引用。而 JDK 1.7(已經將字符串常量池移出了永久代) 的 intern() 則不會再複製實例,只是在常量池中記錄首次出現的實例引用,因此 intern() 返回的引用和 StringBuilder 建立的字符串實例的引用是同一個。而 」java「 這個字符串虛擬機啓動時,已經由 JDK 的 Version 類加載到常量池了,因此不是同一個引用。
方法區沒法知足內存分配需求時,將會拋出 OutOfMemoryError。
運行時常量池是方法區的一部分。Class 文件除了有類的版本、字段、方法、接口等描述信息,還有一項信息是常量池(Constant Pool Table),用於存放編譯期生成的各類字面量(整型字面量、字符串字面量等)和符號引用,相似於 C 程序的 Symbol Table,不過數據類型要比 C 語言豐富的多,這部份內容將在類加載後,進入方法區的常量池。
Java 虛擬機對 Class 文件的每一部分都有嚴格規定,每個字節用於存放哪一種數據都必須符合規範才能被虛擬機承認、裝載和執行。可是對於運行時常量池卻沒有任何細節要求, 因此運行時常量池被實現成了具有動態性,即常量也能夠在運行時生成,好比 String 的 intern() 方法。
當運行時常量池沒法再申請到內存時,會拋出 OutOfMemoryError。
Java 堆是全部 JVM 線程共享的區域。全部類的實例以及數組都在堆上分配。
Java 堆在虛擬機啓動時建立。堆上的對象由垃圾收集器(Garbage Collector)自動管理。
從內存回收角度看,因爲如今垃圾收集器大多采用分代收集算法,因此 Java 堆還能夠細分爲年輕代(Young Generation)和老年代(Old Generation),其中年輕代還能夠按 8:1:1 的比例再分爲 Eden、From Survivor、To Survivor。
從內存分配角度看,Java 堆可能劃分出多個線程私有的內存分配緩衝區(Thread Local Allocation Buffer,TLAB)。
Java 堆內存不要求物理上連續,只要邏輯上連續便可。若是堆中沒有內存完成實例分配,且堆也沒法繼續擴展時,將拋出 OutOfMemoryError。
不像 C 或 C++,內存由開發人員手動分配和釋放,JVM 中的內存由垃圾收集器自動管理,基本步驟以下:
標記哪些對象是被使用的,哪些是再也不使用的:
刪除再也不引用的對象,保留存活的對象,並維護一個空閒內存的引用列表 :
爲了提升性能,有些收集器使用 "標記-清除-整理" 算法來回收內存。將仍然存活的對象移至一端,以便下次更容易找到連續可用內存:
使用 "標記-清除" 法回收對象的效率是比較低的,尤爲是在對象愈來愈多的時候,將須要更長的時間來執行垃圾回收。這是很恐怖的,由於 GC 觸發時,Java 程序須要被凍結,不然對象的引用關係將沒法追蹤。研究代表,大部分對象的存活時間都很短。因此如今的 JVM 大多采用分代收集算法來提升性能:
全部新對象分配和變老的地方。年輕代用完時,將會觸發一次 Minor Garbage Collection。GC 後,仍然存活的對象會變老並最終進入老年代。年輕代使用 "標記-複製" 法進行 GC。
Stop the World Event:全部的 Minor Garbage Collection 都是 "Stop the World Event",這意味着全部的線程都要暫停直至 GC 完成。
用來存放長時間存活的對象。一般,年輕代會設置一個年齡閾值,當對象年齡超過這個閾值時,就會被移至老年代。最終老年代的對象也會被回收,稱之爲 Major Garbage Collection,它也是 "Stop the World Event" 。一般,Major GC 是比較慢的,由於涉及到全部的存活對象。因此,HotSpot 虛擬機同時使用多種垃圾收集器,來下降 GC 時間。老年代使用 "標記-整理" 法進行 GC。
對於 HotSpot 虛擬機來講,就是方法區。該區域具有動態性,能夠在運行時添加常量,也能夠對再也不使用的常量進行丟棄,對再也不使用的類進行卸載,可是類的卸載條件很是苛刻:
Step1. 新分配的對象進入 Eden 空間,兩個 Survivor 開始時都是空的:
Step2. 當 Eden 空間滿時,觸發一次 Minor GC:
Step3. 仍然存活的對象移至 Survivor 空間,年齡爲1,再也不引用的刪除,清理 Eden 空間:
Step4. 下一次 Minor GC 觸發時,重複以上操做。不過此次須要將仍然存活的對象移至另外一個 Survivor 空間,而且在上一次 Minor GC 中存活下來的對象的年齡要 +1。而後清理原來的 Survivor 和 Eden 空間:
Step5. 下一次 Minor GC 觸發時,重複以上操做,即切換 Survivor 空間,年齡自增等:
Step6. 屢次 Minor GC 觸發後,部分存活對象的年齡超過了年輕代的年齡閾值(這裏假設爲8),晉升爲老年代:
Step7. 隨着 Minor GC 不斷觸發,年輕代的存活對象也不斷的晉升爲老年代:
Step8. 如上過程幾乎涵蓋了年輕代的整個過程。最終,老年代也會觸發 Major GC 來進行垃圾回收:
Java 程序經過棧上的 reference 來操做堆上的具體對象。因爲 JVM 規範只規定了 Reference 類型是一個指向對象的引用,並無定義這個引用該經過何種方式去定位、訪問堆中的對象的具體位置。目前主流的訪問方式有使用句柄和直接指針兩種:
HotSpot 虛擬機使用的是直接指針方式,最大好處就是速度更快,節省了一次指針定位的開銷。
SoftReference
;WeakReference
;PhantomReference
。Java 編程規範中明確指出,不要重寫 finalize() 方法,除非你知道本身在幹什麼。finalize() 方法只會被 JVM 執行一次。一個對象被第一次被標記爲死亡時,會進行一次篩選,篩選條件是是否須要執行 finalize() 方法,若是須要(對象重寫了 finalize() 方法),這個對象就會被扔到一個叫作 F-Queue 的隊列中,等待有 JVM 自動建立的、低優先級的 Finalizer 線程去執行。稍後,GC 將對 F-Queue 中的對象進行第二次小規模的標記,若是此時尚未逃脫(在 finalize() 中 將 this 賦給其餘類的成員變量),基本上就真的被回收了。
Java 垃圾收集器有不少,JDK 1.7 Update 14 以後的 HotSpot JVM 就同時有 7 個垃圾收集器,並且年輕代和老年代用的收集器還不同。爲何要用這麼多的垃圾收集器呢?就是爲了提升虛擬機的性能。不過不管怎麼優化, "Stop The World" 都是沒法避免的,時間長短而已。Android 的 Dalvik 或者 ART 虛擬機也是如此。因此一是要減小 GC 的時間,二是避免頻繁觸發 GC。
HotSpot 虛擬機在 JDK 1.7 Update 14 以後使用的垃圾收集器:
連線表示能夠搭配使用。
發展歷史最悠久的單線程收集器,使用 "標記-複製" 算法。GC 時,必須暫停其它全部工做線程,直至 GC 結束。
是 Serial 收集器的多線程版本,使用 "標記-複製" 算法。
相似於 ParNew 收集器,不過關注點是達到一個可控制的吞吐量,使用 "標記-複製" 算法。
Serial 收集器的老年代版本,使用 "標記-整理" 算法。
Parallel Scavenge 的老年代版本,使用 "標記-整理" 算法。
Concurrent Mark Sweep,是一種以獲取最短回收停頓時間爲目標的收集器,使用 "標記-清除" 算法。
Garbage First,是當今收集器技術發展的最前沿成果之一,是一款面向服務端應用的收集器。具備並行與併發、分代收集、空間整合和可預測的停頓等特色。
如何獲取 JVM 規範?