JVM 自動內存管理機制及 GC 算法

讀書筆記,如需轉載,請註明做者:Yuloran (t.cn/EGU6c76)html

前言(Preface)

《Java 虛擬機規範》讀書筆記,部份內容摘自 The Java® Virtual Machine Specification Java SE 11 EditionJava Garbage Collection Basics ,部份內容摘自《深刻理解Java虛擬機_JVM高級特性與最佳實踐 第2版》(周志朋著)。java

首先,我們要明確一個概念:《Java 虛擬機規範》是獨立於具體的編程語言以及具體的虛擬機實現的,Java 只是你們最爲熟悉的一種 JVM 編程語言而已,目前比較火的其它 JVM 語言 還有:web

實現這種語言無關性的基石就是平臺無關的程序存儲格式 - 字節碼(ByteCode),即二進制文件 *.Class:算法

jvm language

另外,《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 JVM Architecture

摘自 Java SE HotSpot 概覽Java Garbage Collection Basics併發

HotSpot 虛擬機是 Java SE 平臺的一個核心組件,是 Java 虛擬機規範的實現之一,並做爲 JRE 的一個共享庫來提供。做爲 Java 字節碼執行引擎,它在多種操做系統和架構上提供 Java 運行時設施,如線程和對象同步。它包括自適應將 Java 字節碼編譯成優化機器指令的動態編譯器,並使用爲下降暫停時間和吞吐量而優化的垃圾收集器來高效管理 Java 堆。

HotSpot 虛擬機可根據平臺配置,選擇合適的編譯器、Java 堆配置和垃圾收集器,以保證爲大多數應用程序提供優良性能。下圖爲 HotSpot 虛擬機的架構:

HotSpot JVM Architecture

其主要組件包括:Class Loader, Runtime Data Areas 和 Execution Engine.

注:上圖 Run-Time Data AreasJava Threads 指的是 Java Virtual Machine StacksNative Internal Threads 指的是 Native Method Stacks

Runtime Data Areas

爲了便於理解,本人結合 JVM 規範,將上圖的 Runtime Data Areas 從新繪製以下:

Java Virtual Machine Runtime Data Areas

上圖爲 JVM 規範的闡述,無關具體的虛擬機。好比,Native Method Stacks 可能不存在,HotSopt JVM 就將 JVM Stacks 與 Native Method Stacks 合二爲一了。

The PC Register

Java 虛擬機支持多線程併發執行,每一個線程都有本身的程序計數器(Program Counter Register)。任何肯定的時刻,JVM 線程只能執行一個方法,稱爲當前方法。若是當前方法是 Java 方法,那麼程序計數器裏存儲的就是 JVM 當前正在執行的指令的地址。若是當前方法是本地方法,程序計數器的值則爲空(Undefined)。程序計數器是一塊較小的內存空間,能夠看做是當前線程所執行的字節碼的行號指示器,也是惟一一個不會產生 OutOfMemoryError 的區域。

Java Virtual Machine Stacks

每一個 JVM 線程都用於一個私有的 Java 虛擬機棧,它隨着進程的建立而分配,隨着進程的退出而銷燬。Java 虛擬機棧,描述的是 Java 方法執行的內存模型,因此也能夠稱之爲 Java 方法棧。每次 Java 方法執行時都會建立一個棧幀,棧幀是描述虛擬機進行方法調用和方法執行的數據結構,用於存儲方法的局部變量表、操做數棧、動態鏈接、方法返回地址和一些附加信息。方法從調用至完成的過程,對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。

在編譯程序代碼的時候,棧幀中須要多大的局部變量表,多深的操做數棧都已經徹底肯定,並寫入到方法表的 Code 屬性之中。所以一個棧幀須要分配多少內存,不會受到程序運行期變量數據的影響,僅取決於具體的虛擬機實現。

棧幀的概念結構

局部變量表

用於存儲方法參數和方法內定義的局部變量,以 Variable Slot 爲最小單位,能夠存放一個 boolean、byte、char、short、int、float、reference 或 returnAddress 類型(可按照 Java 語言對應的類型理解,但本質上是不同的)的數據。

  • reference 類型:虛擬機規範既沒有說明它的長度,也沒有明確指出這種引用應有怎樣的結構。可是虛擬機的實現至少要作到兩點:
    • 能夠今後引用中直接或間接查找到對象在 Java 堆中的數據存放地址的起始地址索引;
    • 能夠直接或間接今後引用查到對象所屬數據類型在方法區中存儲的類型信息。
  • returnAddress 類型:指向一條字節碼指令的地址,目前已經不多用了,很古老的虛擬機曾用來實現異常處理。

