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

目錄

一、什麼是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的對比過程

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