JVM記憶體管理面試常見問題全解

詩風雅韻發表於2022-04-16

目錄

一、什麼是JVM

Java Virtual Machine(Java虛擬機器)是java程式實現跨平臺的⼀個重要的⼯具(部件)。

HotSpot VM,相信所有Java程式設計師都知道,它是Sun JDK和OpenJDK中所帶的虛擬機器,也是⽬前使⽤範圍最⼴的Java虛擬機器。

只要裝有JVM的平臺,都可以運⾏java程式。那麼Java程式在JVM上是怎麼被運⾏的?

通過介紹以下JVM的三個組成部分,就可以瞭解到JVM內部的⼯作機制

  • 類載入系統:負責完成類的載入

  • 運⾏時資料區:在運⾏Java程式的時候會產⽣的各種資料會儲存在運⾏時資料區

  • 執⾏引擎:執⾏具體的指令(程式碼)
    在這裡插入圖片描述

1、jvm的三個組成部分

  • 類載入系統

  • 執行時資料區

  • 執行引擎

二、類載入系統

1、類的載入過程

⼀個類被載入進JVM中要經歷哪⼏個過程

  • 載入: 通過io流的⽅式把位元組碼⽂件讀⼊到jvm中(⽅法區)

  • 校驗:通過校驗位元組碼⽂件的頭8位的16進位制是否是java魔數cafebabe

  • 準備:為類中的靜態部分開闢空間並賦初始化值

  • 解析:將符號引⽤轉換成直接引⽤。——靜態連結

  • 初始化:為類中的靜態部分賦指定值並執⾏靜態程式碼塊。

類被載入後,類中的型別資訊、⽅法資訊、屬性資訊、運⾏時常量池、類載入器的引⽤等資訊會被載入到元空間中。

2、類載入器

  1. 類是誰來負載載入的?——類載入器
  2. Bootstrap ClassLoader 啟動類載入器:負載載入jre/lib下的核⼼類庫中的類,⽐如rt.jar、charsets.jar
  • ExtClassLoader 擴充套件類載入器:負載載入jre/lib下的ext⽬錄內的類

ext 載入路徑:System.getProperty("java.ext.dirs");

  • AppClassLoader 應⽤類載入器:負載載入⽤戶⾃⼰寫的類

app 載入路徑:System.getProperty("java.class.path");

  • ⾃定義類載入器:⾃⼰定義的類載入器,可以打破雙親委派機制。
    在這裡插入圖片描述

三、雙親委派機制

1、雙親委派機制介紹

當類載入進⾏載入類的時候,類的載入需要向上委託給上⼀級的類載入器,上⼀級繼續向上委託,直到啟動類載入器。啟動類載入器去核⼼類庫中找,如果沒有該類則向下委派,由下⼀級擴充套件類載入器去擴充套件類庫中,如果也沒有繼續向下委派,直到找不到為⽌,則報類找不到的異常。

應⽤類載入器怎麼載入Student和String呢?需要通過雙親委派機制

在這裡插入圖片描述

2、為什麼要雙親委派機制

防⽌核⼼類庫中的類被隨意篡改

防⽌類的重複載入

3、雙親委派機制的核心原始碼

  • ClassLoader.class

4、全盤委託機制

當⼀個類被當前的ClassLoader載入時,該類中的其他類也會被當前該ClassLoader載入。除⾮指明其他由其他類載入器載入。

5、自定義載入器實現雙親委託機制

6、自定義載入器打破雙親委派機制

四、執行時資料區

1、執行時資料區的介紹(也叫JVM的記憶體模型 JMM、記憶體區域)

JMM分成了這麼⼏個部分

  1. 堆空間(執行緒共享):存放new出來的物件
  2. 元空間(執行緒共享):存放類元資訊、類的模版、常量池、靜態部分
  3. 執行緒棧(執行緒獨享):⽅法的棧幀
  4. 本地⽅法區(執行緒獨享):本地⽅法產⽣的資料
  5. 程式計數器(執行緒獨享):配合執⾏引擎來執⾏指令
    在這裡插入圖片描述

