JVM理論知識

java虛擬機(java virtual machine,JVM),一種能夠運行java字節碼的虛擬機。作爲一種編程語言的虛擬機,實際上不只是專用於Java語言,只要生成的編譯文件匹配JVM對加載編譯文件格式要求,任何語言都可以由JVM編譯運行。比如kotlin、scala等。

JVM的基本結構

JVM由三個主要的子系統構成

  • 類加載子系統
  • 運行時數據區(內存結構)
  • 執行引擎

jvm-architecture.png

類加載機制

類的生命週期

類加載.jpg

1.加載

將.class文件從磁盤讀到內存

2.連接

2.1 驗證

驗證字節碼文件的正確性

2.2 準備

給類的靜態變量分配內存,並賦予默認值

2.3 解析

類裝載器裝入類所引用的其它所有類

3.初始化

爲類的靜態變量賦予正確的初始值,上述的準備階段爲靜態變量賦予的是虛擬機默認的初始值,此處賦予的纔是程序編寫者爲變量分配的真正的初始值,執行靜態代碼塊

4.使用

5.卸載

類加載器的種類

啓動類加載器(Bootstrap ClassLoader)

負責加載JRE的核心類庫,如JRE目標下的rt.jar,charsets.jar等

擴展類加載器(Extension ClassLoader)

負責加載JRE擴展目錄ext中jar類包

系統類加載器(Application ClassLoader)

負責加載ClassPath路徑下的類包

用戶自定義加載器(User ClassLoader)

負責加載用戶自定義路徑下的類包

類加載器.jpg

類加載機制

全盤負責委託機制

當一個ClassLoader加載一個類的時候,除非顯示的使用另一個ClassLoader,該類所依賴和引用的類也由這個ClassLoader載入

雙親委派機制

指先委託父類加載器尋找目標類,在找不到的情況下載自己的路徑中查找並載入目標類

雙親委派模式的優勢

  • 沙箱安全機制:比如自己寫的String.class類不會被加載,這樣可以防止核心庫被隨意篡改
  • 避免類的重複加載:當父ClassLoader已經加載了該類的時候,就不需要子ClassLoader再加載一次

運行時數據區(內存結構)

1.方法區(Method Area)

類的所有字段和方法字節碼,以及一些特殊方法如構造函數,接口代碼也在這裏定義。簡單來說,所有定義的方法的信息都保存在該區域,靜態變量+常量+類信息(構造方法/接口定義)+運行時常量池都存在方法區中,雖然Java虛擬機規範把方法區描述爲堆的一個邏輯部分,但是它卻有一個別名叫做Non-Heap(非堆),目的應該是爲了和Java的堆區分開(jdk1.8以前hotspot虛擬機叫永久代、持久代,jdk1.8時叫元空間)

2.堆(Heap)

虛擬機啓動時自動分配創建,用於存放對象的實例,幾乎所有對象都在堆上分配內存,當對象無法在該空間申請到內存是將拋出OutOfMemoryError異常。同時也是垃圾收集器管理的主要區域。

堆.png

2.1 新生代(Young Generation)

類出生、成長、消亡的區域,一個類在這裏產生,應用,最後被垃圾回收器收集,

結束生命。

新生代分爲兩部分:伊甸區(Eden space)和倖存者區(Survivor space),所有的類都是在伊甸區被new出來的。倖存區又分爲From和To區。當Eden區的空間用完是,程序又需要創建對象,JVM的垃圾回收器將Eden區進行垃圾回收(Minor GC),將Eden區中的不再被其它對象應用的對象進行銷燬。然後將Eden區中剩餘的對象移到From Survivor區。若From Survivor區也滿了,再對該區進行垃圾回收,然後移動到To Survivor區。

2.2 老年代(Old Generation)

新生代經過多次GC仍然存貨的對象移動到老年區。若老年代也滿了,這時候將發生Major GC(也可以叫Full GC),進行老年區的內存清理。若老年區執行了Full GC之後發現依然無法進行對象的保存,就會拋出OOM(OutOfMemoryError)異常

2.3 元空間(Meta Space)

在JDK1.8之後,元空間替代了永久代,它是對JVM規範中方法區的實現,區別在於元數據區不在虛擬機當中,而是用的本地內存,永久代在虛擬機當中,永久代邏輯結構上也屬於堆,但是物理上不屬於。

爲什麼移除了永久代

參考官方解釋http://openjdk.java.net/jeps/122

