讀書筆記之《深入理解Java虛擬機器:JVM高階特性與最佳實踐》

蔡不菜丶發表於2022-07-11

? 學而不思則罔,思而不學則殆。 —— 孔子

? 微信公眾號已開啟,菜農曰,沒關注的同學們記得關注哦!

本篇帶來的是周志明老師編寫的《深入理解Java虛擬機器:JVM高階特性與最佳實踐》,十分硬核!

全書共分為 5 部分,圍繞記憶體管理執行子系統程式編譯與優化高效併發等核心主題對JVM進行了全面而深入的分析,深刻揭示了JVM工作原理。

全書整體5個部分,十三章,共 358929 字。整體結構相當清晰,以至於寫讀書筆記的時候無從摘抄(甚至想把全書複述一遍),以下是全書第二部分的內容,望讀者細細品嚐!

一、第一部分 走進Java

第一部分介紹了關於 Java 的技術體系與發展史,談及未來。該部分內容不做摘抄,直接進入核心主題。

二、第二部分 自動記憶體管理機制

Java 與 C++ 之間有一堵由記憶體動態分配和垃圾收集技術所圍成的“高牆”,牆外面的人想進去,牆裡面的人卻想出來。

第二章 Java記憶體區域與記憶體溢位異常

對於 Java 程式設計師來說是幸福的也是可悲的,在虛擬機器自動記憶體管理機制的幫助下不需要為每一個 new 操作去寫配對的 delete/ free 程式碼,不容易出現記憶體洩露記憶體溢位問題,但是在記憶體管理領域中,C或 C++,既是擁有最高權力的 "皇帝" 又是從事最基礎工作的 "勞動人民"。

1)執行時資料區域

Java 虛擬機器在執行 Java 程式的過程中會把它所管理的記憶體劃分為若干個不同的資料區域.

  1. 程式計數器

這是一塊較小的記憶體空間,可以看作是當前執行緒所執行的位元組碼的行號指示器

為了執行緒切換後能夠恢復到正確的執行位置,每條執行緒都需要有一個獨立的程式計數器,各條執行緒之間計數器互不影響,獨立儲存,執行緒私有

此記憶體區域是唯一一個在 Java 虛擬機器規範中沒有規定任何 OutOfMemoryError 情況的區域。

1. 虛擬機器棧

與程式計數器一樣,Java 虛擬機器棧也是執行緒私有的,它的生命週期與執行緒相同。虛擬機器描述的是 Java 方法執行的記憶體模型:每個方法在執行的同時都會建立一個棧幀用於儲存 區域性變數表、運算元棧、動態連結、方法出口等資訊。每一個方法從呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程。

2. 本地方法棧

與虛擬機器棧十分相似,主要的區別在於虛擬機器棧是為虛擬機器執行 Java 方法服務,而本地方法棧是為虛擬機器使用到的 Native 方法服務。

與虛擬機器棧一樣的是:本地方法棧也會丟擲 StackOverFlowError 和 OutOfMemoryError 異常

3. Java 堆

Java 堆是 Java 虛擬機器所管理的記憶體中最大的一塊。Java 堆是被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立。

此記憶體區域的唯一目的就是存放物件例項,幾乎所有的物件例項都會在這裡進行記憶體分配。

Java 堆是垃圾收集器管理的主要區域,因此很多時候也會稱為 GC 堆 ,因此還可以細分為:新生代和老年代,而新生代中又可以細緻為 Eden空間From Survivor 空間To Survivor 空間等。

4. 方法區

方法區與 Java 堆一樣,是各個執行緒共享的記憶體區域,它用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。

永久代與方法區,本質上兩者並不等價

HotSpot 虛擬機器設計團隊會利用 永久代 來實現方法區,這樣 HotSpot 的垃圾收集器就可以像 Java 堆那樣管理這部分記憶體,能夠省去專門為方法區編寫記憶體管理程式碼的工作。

Java 虛擬機器規範對方法區的限制非常寬鬆,除了和 Java 堆一樣不需要連續的記憶體和可以選擇固定大小,還可以選擇不實現垃圾收集。