2、程式在執行時執行資料區的記憶體變化

執行緒棧:執⾏⼀個⽅法就會線上程棧中建立⼀個棧幀。

棧幀包含如下四個內容:

區域性變數表:存放⽅法中的區域性變數

運算元棧:⽤來存放⽅法中要操作的資料

動態連結:存放⽅法名和⽅法內容的對映關係,通過⽅法名找到⽅法內容

⽅法出⼝:記錄⽅法執⾏完後調⽤次⽅法的位置。

五、物件的建立流程

1、物件建立流程

在這裡插入圖片描述

2、類載入校驗

校驗該類是否已被載入。主要是檢查常量池中是否存在該類的類元資訊。如果沒有,則需要進⾏載入。

3、記憶體分配

為物件分配記憶體。具體的分配策略如下:

  • Bump the Pointer(指標碰撞):如果記憶體空間的分配是絕對規整的,則JVM記錄當前剩餘記憶體的指標,在已⽤記憶體分配

  • Free List(空閒列表):如果記憶體空間的分配不規整,那麼JVM會維護⼀個可⽤記憶體空間的列表⽤於分配。

物件併發分配存在的問題:

  • Compare And Swap: ⾃旋分配,如果併發分配失敗則重試分配之後的地址

  • Thread Local Allocation Buffer(TLAB):本地執行緒分配緩衝,JVM被每個執行緒分配⼀空間,每個執行緒在⾃⼰的空間中建立物件(jdk8預設使⽤,之前版本需要通過-XX:+UseTLAB開啟)

4、設定初值

根據資料型別,為物件空間初始化賦值

5、設定物件頭

為物件設定物件頭資訊,物件頭資訊包含以下內瑞:類元資訊、物件雜湊碼、物件年齡、鎖狀態標誌等

  • 物件頭中的Mark Work 欄位(32位)

在這裡插入圖片描述

  • 物件頭中的型別指標

型別指標是用來指向元空間當前類的類元資訊。⽐如調⽤類中的⽅法,通過型別指標找到元空間中的該類,再找到相應的⽅法。

開啟指標壓縮後,型別指標只⽤4個位元組 儲,否則需要8個位元組儲存

過⼤的物件地址,會佔⽤更⼤的頻寬和增加GC的壓⼒。

物件中指向其他物件所使⽤的指標:8位元組被壓縮成4位元組。 最早的機器是32位,最⼤⽀持記憶體 2的32次⽅=4G。現在是64位,2的64次⽅可以表示N個T的記憶體。記憶體32G即等於2的35次⽅。如果記憶體是32G的話,⽤35位表示記憶體地址,這樣過於浪費。如果把35位的資料,根據演算法,壓縮成32位的資料(也就是4個位元組)。在儲存時⽤4個位元組,再使⽤時使⽤8個位元組。之前⽤35位儲存記憶體地址,就可以⽤32位儲存。這樣8個位元組的物件,實際上使⽤32位來儲存,這樣64位就能表示2個物件。如果記憶體⼤於32G,指標壓縮會失效,會強制使⽤64位來表示物件地址。因此jvm堆記憶體最好不要⼤於32G。

6、執行init方法

為物件中的屬性賦值和執⾏構造⽅法。

六、垃圾回收

1、物件成為垃圾的判斷依據

在堆空間和元空間中,GC這條守護執行緒會對這些空間開展垃圾回收⼯作,那麼GC如何判斷這些空間的物件是否是垃圾,有兩種演算法:

  • 引⽤計數法:

物件被引⽤,則計數器+1,如果計數器是0,那麼物件將被判定為是垃圾,於是被回收。但是這種演算法沒有辦法解決迴圈依賴的物件。因此JVM⽬前的主流⼚商Hotspot沒有使⽤這種演算法。

  • 可達性分析演算法

    :GC Roots根

    • gc roots根節點: 在物件的引⽤中,會有這麼⼏種物件的變數:來⾃於執行緒棧中的區域性變數表中的變數、靜態變數、本地⽅法棧中的變數,這些變數都被稱為gc roots根節點
  • 判斷依據:gc在掃描堆空間中的某個節點時,向上遍歷,看看能不能遍歷到gc roots根節點,如果不能,那麼意味著這個物件是垃圾。