大概意思是移除永久代是爲融合HotSpot與 JRockit而做出的努力,因爲JRockit沒有永久代,不需要配置永久代。

元空間.jpg

3.棧(Stack)

Java線程執行方法的內存模型,一個線程對應一個棧,每個方法在執行的同時都會創建一個棧幀(用於存儲局部變量表,操作數棧,動態鏈接,方法出口等信息)不存在垃圾回收問題,只要線程一結束該棧就釋放,生命週期和線程一致

4.本地方法棧(Native Method Stack)

和棧作用很相似,區別不過是Java棧爲JVM執行Java方法服務,而本地方法棧爲JVM執行native方法服務。登記native方法,在Execution Engine執行時加載本地方法庫

5.程序計數器(Program Counter Register)

就是一個指針,指向方法區中的方法字節碼(用來存儲指向嚇一跳指令的地址,也即將要執行的指令代碼),由執行引擎讀取下一條指令,是一個非常小的內存空間,幾乎可以忽略不計

Java棧:

 

理解:JVM執行Java代碼所使用的棧。

  1. Java棧中存放的是一個個的棧幀,每個棧幀對應一個被調用的方法,在棧幀中包括局部變量表(Local Variables)、操作數棧(Operand Stack)、指向當前方法所屬的類的運行時常量池(運行時常量池的概念在方法區部分會談到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些額外的附加信息。
  2. 當線程執行一個方法時,就會隨之創建一個對應的棧幀,並將建立的棧幀壓棧。當方法執行完畢之後,便會將棧幀出棧。因此可知,線程當前執行的方法所對應的棧幀必定位於Java棧的頂部。(在使用遞歸方法的時候容易導致棧內存溢出的現象)
    1. 局部變量表:

就是用來存儲方法中的局部變量(包括在方法中聲明的非靜態變量以及函數形參)。對於基本數據類型的變量,則直接存儲它的值,對於引用類型的變量,則存的是指向對象的引用。局部變量表的大小在編譯器就可以確定其大小了,因此在程序執行期間局部變量表的大小是不會改變的

  1. 操作數棧:

棧最典型的一個應用就是用來對錶達式求值。當一個線程執行方法的過程中,實際上就是不斷執行語句的過程,而歸根到底就是進行計算的過程。因此可以這麼說,程序中的所有計算過程都是在藉助於操作數棧來完成的。

  1. 指向運行時常量池的引用

因爲在方法執行的過程中有可能需要用到類中的常量,所以必須要有一個引用指向運行時常量。

  1. 方法返回地址:

當一個方法執行完畢之後,要返回之前調用它的地方,因此在棧幀中必須保存一個方法返回地址。

由於每個線程正在執行的方法可能不同,因此每個線程都會有一個自己的Java棧,互不干擾。

JAVA堆:

 

理解:Java中堆是用來存儲對象本身以及數組(數組及對象的引用是存放在Java棧中的),Java堆基本上不用區關心內存的釋放問題,Java的垃圾回收機制會自動進行處理,因此這部分空間也是Java垃圾收集器管理的主要區域,另外,堆是被所有線程共享的,在JVM中只有唯一一個堆。(JVM執行Java代碼所使用的堆)

 

GC算法和收集器

如何判斷對象可以被回收

堆中幾乎放着所有的對象實例,對堆垃圾回收前的第一步就是要判斷哪些對象已經死亡(即不能再被任何途徑使用的對象)

引用計數法

給對象添加一個引用計數器,每當有一個地方引用,計數器就加1。當引用失效,計數器就減1。任何時候計數器爲0的對象就是不可能再被使用的。

這個方法實現簡單,效率高,但是目前主流的虛擬機中沒有選擇這個算法來管理內存,最主要的原因是它很難解決對象之前相互循環引用的問題。所謂對象之間的相互引用問題,通過下面代碼所示:除了對象a和b相互引用着對方之外,這兩個對象之間再無任何引用。但是它們因爲互相引用對方,導致它們的引用計數器都不爲0,於是引用計數器法無法通知GC回收器回收它們。

 

可達性分析算法

這個算法的基本思想就是通過一系列的稱爲”GC Roots“的對象作爲起點,從這些節點開始向下搜索,節點所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連的話,則證明此對象時不可用的。

GC Roots根節點:類加載器、Thread、虛擬機棧的局部變量表、static成員、常量引用、本地方法棧的變量等等

8.png

如何判斷一個常量是廢棄常量

運行時常量池主要回收的是廢棄的常量。那麼,我們怎麼判斷一個常量時廢棄常量呢?

假如在常量池中存在字符串"abc",如果當前沒有任何String對象引用該字符串常量的話,就說明常量”abc“就是廢棄常量,如果這時發生內存回收的話而且有必要的話,”abc“會被系統清理出常量池。

如何判斷一個類是無用的類

需要滿足以下三個條件:

  • 該類所有的實例都已經被回收,也就是 Java 堆中不存在該類的任何實例。
  • 加載該類的 ClassLoader 已經被回收。
  • 該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

虛擬機可以對滿足上述3個條件的無用類進行回收,這裏僅僅是”可以“,而並不是和對象一樣不適用了就必然會被回收。

垃圾回收算法

 

標記-清除算法

它是最基礎的收集算法,這個算法分爲兩個階段,“標記”和”清除“。首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象。它有兩個不足的地方:

  1. 效率問題,標記和清除兩個過程的效率都不高;
  2. 空間問題,標記清除後會產生大量不連續的碎片;

 

複製算法

爲了解決效率問題,複製算法出現了。它可以把內存分爲大小相同的兩塊,每次只使用其中的一塊。當這一塊的內存使用完後,就將還存活的對象複製到另一塊區,然後再把使用的空間一次清理掉。這樣就使每次的內存回收都是對內存區間的一半進行回收

 

標記-整理算法

根據老年代的特點提出的一種標記算法,標記過程和“標記-清除”算法一樣,但是後續步驟不是直接對可回收對象進行回收,而是讓所有存活的對象向一段移動,然後直接清理掉邊界以外的內存

 

分代收集算法

現在的商用虛擬機的垃圾收集器基本都採用"分代收集"算法,這種算法就是根據對象存活週期的不同將內存分爲幾塊。一般將java堆分爲新生代和老年代,這樣我們就可以根據各個年代的特點選擇合適的垃圾收集算法。

在新生代中,每次收集都有大量對象死去,所以可以選擇複製算法,只要付出少量對象的複製成本就可以完成每次垃圾收集。而老年代的對象存活機率時比較高的,而且沒有額外的空間對它進行分配擔保,就必須選擇“標記-清除”或者“標記-整理”算法進行垃圾收集

垃圾收集器

java虛擬機規範對垃圾收集器應該如何實現沒有任何規定,因爲沒有所謂最好的垃圾收集器出現,更不會有萬金油垃圾收集器,只能是根據具體的應用場景選擇合適的垃圾收集器。

14.png

Serial收集器

Serial(串行)收集器收集器是最基本、歷史最悠久的垃圾收集器了。大家看名字就知道這個收集器是一個單線程收集器了。它的 “單線程” 的意義不僅僅意味着它只會使用一條垃圾收集線程去完成垃圾收集工作,更重要的是它在進行垃圾收集工作的時候必須暫停其他所有的工作線程( “Stop The World” ),直到它收集結束。

新生代採用複製算法,老年代採用標記-整理算法。

15.jpg

虛擬機的設計者們當然知道Stop The World帶來的不良用戶體驗,所以在後續的垃圾收集器設計中停頓時間在不斷縮短(仍然還有停頓,尋找最優秀的垃圾收集器的過程仍然在繼續)。

但是Serial收集器有沒有優於其他垃圾收集器的地方呢?當然有,它簡單而高效(與其他收集器的單線程相比)。Serial收集器由於沒有線程交互的開銷,自然可以獲得很高的單線程收集效率。Serial收集器

對於運行在Client模式下的虛擬機來說是個不錯的選擇。

ParNew收集器

ParNew收集器其實就是Serial收集器的多線程版本,除了使用多線程進行垃圾收集外,其餘行爲(控制參數、收集算法、回收策略等等)和Serial收集器完全一樣。

新生代採用複製算法,老年代採用標記-整理算法。

 

它是許多運行在Server模式下的虛擬機的首要選擇,除了Serial收集器外,只有它能與CMS收集器(真正意義上的併發收集器,後面會介紹到)配合工作。

Parallel Scavenge收集器(JDK1.8)

Parallel Scavenge 收集器類似於ParNew 收集器。

Parallel Scavenge收集器關注點是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的關注點更多的是用戶線程的停頓時間(提高用戶體驗)。所謂吞吐量就是CPU中用於運行用戶代碼的時間與CPU總消耗時間的比值。 Parallel Scavenge收集器提供了很多參數供用戶找到最合適的停頓時間或最大吞吐量,如果對於收集器運作不太瞭解的話,手工優化存在的話可以選擇把內存管理優化交給虛擬機去完成也是一個不錯的選擇。

新生代採用複製算法,老年代採用標記-整理算法。

17.jpg

Serial Old收集器

Serial收集器的老年代版本,它同樣是一個單線程收集器。它主要有兩大用途:一種用途是在JDK1.5以及以前的版本中與Parallel Scavenge收集器搭配使用,另一種用途是作爲CMS收集器的後備方案。

Parallel Old收集器

Parallel Scavenge收集器的老年代版本。使用多線程和“標記-整理”算法。在注重吞吐量以及CPU資源的場合,都可以優先考慮 Parallel Scavenge收集器和Parallel Old收集器。

CMS收集器

並行和併發概念補充:

  • 並行(Parallel) :指多條垃圾收集線程並行工作,但此時用戶線程仍然處於等待狀態。
  • 併發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不一定是並行,可能會交替執行),用戶程序在繼續運行,而垃圾收集器運行在另一個CPU上。

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。它而非常符合在注重用戶體驗的應用上使用。