在 Java 程序編譯爲 Class 文件時,就在方法的 Code 屬性的 max_locals 數據項中肯定了該方法所須要分配的局部變量表的最大容量。

操做數棧

也常稱爲操做棧,其最大深度在編譯期寫入 Code 屬性的 max_stacks 中。棧元素能夠是 Java 語言的任意數據類型。在方法剛執行時,該棧是空的。在方法執行過程當中,會有各類字節碼指令對操做棧進行讀寫操做,好比算術運算是經過操做棧來完成的,或者調用其它方法時,使用操做棧來傳遞參數。

動態鏈接

字節碼中的方法調用指令以常量池中指向該方法的符號引用做爲參數,這些符號引用有一部分會在類加載階段或第一次使用時轉爲直接引用,還有一部分在運行期才轉爲直接引用,這稱爲動態鏈接。每一個棧幀中都包含一個指向運行時常量池中該棧幀所屬方法的引用,以實現動態鏈接。

方法返回地址

方法退出後,須要返回方法被調用的位置。方法正常退出時,調用者的 PC 值可做爲返回地址,棧幀中極可能保存了這個值。方法異常退出時,返回地址須要經過異常處理器表來肯定,棧幀中通常不會保存這部分信息。

附加信息

虛擬機規範裏沒有描述的信息,好比調試信息等。

可能產生的異常:

  • StackOverflowError:線程請求的棧深度大於虛擬機棧深度,就會拋出該異常
  • OutOfMemoryError:若是虛擬機棧支持動態擴展,可是擴展時申請不到足夠內存,或者建立線程時沒有足夠內存初始化虛擬機棧,就會拋出該異常

Native Method Stacks

與 Java 虛擬機棧很是類似,只不過是描述 Java 本地方法執行的內存模型,因此稱之爲本地方法棧。雖然虛擬機規範沒有規定本地方法棧中方法使用的語言、使用方式和與數據結構,不過通常指 C 語言。說到這兒,就不得不說,其實從進程角度來看,不管什麼編程語言編寫的程序,其內存模型或者說內存佈局都差很少。下圖爲 Linux 進程中,C 程序的內存佈局

A typical memory layout of a running process

Java 本地方法棧在功能上就相似於 C 程序的 C Stack。一樣,Java 本地方法棧也會拋出 StackOverflowError 和 OutOfMemoryError。

Method Area

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。

Run-Time Constant Pool

運行時常量池是方法區的一部分。Class 文件除了有類的版本、字段、方法、接口等描述信息,還有一項信息是常量池(Constant Pool Table),用於存放編譯期生成的各類字面量(整型字面量、字符串字面量等)和符號引用,相似於 C 程序的 Symbol Table,不過數據類型要比 C 語言豐富的多,這部份內容將在類加載後,進入方法區的常量池。

Java 虛擬機對 Class 文件的每一部分都有嚴格規定,每個字節用於存放哪一種數據都必須符合規範才能被虛擬機承認、裝載和執行。可是對於運行時常量池卻沒有任何細節要求, 因此運行時常量池被實現成了具有動態性,即常量也能夠在運行時生成,好比 String 的 intern() 方法。

當運行時常量池沒法再申請到內存時,會拋出 OutOfMemoryError。

Heap

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。

Automatic Garbage Collection

不像 C 或 C++,內存由開發人員手動分配和釋放,JVM 中的內存由垃圾收集器自動管理,基本步驟以下:

Step1:標記(Mark)

標記哪些對象是被使用的,哪些是再也不使用的:

Step2:清除(Sweep)

刪除再也不引用的對象,保留存活的對象,並維護一個空閒內存的引用列表 :

Step2a:清除並整理(Sweep-Compact)

爲了提升性能,有些收集器使用 "標記-清除-整理" 算法來回收內存。將仍然存活的對象移至一端,以便下次更容易找到連續可用內存:

Generational Garbage Collection

使用 "標記-清除" 法回收對象的效率是比較低的,尤爲是在對象愈來愈多的時候,將須要更長的時間來執行垃圾回收。這是很恐怖的,由於 GC 觸發時,Java 程序須要被凍結,不然對象的引用關係將沒法追蹤。研究代表,大部分對象的存活時間都很短。因此如今的 JVM 大多采用分代收集算法來提升性能:

年輕代(Young Generation)

全部新對象分配和變老的地方。年輕代用完時,將會觸發一次 Minor Garbage Collection。GC 後,仍然存活的對象會變老並最終進入老年代。年輕代使用 "標記-複製" 法進行 GC。

Stop the World Event:全部的 Minor Garbage Collection 都是 "Stop the World Event",這意味着全部的線程都要暫停直至 GC 完成。

老年代(Old Generation)

