26、如何監控和診斷JVM堆內和堆外內存使用?

目錄

今天我要問你的問題是,如何監控和診斷 JVM 堆內和堆外內存使用?

典型回答

考點分析

知識擴展

首先,堆內部是什麼結構?

1. 新生代

2. 老年代

3. 永久代

第二,分析完堆內空間,我們一起來看看 JVM 堆外內存到底包括什麼?

一課一練


上一講我介紹了 JVM 內存區域的劃分,總結了相關的一些概念,今天我將結合 JVM 參數、工具等方面,進一步分析 JVM 內存結構,包括外部資料相對較少的堆外部分。

今天我要問你的問題是,如何監控和診斷 JVM 堆內和堆外內存使用?

典型回答

瞭解 JVM 內存的方法有很多,具體能力範圍也有區別,簡單總結如下:

  •   可以使用綜合性的圖形化工具,如 JConsole、VisualVM(注意,從 Oracle JDK 9 開始,VisualVM 已經不再包含在 JDK  安裝包中)等。這些工具具體使用起來相對比較直觀,直接連接到 Java 進程,然後就可以在圖形化界面裏掌握內存使用情況。

 

以 JConsole 爲例,其內存頁面可以顯示常見的堆內存和各種堆外部分使用狀態。

  •   也可以使用命令行工具進行運行時查詢,如 jstat 和 jmap 等工具都提供了一些選項,可以查看堆、方法區等使用數據。
  •   或者,也可以使用 jmap 等提供的命令,生成堆轉儲(Heap Dump)文件,然後利用 jhat 或 Eclipse MAT 等堆轉儲分析工具進行詳細分析。
  •   如果你使用的是 Tomcat、Weblogic 等 Java EE 服務器,這些服務器同樣提供了內存管理相關的功能。
  •   另外,從某種程度上來說,GC 日誌等輸出,同樣包含着豐富的信息

這裏有一個相對特殊的部分,就是是堆外內存中的直接內存,前面的工具基本不適用,可以使用 JDK 自帶的 Native Memory Tracking(NMT)特性,它會從 JVM 本地內存分配的角度進行解讀。

 

考點分析

今天選取的問題是 Java 內存管理相關的基礎實踐,對於普通的內存問題,掌握上面我給出的典型工具和方法就足夠了。這個問題也可以理解爲考察兩個基本方面能力,第一,你是否真的理解了 JVM 的內部結構;第二,具體到特定內存區域,應該使用什麼工具或者特性去定位,可以用什麼參數調整。

對於 JConsole 等工具的使用細節,我在專欄裏不再贅述,如果你還沒有接觸過,你可以參考JConsole 官方教程。我這裏特別推薦Java Mission Control(JMC),這是一個非常強大的工具,不僅僅能夠使用JMX進行普通的管理、監控任務,還可以配合Java Flight Recorder(JFR)技術,以非常低的開銷,收集和分析 JVM 底層的 Profiling 和事件等信息。目前, Oracle 已經將其開源,如果你有興趣請可以查看 OpenJDK 的Mission Control項目。

關於內存監控與診斷,我會在知識擴展部分結合 JVM 參數和特性,儘量從龐雜的概念和 JVM 參數選項中,梳理出相對清晰的框架:

  •   細化對各部分內存區域的理解,堆內結構是怎樣的?如何通過參數調整?
  •   堆外內存到底包括哪些部分?具體大小受哪些因素影響?


知識擴展

今天的分析,我會結合相關 JVM 參數和工具,進行對比以加深你對內存區域更細粒度的理解。

 

首先,堆內部是什麼結構?

對於堆內存,我在上一講介紹了最常見的新生代和老年代的劃分,其內部結構隨着 JVM 的發展和新 GC 
方式的引入,可以有不同角度的理解,下圖就是年代視角的堆結構示意圖。


你可以看到,按照通常的 GC 年代方式劃分,Java 堆內分爲:

1. 新生代

新生代是大部分對象創建和銷燬的區域,在通常的 Java 應用中,絕大部分對象生命週期都是很短暫的。其內部又分爲 Eden 區域,作爲對象初始分配的區域;兩個 Survivor Space,有時候也叫 from、to 區域,被用來放置從 Minor GC 中保留下來的對象。

  •   JVM 會隨意選取一個 Survivor 區域作爲“to”,然後會在 GC 過程中進行區域間拷貝,也就是將 Eden 中存活下來的對象和 from  區域的對象,拷貝到這個“to”區域。這種設計主要是爲了防止內存的碎片化,並進一步清理無用對象。
  •   從內存模型而不是垃圾收集的角度,對 Eden 區域繼續進行劃分,Hotspot JVM 還有一個概念叫做 Thread Local Allocation Buffer(TLAB),據我所知所有 OpenJDK 衍生出來的 JVM 都提供了 TLAB 的設計。這是 JVM  爲每個線程分配的一個私有緩存區域,否則,多線程同時分配內存時,爲避免操作同一地址,可能需要使用加鎖等機制,進而影響分配速度,你可以參考下面的示意圖。從圖中可以看出,TLAB 仍然在堆上,它是分配在 Eden 區域內的。其內部結構比較直觀易懂,start、end 就是起始地址,top(指針)則表示已經分配到哪裏了。所以我們分配新對象,JVM 就會移動 top,當 top 和 end 相遇時,即表示該緩存已滿,JVM 會試圖再從 Eden 裏分配一塊兒。

 

