1. 程式人生 > 實用技巧 >JVM面試總結

JVM面試總結

Java虛擬機器 JVM

常見面試題如下:

  1. JVM記憶體模型
  2. 垃圾回收機制
  3. 那些物件可以作為GC Root
  4. Minor GC和Full GC
  5. 物件什麼時候進入老年代
  6. 類載入過程
  7. 雙親委派模型和破壞雙親委派模型
  8. JVM何時開始類載入
  9. JVM常見的引數
  10. 線上CPU過高如何排查
  11. 什麼時候需要JVM調優
  12. 如何JVM調優
  13. 常見的垃圾收集器

JVM記憶體模型

程式計數器:一塊較小的記憶體空間, 是當前執行緒所執行的位元組碼的行號指示器,每條執行緒都要有一個獨立的程式計數器,正在執行 java 方法的話,計數器記錄的是虛擬機器位元組碼指令的地址(當前指令的地址)。如果是 Native 方法,則為空。唯一一個在虛擬機器中沒有規定任何 OutOfMemoryError 情況的區域

虛擬機器棧(執行緒私有):是描述java方法執行的記憶體模型,每個方法在執行的同時都會建立一個棧幀(Stack Frame)用於儲存區域性變量表、運算元棧、動態連結、方法出口等資訊。每一個方法從呼叫直至執行完成
的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程。棧幀隨著方法呼叫而建立,隨著方法結束而銷燬——無論方法是正常完成還是異常完成(丟擲了在方法內未被捕獲的異
常)都算作方法結束。

本地方法棧(執行緒私有):和虛擬機器棧所發揮的作用是非常相似的,它們之間的區別不過是虛擬機器棧為虛擬機器執行Java方法(也就是位元組碼)服務,而本地方法棧則為虛擬機器使用到的Native方法服務。

:Java堆是Java虛擬機器所管理的記憶體中最大的一塊。Java堆是被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立。此記憶體區域的唯一目的就是存放物件例項,幾乎所有的物件例項都在這裡分配記憶體,也是垃圾收集器進行垃圾收集的最重要的記憶體區域。Java 堆從 GC 的角度還可以細分為: 新生代(Eden 區、From Survivor 區和 To Survivor 區)和老年代。

方法區:方法區用於儲存已被JVM載入的類資訊、常量、靜態變數,執行時常量池是方法區的一部分,class檔案除了有類的欄位、介面、方法等描述資訊之外,還有常量池用於存放編譯期間生成的各種字面量和符號引用。方法區也被稱為永久代.

在 Java8中為了避免永久代的記憶體溢位以及OutOfMemoryError使用元空間替代永久代,區別在於:元空間並不在虛擬機器中,而是使用
本地記憶體。因此,預設情況下,元空間的大小僅受本地記憶體限制。類的元資料放入 native
memory, 字串池和類的靜態變數放入 java 堆中,這樣可以載入多少類的元資料就不再由
MaxPermSize 控制, 而由系統的實際可用空間來控制。

GC如何確定垃圾/確定死亡物件

  • 引用計數法[迴圈引用問題]
  • 可達性分析演算法 [那些可以作為GC Root]

那些物件可以作為GC Roots

  • 虛擬機器棧(棧幀中的本地變量表)中引用的物件。
  • 方法區中類靜態屬性引用的物件。
  • 方法區中常量引用的物件。
  • 本地方法棧中JNI(即一般說的Native方法)引用的物件。

GC如何回收垃圾/垃圾收集演算法

分代收集演算法
:GC主要作用於堆 堆可分為新生代和老年代 不同區域使用不同的收集演算法,新生代物件每次垃圾收集都會有大批量的物件死去,少數生還,選用複製演算法,只需要付出少量存活物件的複製成本即可完成收集,老年代中物件存活率高且沒有額外空間對它進行分配擔保,所以使用“標記-清除”或“標記-整理”演算法進行回收。