並非資料進入了方法區就如永久代的名字一樣“永久”存在了。這區域的記憶體回收目標主要是針對常量池的回收和對型別的解除安裝,一般來說,這個區域的回收“成績”比較難以令人滿意,尤其是型別的解除安裝,條件相當苛刻,但是這部分割槽域的回收確實是必要的

2)擴充套件說明

  1. 執行時常量池

執行時常量是方法區的一部分。用於存放 編譯期生成的各種字面量和符號引用,這部分記憶體將在類載入後進入方法區的執行時常量池中存放。

執行時常量池相對於 Class 檔案常量池的另外一個重要特徵是具備 動態性(並不要求常量一定只有編譯期才能產生,執行期間也可以將新的常量存入池中)

執行時常量池是 方法區 的一部分,當常量池無法再申請到記憶體時也會丟擲 OutOfMemoryError 異常。

  1. 直接記憶體

直接記憶體並不是虛擬機器執行時資料區的一部分,也不是 Java 虛擬機器規範中定義的記憶體區域,但這部分也會導致 OutOfMemoryError 異常。

在Java1.4 引入的 NIO,是一種基於通道(Channel)與緩衝區(Buffer)的 I/O 方式,可以使用 Native 函式庫直接分配 堆外記憶體,避免了在 Java 堆和 Native 堆中來回複製資料,可以在一些場景中顯著提高效能。

直接記憶體的分配不會受到 Java 堆大小的限制,但還是會受到本機總記憶體大小以及處理器定址空間的限制。

3)HotSpot 虛擬機器物件探祕

  1. 物件的建立
眾所周知,物件的建立通常是使用 new 關鍵字生成的

虛擬機器遇到一條 new 指令時,首先將去檢查這個指令的引數是否能在常量池中定位到一個類的 符號引用,並且檢查該符號引用是否已經被載入解析初始化(類載入的過程)。如果沒有,則必須先執行相應的類載入過程。

在類載入檢查通過後,虛擬機器將為新生物件分配記憶體,物件所需記憶體的大小在類載入完成後便可完全確定,為物件分配空間的任務等同於把一塊確定大小的記憶體從 Java 堆中劃分出來。

分配記憶體僅僅是把那個指標向空閒空間那邊挪動一段與物件大小相等的距離,這種分配的方式稱為 指標碰撞。(虛擬機器必須維護一個列表,記錄哪些記憶體塊是可用的,在分配的時候就從列表中找到一塊足夠大的空間劃分給物件例項,這種分配方式稱為空閒列表

當然,建立物件還需要考慮 併發問題,有可能給物件 A 分配記憶體,指標還沒來得及修改,物件 B 又同時使用了原來的指標來分配記憶體。

解決該問題有兩種方案:

  • 對分配記憶體空間的動作進行同步處理——實際上虛擬機器採用 CAS配上失敗重試的方式保證更新操作的原子性
  • 把記憶體分配的動作按照執行緒劃分在不同的空間之中,即每個執行緒在Java堆中預先分配一小塊記憶體,稱為 本地執行緒分配緩衝(TLAB)。

記憶體分配完畢後,虛擬機器需要將分配到記憶體空間都初始化為零值(不包括物件頭),如果使用 TLAB,這一工作過程也可以提前至 TLAB 分配時進行。

到此,一個新的物件產生了,但是由於還沒有執行<init> 方法,這個時候物件的所有欄位都是 0。

  1. 物件的記憶體佈局

物件記憶體中儲存的佈局可以分為3塊區域:物件頭(Header)、例項資料(Instance Data)和對齊填充(Padding)

物件頭:

  • 用於儲存物件自身的執行時資料,如雜湊碼、GC 分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等,這部分資料的長度在 32 位和 64 位的虛擬機器(未開啟壓縮指標)中分別為 32 bit和 64bit,官方稱為 Mark Word。

  • 另一部分是型別指標,即物件指向它的類後設資料的指標,虛擬機器通過這個指標來確定這個物件是哪個類的例項。

例項資料:

是物件真正儲存的有效資訊,也是在程式程式碼中所定義的各種型別的欄位內容。無論是從父類繼承下來的,還是在子類中定義的,都需要記錄下來。這部分儲存順序會受到虛擬機器分配策略引數和欄位在 Java 原始碼中定義順序的影響。

對齊填充:

這部分並不是必然存在的,也沒有特別的含義,它僅僅起著佔位符的作用。

  1. 物件的訪問定位
建立物件是為了使用物件,我們的Java 程式需要通過棧上的 reference 資料來操作堆上的具體物件

目前主流的物件訪問方式有兩種

  • 控制程式碼訪問。Java堆中會劃分出一塊記憶體來作為控制程式碼池,reference 中儲存的就是物件的控制程式碼地址,控制程式碼中包含了物件例項資料與型別資料各自的具體地址資訊。

  • 直接指標訪問。Java堆物件的佈局中就必須考慮如何放置訪問型別資料的相關資訊,而 reference 中儲存的就是物件地址。

這兩種物件訪問方式各有優勢:

  • 控制程式碼訪問:reference中儲存的最穩定的控制程式碼地址,在物件被移動(垃圾收集時移動物件是非常普遍的行為)時只會改變控制程式碼中的例項資料指標,而 reference 本身不需要改變。
  • 直接指標訪問:速度更快,節省了一次指標定位的時間開銷

4)OutOfMemoryError 異常

