目錄
Java中可作為GC Roots的引用有哪幾種?
finalize方法
垃圾回收算法
標記-清除
標記-復制
標記-整理
分代收集算法
為什么要用分代收集
標記復制的標記過程和復制會不會停頓
MinorGC,MajorGC,MixedGC,FullGC
FullGC怎么清理
什么時候觸發FullGC
空間分配擔保是什么
垃圾收集器
Serial收集器
Serial收集器是最基礎,歷史最悠久的收集器
ParNew收集器
Parallerl Scavenge收集器
SerialOld收集器
Parallel Old收集器
CMS收集器
G1收集器
ZGC收集器
?
垃圾回收器的作用
CMS
重新標記
什么是三色標記
G1
CMS,G1
如何選擇垃圾收集器
JVM調優
jmap
可視化的性能監控工具
第三方工具
JVM常見參數
堆內存
GC收集器
JVM調優
CPU內存過高怎么排查
內存飆高問題怎么排查
頻繁的minroGc怎么辦
頻繁FUllGC
怎么排查
類加載機制
類加載機制
類的生命周期
類裝載的過程
雙親委派
?為什么使用雙親委派
破壞雙親委派
解釋執行和編譯執行的區別
Java中可作為GC Roots的引用有哪幾種?
所謂的GC Roots,就是一組必須活躍的引用,它們是程序運行時的起點,是一切引用鏈的源頭。在Java中,GC Roots包括以下幾種:
- 虛擬機棧中的引用(方法的參數、局部變量等)
- 本地方法棧中 JNI 的引用
- 類靜態變量
- 運行時常量池中的常量(String 或 Class 類型)
finalize方法
如果對象在進行可達性分析后發現沒有與 GC Roots 相連接的引用鏈,那它將會被第一次標記,隨后進行一次篩選。
篩選的條件是對象是否有必要執行?finalize()
方法。
如果對象在?finalize()
?中成功拯救自己——只要重新與引用鏈上的任何一個對象建立關聯即可。
譬如把自己 (this 關鍵字)賦值給某個類變量或者對象的成員變量,那在第二次標記時它就”逃過一劫“;但是如果沒有抓住這個機會,那么對象就真的要被回收了。
垃圾回收算法
垃圾收集算法主要是三種:
分別是標記-清除算法、標記-復制算法和標記-整理算法。
標記-清除
標記-清除
算法分為兩個階段:
- 標記:標記所有需要回收的對象
- 清除:回收所有被標記的對象
- 優點是實現簡單,缺點是回收過程中會產生內存碎片。
標記-復制
標記-復制
算法可以解決標記-清除算法的內存碎片問題,因為它將內存空間劃分為兩塊,每次只使用其中一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然后清理掉這一塊。
缺點是浪費了一半的內存空間。
標記-整理
標記-整理
算法是標記-清除復制算法的升級版,它不再劃分內存空間,而是將存活的對象向內存的一端移動,然后清理邊界以外的內存。
缺點是移動對象的成本比較高。
分代收集算法
分代收集
算法是目前主流的垃圾收集算法,它根據對象存活周期的不同將內存劃分為幾塊,一般分為新生代和老年代。
新生代用復制算法,因為大部分對象生命周期短。老年代用標記-整理算法,因為對象存活率較高。
為什么要用分代收集
分代收集算法的核心思想是根據對象的生命周期優化垃圾回收。
新生代的對象生命周期短,使用復制算法可以快速回收。老年代的對象生命周期長,使用標記-整理算法可以減少移動對象的成本。
標記復制的標記過程和復制會不會停頓
在標記-復制算法 中,標記階段和復制階段都會觸發STW。
- 標記階段停頓是為了保證對象的引用關系不被修改。
- 復制階段停頓是防止對象在復制過程中被修改。
MinorGC,MajorGC,MixedGC,FullGC
Minor GC 也稱為 Young GC,是指發生在年輕代的垃圾收集。年輕代包含 Eden 區以及兩個 Survivor 區。
Major GC 也稱為 Old GC,主要指的是發生在老年代的垃圾收集。是 CMS 的特有行為。
Mixed GC 是 G1 垃圾收集器特有的一種 GC 類型,它在一次 GC 中同時清理年輕代和部分老年代。
Full GC 是最徹底的垃圾收集,涉及整個 Java 堆和方法區。它是最耗時的 GC,通常在 JVM 壓力很大時發生。
FullGC怎么清理
Full GC 會從 GC Root 出發,標記所有可達對象。新生代使用復制算法,清空 Eden 區。老年代使用標記-整理算法,回收對象并消除碎片。
什么時候觸發FullGC
在進行 Young GC 的時候,如果發現老年代可用的連續內存空間
?<?新生代歷次 Young GC 后升入老年代的對象總和的平均大小
,說明本次 Young GC 后升入老年代的對象大小,可能超過了老年代當前可用的內存空間,就會觸發 Full GC。
執行 Young GC 后老年代沒有足夠的內存空間存放轉入的對象,會立即觸發一次 Full GC。
空間分配擔保是什么
空間分配擔保是指在進行 Minor GC 前,JVM 會確保老年代有足夠的空間存放從新生代晉升的對象。如果老年代空間不足,可能會觸發 Full GC。
垃圾收集器
JVM 的垃圾收集器主要分為兩大類:分代收集器和分區收集器,分代收集器的代表是 CMS,分區收集器的代表是 G1 和 ZGC。
CMS 是第一個關注 GC 停頓時間的垃圾收集器,JDK 1.5 時引入,JDK9 被標記棄用,JDK14 被移除。
G1 在 JDK 1.7 時引入,在 JDK 9 時取代 CMS 成為了默認的垃圾收集器。
ZGC 是 JDK11 推出的一款低延遲垃圾收集器,適用于大內存低延遲服務的內存管理和回收,在 128G 的大堆下,最大停頓時間才 1.68 ms,性能遠勝于 G1 和 CMS。
Serial收集器
Serial收集器是最基礎,歷史最悠久的收集器
如同它的名字(串行),它是一個單線程工作的收集器,使用一個處理器或一條收集線程去完成垃圾收集工作。并且進行垃圾收集時,必須暫停其他所有工作線程,直到垃圾收集結束——這就是所謂的“Stop The World”。
ParNew收集器
ParNew收集器實質上是Serial收集器的多線程并發版本,使用多條線程進行垃圾收集。
?
Parallerl Scavenge收集器
Paraller Scabenge收集器是一款新生代收集器,基于標記復制算法實現,也能夠并行收集。和 ParNew 有些類似,但 Parallel Scavenge 主要關注的是垃圾收集的吞吐量——所謂吞吐量,就是 CPU 用于運行用戶代碼的時間和總消耗時間的比值,比值越大,說明垃圾收集的占比越小。
SerialOld收集器
?Serial Old 是 Serial 收集器的老年代版本,它同樣是一個單線程收集器,使用標記-整理算法。
Parallel Old收集器
Parallel Old是Parallel Scavenge 收集器的老年代版本,基于標記-整理算法實現,使用多條 GC 線程在 STW 期間同時進行垃圾回收。
CMS收集器
CMS 是一種低延遲的垃圾收集器,采用標記-清除算法,分為初始標記、并發標記、重新標記和并發清除四個階段,優點是垃圾回收線程和應用線程同時運行,停頓時間短,適合延遲敏感的應用,但容易產生內存碎片,可能觸發 Full GC。
G1收集器
G1 在 JDK 1.7 時引入,在 JDK 9 時取代 CMS 成為默認的垃圾收集器。
G1 是一種面向大內存、高吞吐場景的垃圾收集器,它將堆劃分為多個小的 Region,通過標記-整理算法,避免了內存碎片問題。優點是停頓時間可控,適合大堆場景,但調優較復雜。
ZGC收集器
?
ZGC 是 JDK 11 時引入的一款低延遲的垃圾收集器,最大特點是將垃圾收集的停頓時間控制在 10ms 以內,即使在 TB 級別的堆內存下也能保持較低的停頓時間。
它通過并發標記和重定位來避免大部分 Stop-The-World 停頓,主要依賴指針染色來管理對象狀態。
- 標記對象的可達性:通過在指針上增加標記位,不需要額外的標記位即可判斷對象的存活狀態。
- 重定位狀態:在對象被移動時,可以通過指針染色來更新對象的引用,而不需要等待全局同步。
適用于需要超低延遲的場景,比如金融交易系統、電商平臺。
垃圾回收器的作用
垃圾回收器的核心作用是自動管理 Java 應用程序的運行時內存。它負責識別哪些內存是不再被應用程序使用的,并釋放這些內存以便重新使用。
這一過程減少了程序員手動管理內存的負擔,降低了內存泄漏和溢出錯誤的風險。
CMS
CMS 使用標記-清除算法進行垃圾收集,分 4 大步:
- 初始標記:標記所有從 GC Roots 直接可達的對象,這個階段需要 STW,但速度很快。
- 并發標記:從初始標記的對象出發,遍歷所有對象,標記所有可達的對象。這個階段是并發進行的。
- 重新標記:完成剩余的標記工作,包括處理并發階段遺留下來的少量變動,這個階段通常需要短暫的 STW 停頓。
- 并發清除:清除未被標記的對象,回收它們占用的內存空間。
重新標記
remark階段,通常結合三色標記法來執行。確保在并發標記期間所有存活對象都被正確標記。目的是修正并發標記階段中可能遺漏的對象引用變化。
什么是三色標記
?
三色標記法用于標記對象的存活狀態,它將對象分為三類:
- 白色(White):尚未訪問的對象。垃圾回收結束后,仍然為白色的對象會被認為是不可達的對象,可以回收。
- 灰色(Gray):已經訪問到但未標記完其引用的對象。灰色對象是需要進一步處理的。
- 黑色(Black):已經訪問到并且其所有引用對象都已經標記過。黑色對象是完全處理過的,不需要再處理。
三色標記法的工作流程:
①、初始標記(Initial Marking):從 GC Roots 開始,標記所有直接可達的對象為灰色。
②、并發標記(Concurrent Marking):在此階段,標記所有灰色對象引用的對象為灰色,然后將灰色對象自身標記為黑色。這個過程是并發的,和應用線程同時進行。
此階段的一個問題是,應用線程可能在并發標記期間修改對象的引用關系,導致一些對象的標記狀態不準確。
③、重新標記(Remarking):重新標記階段的目標是處理并發標記階段遺漏的引用變化。為了確保所有存活對象都被正確標記,remark 需要在 STW 暫停期間執行。
④、使用寫屏障(Write Barrier)來捕捉并發標記階段應用線程對對象引用的更新。通過遍歷這些更新的引用來修正標記狀態,確保遺漏的對象不會被錯誤地回收。
G1
G1 把 Java 堆劃分為多個大小相等的獨立區域Region,每個區域都可以扮演新生代或老年代的角色。
這種區域化管理使得 G1 可以更靈活地進行垃圾收集,只回收部分區域而不是整個新生代或老年代。
①、并發標記,G1 通過并發標記的方式找出堆中的垃圾對象。并發標記階段與應用線程同時執行,不會導致應用線程暫停。
②、混合收集,在并發標記完成后,G1 會計算出哪些區域的回收價值最高(也就是包含最多垃圾的區域),然后優先回收這些區域。這種回收方式包括了部分新生代區域和老年代區域。
選擇回收成本低而收益高的區域進行回收,可以提高回收效率和減少停頓時間。
③、可預測的停頓,G1 在垃圾回收期間仍然需要「Stop the World」。不過,G1 在停頓時間上添加了預測機制,用戶可以 JVM 啟動時指定期望停頓時間,G1 會盡可能地在這個時間內完成垃圾回收。
CMS,G1
特性 | CMS | G1 |
---|---|---|
設計目標 | 低停頓時間 | 可預測的停頓時間 |
并發性 | 是 | 是 |
內存碎片 | 是,容易產生碎片 | 否,通過區域劃分和壓縮減少碎片 |
收集代數 | 年輕代和老年代 | 整個堆,但區分年輕代和老年代 |
并發階段 | 并發標記、并發清理 | 并發標記、并發清理、并發回收 |
停頓時間預測 | 較難預測 | 可配置停頓時間目標 |
容易出現的問題 | 內存碎片、Concurrent Mode Failure | 較少出現長時間停頓 |
CMS 適用于對延遲敏感的應用場景,主要目標是減少停頓時間,但容易產生內存碎片。
G1 則提供了更好的停頓時間預測和內存壓縮能力,適用于大內存和多核處理器環境。
如何選擇垃圾收集器
如果應用程序只需要一個很小的內存空間(大約 100 MB),或者對停頓時間沒有特殊的要求,可以選擇 Serial 收集器。
如果優先考慮應用程序的峰值性能,并且沒有時間要求,或者可以接受 1 秒或更長的停頓時間,可以選擇 Parallel 收集器。
如果響應時間比吞吐量優先級高,或者垃圾收集暫停必須保持在大約 1 秒以內,可以選擇 CMS/ G1 收集器。
如果響應時間是高優先級的,或者堆空間比較大,可以選擇 ZGC 收集器。
JVM調優
操作系統層面,我用過 top、vmstat、iostat、netstat 等命令,可以監控系統整體的資源使用情況,比如說內存、CPU、IO 使用情況、網絡使用情況。
JDK 自帶的命令行工具層面,我用過 jps、jstat、jinfo、jmap、jhat、jstack、jcmd 等,可以查看 JVM 運行時信息、內存使用情況、堆棧信息等。
jmap
①、我一般會使用?jmap -heap <pid>
?查看堆內存摘要,包括新生代、老年代、元空間等。
②、或者使用?jmap -histo <pid>
?查看對象分布。
③、還有生成堆轉儲文件:jmap -dump:format=b,file=<path> <pid>
。
可視化的性能監控工具
①、JConsole:JDK 自帶的監控工具,可以用來監視 Java 應用程序的運行狀態,包括內存使用、線程狀態、類加載、GC 等。
②、VisualVM:一個基于 NetBeans 的可視化工具,在很長一段時間內,VisualVM 都是 Oracle 官方主推的故障處理工具。集成了多個 JDK 命令行工具的功能,非常友好。
③、Java Mission Control:JMC 最初是 JRockit VM 中的診斷工具,但在 Oracle JDK7 Update 40 以后,就綁定到了 HotSpot VM 中。不過后來又被 Oracle 開源出來作為了一個單獨的產品。
第三方工具
MAT,GChisto
JVM常見參數
堆內存
-Xms
:初始堆大小-Xmx
:最大堆大小-XX:NewSize=n
:設置年輕代大小-XX:NewRatio=n
:設置年輕代和年老代的比值。如:n 為 3 表示年輕代和年老代比值為 1:3,年輕代占總和的 1/4-XX:SurvivorRatio=n
:年輕代中 Eden 區與兩個 Survivor 區的比值。如 n=3 表示 Eden 占 3 Survivor 占 2,一個 Survivor 區占整個年輕代的 1/5
GC收集器
-XX:+UseSerialGC
:設置串行收集器-XX:+UseParallelGC
:設置并行收集器-XX:+UseParalledlOldGC
:設置并行老年代收集器-XX:+UseConcMarkSweepGC
:設置并發收集器
JVM調優
JVM 調優是一個復雜的過程,調優的對象包括堆內存、垃圾收集器和 JVM 運行時參數等。
?如果堆內存設置過小,可能會導致頻繁的垃圾回收。
在項目運行期間,我會使用 JVisualVM 定期觀察和分析 GC 日志,如果發現頻繁的 Full GC,我會特意關注一下老年代的使用情況。
接著,通過分析 Heap dump 尋找內存泄漏的源頭,看看是否有未關閉的資源,長生命周期的大對象等。
之后進行代碼優化,比如說減少大對象的創建、優化數據結構的使用方式、減少不必要的對象持有等。
CPU內存過高怎么排查
首先使用top命令觀看CPU占用情況
接著使用jstack命令查看對應進程的線程堆棧信息
然后再使用 top 命令查看進程中線程的占用情況,找到占用 CPU 較高的線程 ID。
top 命令顯示的線程 ID 是十進制的,而 jstack 輸出的是十六進制的,所以需要將線程 ID 轉換為十六進制。
?jstack 的輸出中搜索這個十六進制的線程 ID,找到對應的堆棧信息。
最后,根據堆棧信息定位到具體的業務方法,查看是否有死循環、頻繁的垃圾回收、資源競爭導致的上下文頻繁切換等問題。
內存飆高問題怎么排查
內存飚高一般是因為創建了大量的 Java 對象導致的,如果持續飆高則說明垃圾回收跟不上對象創建的速度,或者內存泄漏導致對象無法回收。
第一,先觀察垃圾回收的情況,可以通過?jstat -gc PID 1000
?查看 GC 次數和時間。
第二步,通過 jmap 命令 dump 出堆內存信息。
第三步,使用可視化工具分析 dump 文件,比如說 VisualVM,找到占用內存高的對象,再找到創建該對象的業務代碼位置,從代碼和業務場景中定位具體問題。
頻繁的minroGc怎么辦
頻繁的 Minor GC 通常意味著新生代中的對象頻繁地被垃圾回收,可能是因為新生代空間設置的過小,或者是因為程序中存在大量的短生命周期對象(如臨時變量)。
可以使用 GC 日志進行分析,查看 GC 的頻率和耗時,找到頻繁 GC 的原因。
或者使用監控工具查看堆內存的使用情況,特別是新生代(Eden 和 Survivor 區)的使用情況。
如果是因為新生代空間不足,可以通過?-Xmn
?增加新生代的大小,減緩新生代的填滿速度。
如果對象需要長期存活,但頻繁從 Survivor 區晉升到老年代,可以通過?-XX:SurvivorRatio
?參數調整 Eden 和 Survivor 的比例。默認比例是 8:1,表示 8 個空間用于 Eden,1 個空間用于 Survivor 區。調整為 6 的話,會減少 Eden 區的大小,增加 Survivor 區的大小,以確保對象在 Survivor 區中存活的時間足夠長,避免過早晉升到老年代。
頻繁FUllGC
頻繁的 Full GC 通常意味著老年代中的對象頻繁地被垃圾回收,可能是因為老年代空間設置的過小,或者是因為程序中存在大量的長生命周期對象。
怎么排查
過專門的性能監控系統,查看 GC 的頻率和堆內存的使用情況,然后根據監控數據分析 GC 的原因。
或者:我一般會使用 JDK 的自帶工具,包括 jmap、jstat 等。
或者使用一些可視化的工具,比如 VisualVM、JConsole 等,查看堆內存的使用情況。
假如是因為大對象直接分配到老年代導致的 Full GC 頻繁,可以通過?-XX:PretenureSizeThreshold
?參數設置大對象直接進入老年代的閾值。
或者將大對象拆分成小對象,減少大對象的創建。比如說分頁。
假如是因為內存泄漏導致的頻繁 Full GC,可以通過分析堆內存 dump 文件找到內存泄漏的對象,再找到內存泄漏的代碼位置。
假如是因為長生命周期的對象進入到了老年代,要及時釋放資源,比如說 ThreadLocal、數據庫連接、IO 資源等。
假如是因為 GC 參數配置不合理導致的頻繁 Full GC,可以通過調整 GC 參數來優化 GC 行為。或者直接更換更適合的 GC 收集器,如 G1、ZGC 等。
類加載機制
類加載機制
JVM 的操作對象是 Class 文件,JVM 把 Class 文件中描述類的數據結構加載到內存中,并對數據進行校驗、解析和初始化,最終轉化成可以被 JVM 直接使用的類型,這個過程被稱為類加載機制。
- 類加載器:負責加載類文件,將類文件加載到內存中,生成 Class 對象。
- 類加載過程:包括加載、驗證、準備、解析和初始化等步驟。
- 雙親委派模型:當一個類加載器接收到類加載請求時,它會把請求委派給父——類加載器去完成,依次遞歸,直到最頂層的類加載器,如果父——類加載器無法完成加載請求,子類加載器才會嘗試自己去加載。
類的生命周期
一個類從被加載到虛擬機內存中開始,到從內存中卸載,整個生命周期需要經過七個階段:加載 、驗證、準備、解析、初始化、使用和卸載。
?
類裝載的過程
類裝載過程包括三個階段:載入、鏈接和初始化。
①、載入:將類的二進制字節碼加載到內存中。
②、鏈接可以細分為三個小的階段:
- 驗證:檢查類文件格式是否符合 JVM 規范
- 準備:為類的靜態變量分配內存并設置默認值。
- 解析:將符號引用替換為直接引用。
③、初始化:執行靜態代碼塊和靜態變量初始化。
雙親委派
雙親委派模型要求類加載器在加載類時,先委托父加載器嘗試加載,只有父加載器無法加載時,子加載器才會加載。
?為什么使用雙親委派
①、避免類的重復加載:父加載器加載的類,子加載器無需重復加載。
②、保證核心類庫的安全性:如?java.lang.*
?只能由 Bootstrap ClassLoader 加載,防止被篡改。
破壞雙親委派
重寫 ClassLoader 的?loadClass()
?方法。
解釋執行和編譯執行的區別
- 解釋:將源代碼逐行轉換為機器碼。
- 編譯:將源代碼一次性轉換為機器碼。
一個是逐行,一個是一次性,再來說說解釋執行和編譯執行的區別:
- 解釋執行:程序運行時,將源代碼逐行轉換為機器碼,然后執行。
- 編譯執行:程序運行前,將源代碼一次性轉換為機器碼,然后執行。
但 JIT 的出現打破了這種刻板印象,JVM 會將熱點代碼(即運行頻率高的代碼)編譯后放入 CodeCache,當下次執行再遇到這段代碼時,會從 CodeCache 中直接讀取機器碼,然后執行。