複製演算法
:按記憶體容量將記憶體劃分為等大小
的兩塊。每次只使用其中一塊,當這一塊記憶體滿後將尚存活的物件複製到另一塊上去,把已使用
的記憶體清掉。新生代分為1個Eden區和2個Survivor區(分別叫from和to)每次只使用其中的2塊,且Eden和Survivor的大小為8/1

標記-清除演算法: 分為標記和清除2個階段,先標記需要回收的物件,標記完成後統一回收所有需要的物件。會產生較多不連續的記憶體碎片,可能會在下次分匹配大物件時候無法找到連續夠大的記憶體空間而提前出發一次垃圾收集動作。

標記-整理:比標記-清除多了一步整理,可以避免產生較多的記憶體碎片.

垃圾收集器

CMS收集器(Concurrent Mark Sweep)

它是一種以獲取最短回收停頓時間為目標的收集器。優點:併發收集,低停頓。基於“標記-清除”演算法。目前很大一部分Java應用都集中在網際網路站或B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給使用者帶來較好的體驗,CMS收集器就非常符合這類應用的需求。

執行步驟

  1. 初始標記(CMS initial mark):需要“Stop The World”,標記GC Roots能直接關聯到的物件,速度快。
  2. 併發標記(CMS concurrent mark):進行GC Roots Tracing 過程
  3. 重新標記(CMS remark):需要“Stop The World”,修正併發標記期間,因使用者程式繼續執行而導致標記產生變動的那一部分物件的標記記錄。停頓時間:初始標記<重新標記<<併發標記
  4. 併發清除(CMS concurrent sweep):時間較長。

缺點

  1. 對CPU資源非常敏感,面向併發設計的程式都會對CPU資源較敏感。在併發階段會因為佔用了一部分執行緒[CPU資源]會使應用程式變慢,總吞吐量降低,CMS預設的回收執行緒數: (CPU數量+3)/4

  2. 無法處理浮動垃圾,可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生。併發清理階段使用者程式執行產生的垃圾過了標記階段所以無法在本次收集中清理掉,稱為浮動垃圾。CMS收集器預設在老年代使用了68%的空間後被啟用。若老年代增長的不是很快,可以適當調高參數-XX:CMSInitiatingOccupancyFraction 提高觸發百分比,但調得太高會容易導致“Concurrent Mode Failure”失敗。

  3. 基於“標記-清除”演算法會產生大量空間碎片。提供開關引數-XX:+UseCMSCompactAtFullCollection 用於在“ 享受”完Full GC服務之後進行碎片整理過程,記憶體整理的過程是無法併發的。但是停頓時間會變長。-XX:CMSFullGCsBeforeCompation 設定在執行多少次不壓縮的Full GC後,進行一次帶壓縮的。

G1垃圾收集器

G1收集器可以在幾乎不犧牲吞吐量的前提下完成低停頓的記憶體回收,這是由於它能夠極力避免全區域的垃圾收集,之前的收集器進行收集的範圍都是整個新生代或老年代,而G1將整個Java堆(包括新生代、老年代)劃分為多個大小固定的獨立區域(Region),並且跟蹤這些區域裡面的垃圾堆積程度,在後臺維護一個優先列表,每次根據允許的收集時間,優先回收垃圾最多的區域(這就是Garbage First名稱的由來)。區域劃分、有優先順序的區域回收,保證了G1收集器在有限的時間內可以獲得最高的收集效率。與CMS相比有兩個顯著改進:

  1. 基於標記-整理演算法實現收集器
  2. 非常精確地控制停頓

執行步驟

  1. 初始標記:GC Roots可以直接關聯到的物件,並且修改TAMS(Next Top at Mark Start)的值,讓下一階段使用者程式併發執行的時候,能在真確可用的Region中建立物件。這階段需要停頓執行緒但是很短。
  2. 併發標記:從GC Root開始對堆中的物件進行可達性分析,找出存活的物件,可併發執行,耗時長。
  3. 最終標記:為了修正上一個標記階段因為使用者執行緒的執行導致標記變化的部分。需要停頓執行緒可以併發執行。
  4. 篩選回收:對每個Region的回收價值和成本進行排序,按使用者期望的GC停頓時間來制定回收計劃。