除了程式計數器之外的其他幾個執行時區域都有發生 OOM 異常的可能。

  1. 堆記憶體溢位

Java 堆用於儲存物件例項,只要不斷建立物件,並且保證 GC Roots 到物件之間有可達路徑來避免垃圾回收機制來清除這些物件,那麼在物件數量達到最大堆的容量限制後就會產生 記憶體溢位異常。

具體情況具體分析,可以藉助記憶體映像分析工具(如Eclipse Memory Analyzer)對Dump出來的堆轉儲快照進行分析,重點是確認記憶體中的物件是否是必要的,也就是要先分清楚到底是出現了記憶體洩漏(Memory Leak)還是記憶體溢位(Memory Overflow)

如果是記憶體洩露,那麼需要找到洩露物件是通過怎樣的路徑與 GC Roots 相關聯並導致垃圾收集器無法自動回收它們的。

如果是記憶體溢位,那就需要檢查虛擬機器的堆引數(-Xms 和 -Xmx),與機器實體記憶體對比看看是否還可以調大,從程式碼上檢查是否存在某些物件生命週期過長,持有狀態時間過長的情況,嘗試減少程式執行期的記憶體消耗。

  1. 虛擬機器棧和本地方法棧溢位
  • 如果執行緒請求的棧深度大於虛擬機器所允許的最大深度,將丟擲 StackOverflowError 異常
  • 如果虛擬機器在擴充套件棧時無法申請到足夠的記憶體空間,則丟擲 OutOfMemoryError 異常
  1. 方法區和執行時常量池溢位

在執行時常量池溢位,那麼在 OutOfMemoryError 後面跟隨的提示資訊是 PermGen space。

方法區溢位也是一種常見的記憶體溢位異常,一個類要被垃圾收集器回收掉,判定條件是比較苛刻的。

在經常動態生成大量Class的應用中,需要特別注意類的回收狀況。

這類場景除了使用了CGLib位元組碼增強和動態語言之外,常見的還有:大量JSP或動態產生JSP檔案的應

用(JSP第一次執行時需要編譯為Java類)、基於OSGi的應用(即使是同一個類檔案,被不同的載入器

載入也會視為不同的類)等。

  1. 本機直接記憶體溢位

DirectMemory 容量可以通過 -XX: MaxDirectMemorySize 指定。如果不指定,則預設與 Java 堆最大值(-Xmx)一樣。

在 DirectByteBuffer 分配記憶體時也會丟擲記憶體溢位異常,但它丟擲異常時並沒有真正向作業系統申請分配記憶體,而是通過計算機得知記憶體無法分配,於是手動丟擲異常,真正申請分配記憶體的方法是:unsafe.allocateMemory()