CMS(Concurrent Mark Sweep)收集器是HotSpot虛擬機第一款真正意義上的併發收集器,它第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作。

從名字中的Mark Sweep這兩個詞可以看出,CMS收集器是一種 “標記-清除”算法實現的,它的運作過程相比於前面幾種垃圾收集器來說更加複雜一些。整個過程分爲四個步驟:

  • 初始標記(CMS initial mark): 暫停所有的其他線程,並記錄下直接與root相連的對象,速度很快
  • 併發標記(CMS concurrent mark): 同時開啓GC和用戶線程,用一個閉包結構去記錄可達對象。但在這個階段結束,這個閉包結構並不能保證包含當前所有的可達對象。因爲用戶線程可能會不斷的更新引用域,所以GC線程無法保證可達性分析的實時性。所以這個算法裏會跟蹤記錄這些發生引用更新的地方。
  • 重新標記(CMS remark): 重新標記階段就是爲了修正併發標記期間因爲用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段的時間稍長,遠遠比並發標記階段時間短
  • 併發清除(CMS concurrent sweep): 開啓用戶線程,同時GC線程開始對爲標記的區域做清掃。

18.jpg

 

CMS主要優點:併發收集、低停頓。但是它有下面三個明顯的缺點:

  • 對CPU資源敏感;
  • 無法處理浮動垃圾;併發運行時產生的垃圾
  • 它使用的回收算法-“標記-清除”算法會導致收集結束時會有大量空間碎片產生。