用來存放長時間存活的對象。一般,年輕代會設置一個年齡閾值,當對象年齡超過這個閾值時,就會被移至老年代。最終老年代的對象也會被回收,稱之爲 Major Garbage Collection,它也是 "Stop the World Event" 。一般,Major GC 是比較慢的,由於涉及到全部的存活對象。因此,HotSpot 虛擬機同時使用多種垃圾收集器,來下降 GC 時間。老年代使用 "標記-整理" 法進行 GC。

永久代(Permanent generation)

對於 HotSpot 虛擬機來講,就是方法區。該區域具有動態性,能夠在運行時添加常量,也能夠對再也不使用的常量進行丟棄,對再也不使用的類進行卸載,可是類的卸載條件很是苛刻:

  1. 該類全部的實例都被回收;
  2. 加載該類的 ClassLoader 已經被回收;
  3. 該類對應的 Class 對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。

分代收集的步驟

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 虛擬機使用的是直接指針方式,最大好處就是速度更快,節省了一次指針定位的開銷。

對象引用分析

  • 引用計數法:兩個對象相互引用時,沒法判斷其中對象是否再也不使用
  • 可達性分析法:引入 GC Roots 概念,若是一個對象到 GC Roots 沒有任何引用鏈,這個對象就是能夠被回收的。在 Java 中,可做爲 GC Roots 的對象有:
    • Java 方法棧中引用的對象
    • 方法區中類靜態屬性引用的對象
    • 方法區常量引用的對象
    • 本地方法棧中引用的對象

引用細分

  • 強引用:相似於 "Object obj = new Object();" 都是強引用,GC 永遠不會回收;
  • 軟引用:有用但沒必要要的對象,在發生 OOM 以前,會對這些對象進行第二次回收,若是回收後,內存仍然不足,才拋出 OOM。實現類爲 SoftReference;
  • 弱引用:比軟引用弱,只能活到下一次 GC 前。下次 GC 發生時,不管內存是否緊張,都會回收掉只被弱引用關聯的對象。實現類爲 WeakReference
  • 虛引用:最弱的一種引用關係,沒法經過它獲取被引用對象。惟一做用就是被回收時收到一個系統通知。實現類爲 PhantomReference

finalize()

Java 編程規範中明確指出,不要重寫 finalize() 方法,除非你知道本身在幹什麼。finalize() 方法只會被 JVM 執行一次。一個對象被第一次被標記爲死亡時,會進行一次篩選,篩選條件是是否須要執行 finalize() 方法,若是須要(對象重寫了 finalize() 方法),這個對象就會被扔到一個叫作 F-Queue 的隊列中,等待有 JVM 自動建立的、低優先級的 Finalizer 線程去執行。稍後,GC 將對 F-Queue 中的對象進行第二次小規模的標記,若是此時尚未逃脫(在 finalize() 中 將 this 賦給其餘類的成員變量),基本上就真的被回收了。

Garbage Collectors

Java 垃圾收集器有不少,JDK 1.7 Update 14 以後的 HotSpot JVM 就同時有 7 個垃圾收集器,並且年輕代和老年代用的收集器還不同。爲何要用這麼多的垃圾收集器呢?就是爲了提升虛擬機的性能。不過不管怎麼優化, "Stop The World" 都是沒法避免的,時間長短而已。Android 的 Dalvik 或者 ART 虛擬機也是如此。因此一是要減小 GC 的時間,二是避免頻繁觸發 GC。

HotSpot 虛擬機在 JDK 1.7 Update 14 以後使用的垃圾收集器:

連線表示能夠搭配使用。

Serial

發展歷史最悠久的單線程收集器,使用 "標記-複製" 算法。GC 時,必須暫停其它全部工做線程,直至 GC 結束。

ParNew

是 Serial 收集器的多線程版本,使用 "標記-複製" 算法。

Parallel Scavenge

相似於 ParNew 收集器,不過關注點是達到一個可控制的吞吐量,使用 "標記-複製" 算法。

Serial Old

Serial 收集器的老年代版本,使用 "標記-整理" 算法。

Parallel Old

Parallel Scavenge 的老年代版本,使用 "標記-整理" 算法。

CMS

Concurrent Mark Sweep,是一種以獲取最短回收停頓時間爲目標的收集器,使用 "標記-清除" 算法。

G1

Garbage First,是當今收集器技術發展的最前沿成果之一,是一款面向服務端應用的收集器。具備並行與併發、分代收集、空間整合和可預測的停頓等特色。

如何獲取 JVM 規範?

  1. 進入 Oracle 官網,按圖所示:

  1. 點擊 Java SE documentation

  1. 點擊 Language and VM

  1. 選擇版本

相關文章
相關標籤/搜索