由 DirectMemory 導致的記憶體溢位,一個明顯的特徵是在 Heap Dump 檔案中不會看見明顯的異常,如果讀者發現 OOM 之後 Dump 的檔案很小,而程式中又直接或間接使用了 NIO,那就可以考慮檢查是不是這方面的問題

第三章 垃圾收集器與記憶體分配策略

1)物件已死嗎

在垃圾收集器工作之前需要確定這些物件中哪些是 存活 的,哪些已經 死去

  1. 引用計數法

定義:給每個物件新增一個引用計數器,每當有一個地方引用它時,計數器值就加 1;當引用失效時,計數器值就減 1;任何時刻計數器為 0 的物件就是不可能再被使用的。

缺陷:兩個無用物件出現相互引用而無法回收。

  1. 可達性分析法

定義:通過一系列稱為 GC Roots 的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為 引用鏈 ,當一個物件到 GC Roots 沒有任何引用鏈相連時,則證明此物件是不可用的。

可作為 GC Roots 物件:

  • 虛擬機器棧(棧幀中的本地變數表)中引用的物件
  • 方法區種類靜態屬性引用的物件
  • 方法區中常量引用的物件
  • 本地方法棧中 JNI(Native 方法)引用的物件
  1. 引用種類

JDK 1.2 之後,Java 對引用的概念進行了擴充,將引用分為 強引用、軟引用、弱引用、虛引用 4種

  • 強引用:普遍存在的引用(通過 new 方式),只要強引用還存在,垃圾收集器就永遠不會回收掉被引用的物件
  • 軟引用:一些還有用但非必須的物件。在系統將要發生記憶體溢位異常之前,將會把這些物件列進回收範圍之中進行第二次回收,如果這次回收還沒有足夠的記憶體,才會丟擲記憶體溢位異常
  • 弱引用:強度比軟引用更弱一些,被弱引用關聯的物件只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前記憶體是否夠用,都會回收掉只被弱引用關聯的物件
  • 虛引用:一個物件是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個物件例項。為一個物件設定虛引用關聯的唯一目的就是在這個物件被收集器回收時收到一個系統通知
  1. 生存還是死亡
即使在可達性分析演算法中不可達的物件,也並非是 非死不可

宣告一個物件死亡,至少要經過兩次標記過程:如果物件在進行可達性分析後發現沒有與 GC Roots 相連線的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此物件是否有必要執行 finalize()方法,當物件沒有覆蓋 finalize()方法,或已經被虛擬機器呼叫過,虛擬機器將這兩種情況都視為沒有必要執行。

如果這個物件判定為有必要執行 finalize() 方法,那麼這個物件會被放置在一個叫做 F-Queue 的佇列中,並在稍後由一個虛擬機器自動建立的、低優先順序的 Finalizer 執行緒去執行它。

finalize()方法是物件逃脫死亡命運的最後一次機會,稍後GC將對F-Queue中的物件進行第二次小規模的標記,如果物件要在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個物件建立關聯即可,譬如把自己(this關鍵字)賦值給某個類變數或者物件的成員變數,那在第二次標記時它將被移除出“即將回收”的集合。

注意:任何一個物件的 finalize() 方法都只會被系統自動呼叫一次,如果物件面臨下一次回收,它的 finallize() 方法不會被再次執行。

  1. 回收方法區

Java 虛擬機器規範中對方法區的要求比較寬鬆,可以不要求虛擬機器在方法區中實現垃圾回收,但並不表示方法區沒有垃圾回收。

永久代的垃圾收集主要回收兩部分內容: 廢棄常量 和 無用的類。

類需要同時滿足下面 3 個條件才算是 無用的類:

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

2)垃圾收集演算法

  1. 標記 - 清除演算法

標記 - 清除演算法是最基礎的收集演算法。演算法分為 標記清除 兩個階段。

不足之處:效率問題空間問題

  1. 複製演算法

該演算法將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中一塊,當一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把已使用的記憶體空間一次清理掉。