在這裡插入圖片描述

2、 物件中的finalize方法

Object類中有⼀個finalize⽅法,也就是說任何⼀個物件都有finalize⽅法。這個⽅法是物件被回收之前的最後⼀根救命稻草。

  • GC在垃圾物件回收之前,先標記垃圾物件,被標記的物件的finalize⽅法將被調⽤

  • 調⽤finalize⽅法如果物件被引⽤,那麼第⼆次標記該物件,被標記的物件將移除出即將被回收的集合,繼續存活

  • 調⽤finalize⽅法如果物件沒有被引⽤,那麼將會被回收

  • 注意,finalize⽅法只會被調⽤⼀次。

3、物件逃逸

在jdk1.7之前,物件的建立都是在堆空間中建立,但是會有個問題,⽅法中的未被外部訪問的物件這種物件沒有被外部訪問,且在堆空間上頻繁建立,當⽅法結束,需要被gc,浪費了效能。所以在1.7之後,就會進⾏⼀次逃逸分析(預設開啟),於是這樣的物件就直接在棧上建立,隨著⽅法的出棧⽽被銷燬,不需要進⾏gc。

在棧上分配記憶體的時候:會把聚合量替換成標量,來減少棧空間的開銷,也為了防⽌棧上沒

有⾜夠連續的空間直接存放物件。

標量:java中的基本資料型別(不可再分)

聚合量:引⽤資料型別。

七、垃圾回收演算法

1、標記清除演算法、複製演算法、標記整理演算法、分代回收法

在這裡插入圖片描述
在這裡插入圖片描述

2、分代回收演算法

在這裡插入圖片描述

  1. 堆空間被分成了新⽣代(1/3)和⽼年代(2/3),新⽣代中被分成了eden(8/10)、survivor1(1/10)、survivor2(1/10)
  2. 物件的建立在eden,如果放不下則觸發minor gc
  3. 物件經過⼀次minorgc 後存活的物件會被放⼊到survivor區,並且年齡+1
  4. survivor區執⾏的複製演算法,當物件年齡到達15.進⼊到⽼年代。
  5. 如果⽼年代放滿。就會觸發Full GC

3、物件進⼊到⽼年代的條件

  • ⼤物件直接進⼊到⽼年代:⼤物件可以通過引數設定⼤⼩,多⼤的物件被認為是⼤物件。

-XX:PretenureSizeThreshold

  • 當物件的年齡到達15歲時將進⼊到⽼年代,這個年齡可以通過這個引數設定:

XX:MaxTenuringThreshold

  • 根據物件動態年齡判斷,如果s區中的物件總和超過了s區中的50%,那麼下⼀次做複製的時候,把年齡⼤於等於這次最⼤年齡的物件都⼀次性全部放⼊到⽼年代。
  • ⽼年代空間分配擔保機制 :在minor gc時,檢查⽼年代剩餘可⽤空間是否⼤於年輕代⾥現有的所有物件(包含垃圾)。如果⼤於等於,則做minor gc。如果⼩於,看下是否配置了擔保引數的配置:-XX: -HandlePromotionFailure ,如果配置了,那麼判斷⽼年代剩餘的空間是否⼩於歷史每次minor gc 後進⼊⽼年代的物件的平均⼤⼩。如果是,則直接full gc,減少⼀次minor gc。如果不是,執⾏minor gc。如果沒有擔保機制,直接full gc。
    在這裡插入圖片描述

八、垃圾回收器

1.Serial收集器

-XX:+UseSerialGC -

XX:+UseSerialOldGC

單執行緒執⾏垃圾收集,收集過程中會有較⻓的STW(stop the world),在GC時⼯作執行緒不能⼯作。雖然STW較⻓,但簡單、直接。

新⽣代採⽤複製演算法,⽼年代採⽤標記-整理演算法。