2. 老年代

放置長生命週期的對象,通常都是從 Survivor 區域拷貝過來的對象。當然,也有特殊情況,我們知道普通的對象會被分配在 TLAB 上;如果對象較大,JVM 會試圖直接分配在 Eden 其他位置上;如果對象太大,完全無法在新生代找到足夠長的連續空閒空間,JVM 就會直接分配到老年代。

 

3. 永久代

這部分就是早期 Hotspot JVM 的方法區實現方式了,儲存 Java 類元數據、常量池、Intern 字符串緩存,在 JDK 8 
之後就不存在永久代這塊兒了。

那麼,我們如何利用 JVM 參數,直接影響堆和內部區域的大小呢?我來簡單總結一下:

  •   最大堆體積
-Xmx value

 

  • 初始的最小堆體積
-Xms value

 

  • 老年代和新生代的比例
-XX:NewRatio=value

默認情況下,這個數值是 2,意味着老年代是新生代的 2 倍大;換句話說,新生代是堆大小的 1/3。

 

  •   當然,也可以不用比例的方式調整新生代的大小,直接指定下面的參數,設定具體的內存大小數值。
-XX:NewSize=value

 

  • Eden 和 Survivor 的大小是按照比例設置的,如果 SurvivorRatio 是 8,那麼 Survivor 區域就是 Eden 的 1/8 大小,也就是新生代的 1/10,因爲 YoungGen=Eden + 2*Survivor,JVM 參數格式是
-XX:SurvivorRatio=value

 

  • TLAB 當然也可以調整,JVM 實現了複雜的適應策略,如果你有興趣可以參考這篇說明。

例題:

對於JVM內存配置參數:-Xmx10240m -Xms10240m -Xmn5120m -XX:SurvivorRatio=3,其最小內存值和Survivor區總大小分別是( )

A 5120m,1024m

B 5120m,2048m

C 10240m,1024m

D 10240m,2408m



正確答案是:D

解析:

JVM參數配置中:

-Xmx指定 jvm 的最大堆大小。

-Xms指定 jvm 的初始堆大小。

-Xmn指定 jvm 中年輕代(New Generation)的大小。

-XX:SurvivorRatio:指定年輕代中Eden區與Survivor區的大小比值。



由題目得年輕代爲5120m,年輕代中Eden區與Survivor區的大小比值爲3(-XX:SurvivorRatio=3),
而Survivor區有兩個,即將年輕代看做5份,每個Survivor區佔一份,Eden區佔3份,
Survivor區大小=5120/5=1024m,Survivor區總大小爲2048m。

-Xms初始堆大小即最小內存值爲10240m。

 

不知道你有沒有注意到,我在年代視角的堆結構示意圖也就是第一張圖中,還標記出了 Virtual 區域,這是塊兒什麼區域呢?

在 JVM 內部,如果 Xms 小於 Xmx,堆的大小並不會直接擴展到其上限,也就是說保留的空間(reserved)大於實際能夠使用的空間(committed)。當內存需求不斷增長的時候,JVM 會逐漸擴展新生代等區域的大小,所以 Virtual 區域代表的就是暫時不可用(uncommitted)的空間。

 

第二,分析完堆內空間,我們一起來看看 JVM 堆外內存到底包括什麼?

在 JMC 或 JConsole 的內存管理界面,會統計部分非堆內存,但提供的信息相對有限,下圖就是 JMC 活動內存池的截圖。


接下來我會依賴 NMT 特性對 JVM 進行分析,它所提供的詳細分類信息,非常有助於理解 JVM 內部實現。

首先來做些準備工作,開啓 NMT 並選擇 summary 模式,

-XX:NativeMemoryTracking=summary

爲了方便獲取和對比 NMT 輸出,選擇在應用退出時打印 NMT 統計信息

-XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics

然後,執行一個簡單的在標準輸出打印 HelloWorld 的程序,就可以得到下面的輸出