不足之處:將記憶體縮小為了原來的一半,代價有點高了

現在商業虛擬機器都採用這種收集演算法來回收新生代。將記憶體分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間。當回收時,將Eden和Survivor中還存活著的物件一次性地複製到另外一塊Survivor空間上,最後清理掉Eden和剛才用過的Survivor空間。(預設Eden和Survivor的大小比例是8∶1)

  1. 標記-整理演算法

該演算法分為 標記整理 演算法,標記過程仍然與 標記-清除演算法一樣,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。

  1. 分代收集演算法

在新生代中,每次垃圾收集時都發現有大批物件死去,只有少量存活,那就選用複製演算法,只需要付出少量存活物件的複製成本就可以完成收集。而老年代中因為物件存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記—清理”或者“標記—整理”演算法來進行回收。

3)HotSpot 的演算法實現

  1. 列舉根節點

可達性分析對執行時間的敏感還體現在GC停頓上,因為這項分析工作必須在一個能確保一致性的快照中

進行——這裡“一致性”的意思是指在整個分析期間整個執行系統看起來就像被凍結在某個時間點上,不

可以出現分析過程中物件引用關係還在不斷變化的情況,該點不滿足的話分析結果準確性就無法得到保

證。這點是導致GC進行時必須停頓所有Java執行執行緒(Sun將這件事情稱為“Stop The World”)的其

中一個重要原因,即使是在號稱(幾乎)不會發生停頓的CMS收集器中,列舉根節點時也是必須要停頓的。

虛擬機器應當是有辦法直接得知哪些地方存放著物件引用。在HotSpot的實現中,是使用一組稱 OopMap

的資料結構來達到這個目的。

  1. 安全點

在OopMap的協助下,HotSpot可以快速且準確地完成GC Roots列舉,但如果為每一條指令都生成對應

的OopMap,那將會需要大量的額外空間,這樣GC的空間成本將會變得很高。

因此 HotSpot 會在 特定的位置 記錄這些資訊,這些位置稱為 安全點 程式執行時並非在所有地方都能停頓下來開始 GC ,只有在到達安全點才能暫停。

安全點的選定基本上是以是否具備讓程式長時間執行的特徵選定的。

長時間執行最明顯的特徵就是指令序列複用(方法呼叫、迴圈跳轉、異常跳轉等)

在 GC 發生時要讓所有執行緒都跑到安全點上再停頓下來有兩種方式:

  • 搶先式中斷:不需要執行緒的執行程式碼主動去配合,在GC發生時,首先把所有執行緒都中斷,如果發現有執行緒中斷的地方不在安全點上,就恢復執行緒,讓它跑到安全點上。
  • 主動式中斷:當GC需要中斷執行緒的時候,不直接對執行緒操作,僅僅簡單地設定一個標誌,各個執行緒執行時主動去輪詢這個標誌,發現中斷標誌為真時就自己中斷掛起。
  1. 安全區域

安全點機制保證了程式執行時,在不太長的時間內就會遇到可進入 GC 的 Safepoint。

但是在程式不執行的時候(執行緒Sleep或Blocked狀態)這時候執行緒就無法響應 JVM 的中斷請求,這個時候就需要 安全區域 來解決。安全區域是擴充套件了的 Safepoint。

線上程執行到 Safe Region 中的程式碼時,首先標識自己已經進入了 Safe Region,那樣,當這段時間裡 JVM 要發起 GC 時,就不用管標識自己為 Safe Region狀態的執行緒了,線上程要離開 Safe Region 時,它要檢查系統是否已經完成了根節點列舉(或者整個GC過程),如果完成了,那執行緒就繼續執行,否則它就必須等待直到收到可以安全離開 SafeRegion 的訊號為止。

4)垃圾收集器

如果說收集演算法是記憶體回收的方法論,那麼垃圾收集器就是記憶體回收的具體實現。