GC觸發條件

可以分為2方面新生代Eden區的Minor GC和老年代的Full GC

Minor GC

  1. 當Eden區滿時,觸發Minor GC

Full GC

  1. 呼叫System.gc時,系統建議執行Full GC,但是不必然執行
  2. 老年代空間不足
  3. 方法區空間不足

JVM常見引數

4種引用型別

類載入過程

類從被載入到虛擬機器記憶體中開始,到卸載出記憶體為止,它的整個生命週期包括:載入、驗證、準備、解析、初始化、使用和解除安裝7個階段。其中JVM 類載入機制分為五個部分:載入,驗證,準備,解析,初始化

載入:這個階段會在記憶體中生成一個代表這個類的 java.lang.Class 對 象,作為方法區這個類的各種資料的入口。

驗證:確保 Class 檔案的位元組流中包含的資訊是否符合當前虛擬機器的要求,並
且不會危害虛擬機器自身的安全。

準備:為類變數分配記憶體並設定類變數的初始值階段,即在方法區中分配這些變數所使
用的記憶體空間。

解析:指虛擬機器將常量池中的符號引用替換為直接引用的過程。

類載入器

從頂到底分為->啟動類載入器->擴充套件類載入器->應用程式類載入器->自定義類載入器 //前三種是JVM提供的。

  1. 啟動類載入器負責載入 JAVA_HOME\lib 目錄中的,或通過-Xbootclasspath 引數指定路徑中的,且被
    虛擬機器認可(按檔名識別,如 rt.jar)的類。
  2. 擴充套件類載入器負責載入 JAVA_HOME\lib\ext 目錄中的,或通過 java.ext.dirs 系統變數指定路徑中的類
    庫。
  3. 應用程式類載入器 負責載入使用者路徑(classpath)上的類庫。

雙親委派模型

雙親委派的意思是如果一個類載入器需要載入類,那麼首先它會把這個類請求委派給父類載入器去完成,每一層都是如此。一直遞迴到頂層,當父載入器無法完成這個請求時,子類才會嘗試去載入。

好處:採用雙親委派的一個好處是比如載入位於 rt.jar 包中的類 java.lang.Object,不管是哪個載入
器載入這個類,最終都是委託給頂層的啟動類載入器進行載入,這樣就保證了使用不同的類載入
器最終得到的都是同樣一個 Object 物件。

JVM調優

什麼時候需要調優

  1. GC頻繁[考慮新生代Eden MinorGC 老年代 Full GC]
  2. 單次GC時間過長[STW 一般超過1s]
  3. 應用佔用CUP過高[需要排查是否有記憶體洩漏]
  4. 堆外記憶體佔用過高導致應用響應慢。

如何調優

GC頻繁

一般新生代的空間不大,Eden區域被填滿的時間較快,可以通過增加Eden區域的大小來降低Minor GC的頻率。
增加Eden區域的大小是否會導致單次Minor GC時間增加

Eden區域增加會導致增加了T1(掃描時間),但是T2(複製物件)的時間和存活的物件數有關係,而不是和Eden區域大小有關係,且T2時間遠大於T1,所以增加Eden不會顯著增加單次GC時間。

Full GC一般指全域性GC,而Major GC一般指的是老年代的GC.這在《深入理解JVM虛擬機器第二版中》說法是有誤的,這裡將這2個GC都稱為是老年代的GC,在第三版中做了更正。這2中需要根據不同的垃圾收集器CMS OR G1做具體判斷,比如設定Region等。

GC時間過長: 處理方式同上需要根據不同垃圾收集器來處理

應用佔用CUP過高: 直接排查記憶體溢位 正對性處理即可.

外部環境導致的響應慢: 適當減少外部應用對伺服器記憶體的佔用。

線上CPU過高如何排查