我來仔細分析一下,NMT 所表徵的 JVM 本地內存使用:

  •   第一部分非常明顯是 Java 堆,我已經分析過使用什麼參數調整,不再贅述。
  •   第二部分是 Class 內存佔用,它所統計的就是 Java 類元數據所佔用的空間,JVM 可以通過類似下面的參數調整其大小:
-XX:MaxMetaspaceSize=value

對於本例,因爲 HelloWorld 沒有什麼用戶類庫,所以其內存佔用主要是啓動類加載器(Bootstrap)加載的核心類庫。你可以使用下面的小技巧,調整啓動類加載器元數據區,這主要是爲了對比以加深理解,也許只有在 hack JDK 時纔有實際意義。

-XX:InitialBootClassLoaderMetaspaceSize=30720

 

1、下面是 Thread,這裏既包括 Java 線程,如程序主線程、Cleaner 線程等,也包括 GC 等本地線程。你有沒有注意到,即使是一個 HelloWorld 程序,這個線程數量竟然還有 25。似乎有很多浪費,設想我們要用 Java 作爲 Serverless 運行時,每個 function 是非常短暫的,如何降低線程數量呢?


如果你充分理解了專欄講解的內容,對 JVM 內部有了充分理解,思路就很清晰了:

  •   JDK 9 的默認 GC 是 G1,雖然它在較大堆場景表現良好,但本身就會比傳統的 Parallel GC 或者 Serial GC 之類複雜太多,所以要麼降低其並行線程數目,要麼直接切換 GC 類型;
  •   JIT 編譯默認是開啓了 TieredCompilation 的,將其關閉,那麼 JIT 也會變得簡單,相應本地線程也會減少。

我們來對比一下,這是默認參數情況的輸出:


下面是替換了默認 GC,並關閉 TieredCompilation 的命令行


得到的統計信息如下,線程數目從 25 降到了 17,消耗的內存也下降了大概 1/3。

 

2、 接下來是 Code 統計信息,顯然這是 Code Cache 相關內存,也就是 JIT compiler 存儲編譯熱點方法等信息的地方,JVM 提供了一系列參數可以限制其初始值和最大值等,例如:

-XX:InitialCodeCacheSize=value
-XX:ReservedCodeCacheSize=value

你可以設置下列 JVM 參數,也可以只設置其中一個,進一步判斷不同參數對 Code Cache 大小的影響。

很明顯,Code Cache 空間下降非常大,這是因爲我們關閉了複雜的 TieredCompilation,而且還限制了其初始大小。

 

3、下面就是 GC 部分了,就像我前面介紹的,G1 等垃圾收集器其本身的設施和數據結構就非常複雜和龐大,例如 Remembered Set 通常都會佔用 20%~30% 的堆空間。如果我把 GC 明確修改爲相對簡單的 Serial GC,會有什麼效果呢?

使用命令:

-XX:+UseSerialGC

可見,不僅總線程數大大降低(25 → 13),而且 GC 設施本身的內存開銷就少了非常多。據我所知,AWS Lambda 中 Java 運行時就是使用的 Serial GC,可以大大降低單個 function 的啓動和運行開銷。

  •   Compiler 部分,就是 JIT 的開銷,顯然關閉 TieredCompilation 會降低內存使用。
  •   其他一些部分佔比都非常低,通常也不會出現內存使用問題,請參考官方文檔。唯一的例外就是 Internal(JDK 11 以後在 Other 部分)部分,其統計信息包含着 Direct Buffer 的直接內存,這其實是堆外內存中比較敏感的部分,很多堆外內存 OOM 就發生在這裏,請參考專欄第 12 講的處理步驟。原則上 Direct Buffer 是不推薦頻繁創建或銷燬的,如果你懷疑直接內存區域有問題,通常可以通過類似 instrument  構造函數等手段,排查可能的問題。


JVM 內部結構就介紹到這裏,主要目的是爲了加深理解,很多方面只有在定製或調優 JVM 運行時才能真正涉及,隨着微服務和 Serverless 等技術的興起,JDK 確實存在着爲新特徵的工作負載進行定製的需求。

今天我結合 JVM 參數和特性,系統地分析了 JVM 堆內和堆外內存結構,相信你一定對 JVM 內存結構有了比較深入的瞭解,在定製 Java 運行時或者處理 OOM 等問題的時候,思路也會更加清晰。JVM 問題千奇百怪,如果你能快速將問題縮小,大致就能清楚問題可能出在哪裏,例如如果定位到問題可能是堆內存泄漏,往往就已經有非常清晰的思路和工具可以去解決了。

 

一課一練

關於今天我們討論的題目你做到心中有數了嗎?今天的思考題是,如果用程序的方式而不是工具,對 Java 內存使用進行監控,有哪些技術可以做到?

答:利用JMX MXbean公開出來的api。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章