兩個概念:

  • 並行(Parallel):指多條垃圾收集執行緒並行工作,此時使用者執行緒仍然處於等待狀態
  • 併發(Concurrent):使用者執行緒與垃圾收集執行緒同時執行(不一定是並行的,也可以是交替執行),使用者程式繼續執行,而垃圾收集程式則執行在另一個 CPU 上

在 HotSpot 中大致存在著作用於不同分代的收集器,如果兩個收集器之間存在連線,說明可以搭配使用。

  1. Serial 收集器

這個收集器是一個單執行緒的收集器,在進行垃圾收集時,必須暫停其他所有的工作執行緒,直到它收集結束。

  1. ParNew 收集器

這個收集器就是 Serial 收集器的多執行緒版本(也就是說除了多條執行緒一起處理之外,其餘行為與Serial收集器一致)。除了 Serial 收集器之外,只有 ParNew 才能與 CMS 配合使用

  1. Parallel Scavenge 收集器

該收集器也是使用 複製演算法。該收集器與其他收集器的關注點不同 - 吞吐量優先收集器

  • CMS 等收集器的關注點在於 儘可能地縮短垃圾收集時使用者執行緒的停頓時間
  • Parallel Scanvenge 則 儘可能達到一個可控制的吞吐量(吞吐量 = 執行使用者程式碼時間 /(執行使用者程式碼時間 +垃圾收集時間)
Parallel Scavenge收集器無法與CMS收集器配合工作,如果新生代選擇了Parallel Scavenge收集器,老年代只能選擇 Serial Old 收集器
  1. Serial Old 收集器

Serial Old是Serial收集器的老年代版本,它同樣是一個單執行緒收集器,使用“標記-整理”演算法。

  1. Parallel Old 收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多執行緒和“標記-整理”演算法

  1. CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。

CMS收集器是基於“標記—清除”演算法實現的,它的運作過程相對於前面幾種收集器來說更復雜一些,整個過程分為4個步驟,包括:

  • 初始標記(需 Stop The World)
  • 併發標記
  • 重新標記(需 Stop The World)
  • 併發清除

由於整個過程中耗時最長的併發標記和併發清除過程收集器執行緒都可以與使用者執行緒一起工作,所以,從總體上來說,CMS收集器的記憶體回收過程是與使用者執行緒一起併發執行的。

CMS 收集器明顯的3個缺陷:

  • CMS 收集器對 CPU 資源非常敏感
  • CMS 收集器無法處理浮動垃圾,可能會出現 "Concurrent Mode Failure" 失敗而導致另一次 Full GC發生
  • CMS 收集器會產生大量的記憶體碎片
浮動垃圾:由於 CMS 併發清理階段使用者執行緒還在執行這,伴隨程式執行自然就還有新的垃圾不斷產生,這一部分垃圾出現在標記過程之後,CMS 無法在當次收集中處理,只好在下次 GC 時在清理掉。
  1. G1 收集器

G1 是一款面向服務端應用的垃圾收集器。具備以下特點:

  • 並行與併發
  • 分代收集
  • 空間整合:G1從整體來看是基於“標記—整理”演算法實現的收集器,從區域性(兩個Region之間)上來看是基於“複製”演算法實現的,但無論如何,這兩種演算法都意味著G1運作期間不會產生記憶體空間碎片,收集後能提供規整的可用記憶體
  • 可預測停頓:G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒

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

  • 初始標記
  • 併發標記
  • 最終標記(需要 Stop The World)
  • 篩選回收

5)記憶體分配與回收策略

  1. 物件優先在Eden分配

物件在新生代 Eden 區中分配,當 Eden 區中沒有足夠的空間進行分配時,虛擬機器將發起一次 Minor GC。

Minor GC 和 Full GC 的區別:

  • 新生代GC(Minor GC):發生在新生代的垃圾收集動作,因為 Java 物件太多都具備朝生夕滅的特性,所以 Minor GC 非常頻繁,一般回收速度也比較快
  • 老年代GC(Major GC/ Full GC):指發生在老年代的GC,出現了Major GC,經常會伴隨至少一次的 Minor GC(但非絕對的,在 Parallel Scanvenge 收集器的收集策略裡就有直接進行 Major GC 的策略選擇過程)。Major GC 的速度一般會比Minor GC 慢10倍以上
  1. 大物件直接進入老年代