2、Parallel收集器

-XX:+UseParallelGC

-XX:+UseParallelOldGC

使⽤多執行緒進⾏GC,會充分利⽤cpu,但是依然會有stw,這是jdk8預設使⽤的新⽣代和⽼年代的垃圾收集器。充分利⽤CPU資源,吞吐量⾼。

新⽣代採⽤複製演算法,⽼年代採⽤標記-整理演算法。
在這裡插入圖片描述

3、ParNew收集器

-XX:+UseParNewGC

⼯作原理和Parallel收集器⼀樣,都是使⽤多執行緒進⾏GC,但是區別在於ParNew收集器可以和CMS收集器配合⼯作。主流的⽅案:

ParNew收集器負責收集新⽣代。CMS負責收集⽼年代。

在這裡插入圖片描述

4、CMS收集器

-XX:+UseConcMarkSweepGC

⽬標:儘量減少stw的時間,提升⽤戶的體驗。真正做到gc執行緒和⽤戶執行緒⼏乎同時⼯作。CMS採⽤標記-清除演算法

  • 初始標記: 暫停所有的其他執行緒(STW),並記錄gc roots直接能引⽤的物件。

  • 併發標記:從GC Roots的直接關聯物件開始遍歷整個物件圖的過程, 這個過程耗時較⻓但是不需要STW,可以與垃圾收集執行緒⼀起併發運⾏。這個過程中,⽤戶執行緒和GC執行緒併發,可能會有導致已經標記過的物件狀態發⽣改變。

  • 重新標記:為了修正併發標記期間因為⽤戶程式繼續運⾏⽽導致標記產⽣變動的那⼀部分物件的標記記錄,這個階段的停頓時間⼀般會⽐初始標記階段的時間稍⻓,遠遠⽐併發標記階段時間短。主要⽤到三⾊標記⾥的演算法做重新標記。

  • 併發清理:開啟⽤戶執行緒,同時GC執行緒開始對未標記的區域做清掃。這個階段如果有新增物件會被標記為⿊⾊不做任何處理。

  • 併發重置:重置本次GC過程中的標記資料。

在這裡插入圖片描述

5、三⾊標記演算法

  • 在併發標記階段,物件的狀態可能發⽣改變,GC在進⾏可達性分析演算法分析物件時,⽤三⾊來標識物件的狀態

  • 灰⾊:這個物件被GC Roots遍歷過但其部分的引⽤沒有被GC Roots遍歷。在重新標記時重新遍歷灰⾊物件。

  • ⽩⾊:這個物件沒有被GC Roots遍歷過。在重新標記時該物件如果是⽩⾊的話,那麼將會被回收。

6、垃圾收集器組合⽅案

不同的垃圾收集器可以組合使⽤,在使⽤時選擇適合當前業務場景的組合。

在這裡插入圖片描述

九、JVM調優實戰

在這裡插入圖片描述

1.JVM調優的核⼼引數

  • -Xss:每個執行緒的棧⼤⼩。設定越⼩,說明⼀個執行緒棧⾥能分配的棧幀就越少,但是對JVM整體來說能開啟的執行緒數會更多。
  • -Xms:設定堆的初始可⽤⼤⼩,預設實體記憶體的1/64
  • -Xmx:設定堆的最⼤可⽤⼤⼩,預設實體記憶體的1/4
  • -Xmn:新⽣代⼤⼩
  • -XX:NewRatio:預設2表示新⽣代佔年⽼代的1/2,佔整個堆記憶體的1/3。
  • -XX:SurvivorRatio:預設8表示⼀個survivor區佔⽤1/8的Eden記憶體,即1/10的新⽣代記憶體。以下兩個引數設定元空間⼤⼩建議值相同,且寫死,防⽌在程式啟動時因為需要元空間的空間不夠⽽頻繁full gc。
  • -XX:MaxMetaspaceSize:最⼤元空間⼤⼩
  • XX:MetaspaceSize:元空間⼤⼩,預設是21M,達到該值後會觸發Full GC,同時會按100%進⾏動態調整,為了減少⼤資料量佔滿元空間,頻繁觸發Full GC,建議在初始化時設定為MaxMetaspaceSize相同的值。