G1收集器

G1 (Garbage-First)是一款面向服務器的垃圾收集器,主要針對配備多顆處理器及大容量內存的機器. 以極高概率滿足GC停頓時間要求的同時,還具備高吞吐量性能特徵.

20.png

被視爲JDK1.7中HotSpot虛擬機的一個重要進化特徵。它具備一下特點:

  • 並行與併發:G1能充分利用CPU、多核環境下的硬件優勢,使用多個CPU(CPU或者CPU核心)來縮短Stop-The-World停頓時間。部分其他收集器原本需要停頓Java線程執行的GC動作,G1收集器仍然可以通過併發的方式讓java程序繼續執行
  • 分代收集:雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但是還是保留了分代的概念。 空間整合:與CMS的“標記–清理”算法不同,G1從整體來看是基於“標記整理”算法實現的收集器;從局部上來看是基於“複製”算法實現的
  • 可預測的停頓:這是G1相對於CMS的另一個大優勢,降低停頓時間是G1 和 CMS 共同的關注點,但G1 除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度爲M毫秒的時間片段內

G1收集器的運作大致分爲以下幾個步驟:

  • 初始標記
  • 併發標記
  • 最終標記
  • 篩選回收

G1收集器在後臺維護了一個優先列表,每次根據允許的收集時間,優先選擇回收價值最大的Region(這也就是它的名字Garbage-First的由來)。這種使用Region劃分內存空間以及有優先級的區域回收方式,保證了GF收集器在有限時間內可以儘可能高的收集效率(把內存化整爲零)。
 

怎麼選擇垃圾收集器?

  1. 優先調整堆的大小讓服務器自己來選擇
  2. 如果內存小於100m,使用串行收集器
  3. 如果是單核,並且沒有停頓時間的要求,串行或JVM自己選擇
  4. 如果允許停頓時間超過1秒,選擇並行或者JVM自己選
  5. 如果響應時間最重要,並且不能超過1秒,使用併發收集器

官方推薦G1,性能高。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章