所謂的大物件是指,需要大量連續記憶體空間的Java物件,最典型的大物件就是那種很長的字串以及陣列
  1. 長期存活的物件將進入老年代

在虛擬機器中會為每個物件定義一個物件年齡(Age)計數器。物件每熬過一次 Minor GC 年齡就增加 1 歲,當它年齡增加到一定程度(預設 15歲),就會進入老年代。

  1. 動態物件年齡判定

如果在Survivor空間中相同年齡所有物件大小的總和大於 Survivor 空間的一半,年齡大於或等於該年齡的物件就可以直接進入老年代。

  1. 空間分配擔保

在發生Minor GC 之前,虛擬機器會先檢查老年代最大可用的連續空間是否大於新生代所有物件總空間,如果條件成立,則可以確保 Minor GC 是安全的。如果不成立,則虛擬機器會檢視 HandlePromotionFailure 設定值是否允許擔保失敗。如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代物件的平均大小,如果大於,將嘗試進行一次 Minor GC,如果小於,或者HandlePromotionFailure 設定不允許冒險,那這時也要改為進行一次 Full GC。

第四章 虛擬機器效能監控與故障處理工具

1)JDK 的命令列工具

這些工具都異常小巧,因為這些命令列工具大多數是 jdk/lib/tools.jar 類庫的一層薄包裝,主要功能程式碼是在 tools 類庫中實現的。

  1. jps:虛擬機器程式狀況工具

功能:列出正在執行的虛擬機器程式,並顯示虛擬機器執行主類(main()函式所在的類)名稱以及這些程式的本地虛擬機器唯一ID

主要選項:

  1. jstat:虛擬機器統計資訊監視工具

功能:用於監視虛擬機器各種執行狀態資訊的命令列工具。

主要選項:

  1. jinfo:Java 配置資訊工具

功能:實時地檢視和調整虛擬機器各項引數

  1. jmap:Java 記憶體映像工具

功能:用於生成堆轉儲快照(heapdump或dump檔案)。還可以查詢 finalize 執行佇列,Java 堆和永久代的詳細資訊,如空間使用率、當前用的是哪種收集器等

主要選項:

  1. jhat:虛擬機器堆轉儲快照分析工具

功能:與 jmap 搭配使用,來分析 jmap 生成的堆轉儲快照。

  1. jstack:Java 堆疊跟蹤工具

功能:用於生成虛擬機器當前時刻的執行緒快照(threaddump 或 javacore 檔案)

執行緒快照就是當前虛擬機器中每一條執行緒正在執行的方法堆疊的集合,生成執行緒快照的主要目的是定位執行緒出現長時間停頓的原因,如執行緒間死鎖、死迴圈、請求外部資源導致的長時間等待等都是導致執行緒長時間停頓的常見原因。

主要選項:

2) JDK 的視覺化工具

  1. JConsole:Java 監視與管理控制檯

基於 JMX 的視覺化監視、管理工具。它管理部分的功能是針對 JMX MBean 進行管理。

  1. VisualVM:多合一故障處理工具

是目前為止隨 JDK 釋出的功能最強大的執行監視和故障處理程式。處理執行監視、故障處理外,還提供了效能分析。

這篇我們們主要是針對 《深入理解Java虛擬機器:JVM高階特性與最佳實踐》 第二部分做了相關的讀書筆記,一方面是因為篇幅原因,接下來會針對每部分分別釋出。另一方面是為了讓讀者有充足的時間進行消化,畢竟一口吃不成大胖子 ~ 請讀者慢慢閱讀,靜待下部分讀書筆記的出爐!??

不要空談,不要貪懶,和小菜一起做個 吹著牛X做架構 的程式猿吧~點個關注做個伴,讓小菜不再孤單。我們們下文見!

? 今天的你多努力一點,明天的你就能少說一句求人的話!

?? 微信公眾號:菜農曰,沒關注的同學們記得關注哦!

相關文章