2.JVM調優實戰

  • 設定JVM的引數

‐Xms3072M ‐Xmx3072M ‐Xss1M ‐XX:MetaspaceSize=256M

‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8
在這裡插入圖片描述

  • 調整VM引數

‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M

‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8
在這裡插入圖片描述

3、調優的關鍵點

  • 設定元空間⼤⼩,最⼤值和初始化值相同

  • 根據業務場景計算出每秒產⽣多少的物件。這些物件間隔多⻓時間會成為垃圾(⼀般根據接⼝響應時間來判斷)

  • 計算出堆中新⽣代中eden、survivor所需要的⼤⼩:根據上⼀條每條產⽣的物件和多少時間成為垃圾來計算出,依據是儘量減少full gc。

4、結合垃圾收集器的調優策略

結合垃圾收集器:PraNew+CMS,對於CMS的垃圾收集器,還需要加上相關的配置:

  • 對於⼀些年齡較⼤的bean,⽐如快取物件、spring相關的容器物件,配置相關的物件,這些物件需要儘快的進⼊到⽼年代,因此需要配置:-XX:MaxTenuringThreshold=5
  • ⼤物件直接進⼊到⽼年代:-XX:PretenureSizeThreshold=1M
  • CMS垃圾收集器會有併發模式失敗的⻛險(轉換為使⽤serialOld垃圾收集器),如何避免這種⻛險:將full gc的觸發點調低:

-XX:CMSInitiatingOccupancyFraction=85 (預設是92),相當於⽼年代使⽤率達到85%就觸發full gc,於是還剩15%的空間允許在cms進⾏gc的過程中產⽣新的物件。

  • CMS垃圾收集器收集完後會產⽣碎⽚,碎⽚需要整理,但不是每次收集完就整理,設定做了3次Full GC之後整理⼀次碎⽚:
-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=3
  • PraNew+CMS的具體JVM引數配置:

java -Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M -

XX:MaxMetaspaceSize=256M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -

XX:+UseParNewGC -XX:+UseConcMarkSweepGC

-XX:CMSInitiatingOccupancyFraction=85 -XX:+UseCMSCompactAtFullCollection

-XX:CMSFullGCsBeforeCompaction=3 -jar device-service.jar

重點作業:

  • 清晰的掌握類載入過程及雙親委派機制

  • 掌握程式在運⾏時 JVM的運⾏時資料區中發⽣了怎樣的變化

  • 物件的建立的流程

  • 物件成為垃圾的判斷依據

  • 垃圾回收演算法有哪些

  • JVM空間記憶體分配及垃圾回收器的常⽤引數配置

十、JVM效能調優的原則有哪些?

  1. 多數的Java應用不需要在伺服器上進行GC優化,虛擬機器內部已有很多優化來保證應用的穩定執行,所以不要為了調優而調優,不當的調優可能適得其反
  2. 在應用上線之前,先考慮將機器的JVM引數設定到最優(適合)
  3. 在進行GC優化之前,需要確認專案的架構和程式碼等已經沒有優化空間。我們不能指望一個系統架構有缺陷或者程式碼層次優化沒有窮盡的應用,通過GC優化令其效能達到一個質的飛躍
  4. GC優化是一個系統而複雜的工作,沒有萬能的調優策略可以滿足所有的效能指標。GC優化必須建立在我們深入理解各種垃圾回收器的基礎上,才能有事半功倍的效果
  5. 處理吞吐量和延遲問題時,垃圾處理器能使用的記憶體越大,即java堆空間越大垃圾收集效果越好,應用執行也越流暢。這稱之為GC記憶體最大化原則
  6. 在這三個屬性(吞吐量、延遲、記憶體)中選擇其中兩個進行jvm調優,稱之為GC調優3選2

十一、什麼情況下需要JVM調優?

  • Heap記憶體(老年代)持續上漲達到設定的最大記憶體值

  • Full GC 次數頻繁

  • GC 停頓(Stop World)時間過長(超過1秒,具體值按應用場景而定)

  • 應用出現OutOfMemory 等記憶體異常

  • 應用出現OutOfDirectMemoryError等記憶體異常( failed to allocate 16777216 byte(s) of direct memory (used: 1056964615, max: 1073741824))

  • 應用中有使用本地快取且佔用大量記憶體空間

  • 系統吞吐量與響應效能不高或下降

  • 應用的CPU佔用過高不下或記憶體佔用過高不下

十二、聊聊Java的GC機制

細節可見此部落格連結:點我跳轉

GC:垃圾回收(Garbage Collection),在計算機領域就是指當一個計算機上的動態儲存器(記憶體空間)不再需要時,就應該予以釋放,以讓出儲存器,便於他用。這種儲存器的資源管理,稱為垃圾回收。

這三個問題將分別對應接下來的3節一一解答

  • JVM清理的是哪一塊的物件?判斷垃圾方法

  • 哪些物件會被清理,為什麼清理A而不清理B?

  • JVM又是如何清理的?回收演算法

十三、CMS 和G1 的區別

1、使用範圍不一樣

  • CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用

  • G1收集器收集範圍是老年代和新生代。不需要結合其他收集器使用

2、STW的時間不一樣

  • CMS收集器以最小的停頓時間為目標的收集器。

  • G1收集器可預測垃圾回收的停頓時間(建立可預測的停頓時間模型)

3、垃圾碎片

  • CMS收集器是使用“標記-清除”演算法進行的垃圾回收,容易產生記憶體碎片

  • G1收集器使用的是“標記-整理”演算法,進行了空間整合,降低了記憶體空間碎片。

4、回收演算法不一樣

  • CMS :標記-清除”

  • G1:標記-整理

5、大物件處理不一樣

  • 在CMS記憶體中,如果一個物件過大,進入S1、S2區域的時候大於改分配的區域,物件會直接進入老年代。

  • G1處理大物件時會判斷物件是否大於一個Region大小的50%,如果大於50%就會橫跨多個Region進行存放回收過程不一樣

6、回收過程不一樣

CMS回收垃圾的4個階段

  • 初始標記

  • 併發標記

  • 重新標記

  • 併發清理

  • 併發重置

G1回收垃圾的4個階段

  • 初始標記

  • 併發標記

  • 最終標記

  • 篩選回收

  1. 初始標記:標記GC Roots 可以直接關聯的物件,該階段需要執行緒停頓但是耗時短

  2. 併發標記:尋找存活的物件,可以與其他程式併發執行,耗時較長

  3. 最終標記:併發標記期間使用者程式會導致標記記錄產生變動(好比一個阿姨一邊清理垃圾,另一個人一邊扔垃圾)虛擬機器會將這段時間的變化記錄在Remembered Set Logs 中。最終標記階段會向Remembered Set合併併發標記階段的變化。這個階段需要執行緒停頓,也可以併發執行

  4. 篩選回收:對每個Region的回收成本進行排序,按照使用者自定義的回收時間來制定回收計劃

初始標記和併發標記和CMS的過程是差不多的,最後的篩選回收會首先對各個Region的回收價值和成本進行排序,根據使用者所期望的GC停頓時間來制定回收計劃

因為採用的標記——整理的演算法,所以不會產生記憶體碎片,最終的回收是STW的,所以也不會有浮動垃圾,Region的區域大小是固定的,所以回收Region的時間也是可控的

同時G1 使用了Remembered Set來避免全堆掃描,G1中每個Region都有一個與之對應的RememberedSet ,在各個 Region 上記錄自家的物件被外面物件引用的情況。當進行記憶體回收時,在GC根節點的列舉範圍中加入RememberedSet 即可保證不對全堆掃描也不會有遺漏。

以上就是CMS和G1的對比過程

這是本人今年春招找實習工作準備總結,記錄在此,如有需要的老鐵可以看看,如有問題可以留言指導

相關文章