1.概述
有沒有想過為什麼Java應用程式通過眾所周知的-Xms和-Xmx調優標誌消耗的記憶體比指定數量多得多?出於各種原因和可能的優化,JVM可以分配額外的本機記憶體。這些額外的分配最終會使消耗的記憶體超出-Xmx限制。
在本教程中,我們將列舉JVM中的一些常見記憶體分配源,以及它們的大小調整標誌,然後學習如何使用本機記憶體跟蹤監視它們。
2.原生分配
堆通常是Java應用程式中最大的記憶體使用者,但還有其他人。除了堆之外,JVM還從本機記憶體中分配出一個相當大的塊來維護類的後設資料,應用程式程式碼,JIT生成的程式碼,內部資料結構等。在下面的部分中,我們將探討其中的一些分配。
2.1. Metaspace(元空間)
為了維護有關已載入類的一些後設資料,JVM使用名為Metaspace的專用非堆區域。在Java 8之前,被稱為PermGen或Permanent Generation。 Metaspace或PermGen包含有關已載入類的後設資料,而不是它們的例項,它們儲存在堆中。
這裡重要的是堆大小配置不會影響元空間大小,因為Metaspace是一個堆外資料區。為了限制Metaspace大小,我們使用其他調優標誌:
- -XX:MetaspaceSize和-XX:MaxMetaspaceSize設定最小和最大元空間大小
- 在Java 8之前,-XX:PermSize和-XX:MaxPermSize設定最小和最大PermGen大小
2.2. Threads(執行緒)
JVM中最耗費記憶體的資料區之一是堆疊,與每個執行緒同時建立。堆疊儲存區域性變數和部分結果,在方法呼叫中起著重要作用。
預設的執行緒堆疊大小取決於平臺,但在大多數現代64位作業系統中,它大約為1 MB。此大小可通過-Xss調整標誌進行配置。
與其他資料區域相比,當對執行緒數沒有限制時,分配給堆疊的總記憶體實際上是無限制的。值得一提的是,JVM本身需要一些執行緒來執行其內部操作,如GC或即時編譯。
2.3. Code Cache(程式碼快取)
為了在不同平臺上執行JVM位元組碼,需要將其轉換為機器指令。執行程式時,JIT編譯器負責此編譯。
當JVM將位元組碼編譯為彙編指令時,它會將這些指令儲存在稱為程式碼快取的特殊非堆資料區中。可以像管理JVM中的其他資料區一樣管理程式碼快取。 -XX:InitialCodeCacheSize
和-XX:ReservedCodeCacheSize
調整標誌確定程式碼快取的初始值和可能最大值。
2.4. Garbage Collection(垃圾回收)
JVM附帶了一些GC演算法,每個演算法適用於不同的用例。所有這些GC演算法都有一個共同的特點:他們需要使用一些堆外資料結構來執行他們的任務。這些內部資料結構消耗更多本機記憶體。
2.5. Symbols(符號)
讓我們從 Strings 開始,這是應用程式和庫程式碼中最常用的資料型別之一。由於它們無處不在,它們通常佔據堆的很大一部分。如果大量的這些字串包含相同的內容,那麼堆的很大一部分將被浪費。
為了節省一些堆空間,我們可以儲存每個 String 的一個版本,並讓其他版本引用儲存的版本。此過程稱為 String Interning 。由於JVM只能內部編譯時間字串常量,我們可以手動呼叫字串的intern方法來獲取內部編譯字串。
JVM將實際儲存的字串儲存在本機特殊固定大小並稱為字串表的雜湊表中,也稱為字串池。我們可以通過-XX:StringTableSize
調整標誌配置表大小(即桶的數量)。
除了字串表之外,還有另一個稱為執行時常量池的本機資料區域。 JVM使用此池來儲存常量,如編譯時數字文字或必須在執行時解析的方法和欄位引用。
2.6. Native Byte Buffers(本地位元組緩衝區)
JVM通常有大量分配本機記憶體的嫌疑,但有時開發人員也可以直接分配本機記憶體。最常見的方法是被JNI呼叫的malloc和NIO中可直接呼叫的ByteBuffers。
2.7. Additional Tuning Flags(額外的調整標誌)
在本節中,我們針對不同的優化方案使用了少量JVM調優標誌。使用以下提示,我們幾乎可以找到與特定概念相關的所有調優標誌:
$ java -XX:+PrintFlagsFinal -version | grep <concept>
PrintFlagsFinal列印JVM中的所有-XX選項。例如,要查詢所有與Metaspace相關的標誌:
$ java -XX:+PrintFlagsFinal -version | grep Metaspace
// truncated
uintx MaxMetaspaceSize = 18446744073709547520 {product}
uintx MetaspaceSize = 21807104 {pd product}
// truncated
3. 本機記憶體跟蹤 (NMT)
現在我們已經瞭解了JVM中本機記憶體分配的常見來源,現在是時候找出如何監視它們了。首先,我們應該使用另一個JVM調優標誌啟用本機記憶體跟蹤:-XX:NativeMemoryTracking = off | sumary | detail
。預設情況下,NMT處於關閉狀態,但我們可以使其檢視其觀察的摘要或詳細檢視。
假設我們想要跟蹤典型Spring Boot應用程式的本機分配:
$ java -XX:NativeMemoryTracking=summary -Xms300m -Xmx300m -XX:+UseG1GC -jar app.jar
在這裡,我們在分配300 MB堆空間的同時啟用NMT,G1作為我們的GC演算法。
3.1. 例項快照
啟用NMT後,我們可以使用jcmd命令隨時獲取本機記憶體資訊:
$ jcmd <pid> VM.native_memory
為了找到JVM應用程式的PID,我們可以使用jps命令:
$ jps -l
7858 app.jar // This is our app
7899 sun.tools.jps.Jps
現在,如果我們將jcmd與適當的pid一起使用,VM.native_memory會使JVM列印出有關本機分配的資訊:
$ jcmd 7858 VM.native_memory
讓我們逐節分析NMT輸出。
3.2. 總分配
NMT報告全部保留和提交的記憶體如下:
Native Memory Tracking:
Total: reserved=1731124KB, committed=448152KB
保留記憶體表示我們的應用程式可能使用的記憶體總量。相反,提交的記憶體表示我們的應用程式現在使用的記憶體量。
儘管分配了300MB的堆,我們的應用程式的總預留記憶體幾乎是1.7 GB,遠遠超過它。類似地,提交的記憶體大約為440 MB,這再次遠遠超過300 MB。
在整體瞭解之後,NMT報告每個分配源的記憶體分配。所以,讓我們深入探討每個來源。
3.3. Heap(堆)
NMT按我們的預期報告堆分配:
Java Heap (reserved=307200KB, committed=307200KB)
(mmap: reserved=307200KB, committed=307200KB)
300 MB的保留和已提交記憶體,與我們的堆大小設定相匹配。
3.4. Metaspace(元空間)
這是NMT關於載入類的後設資料的報告:
Class (reserved=1091407KB, committed=45815KB)
(classes #6566)
(malloc=10063KB #8519)
(mmap: reserved=1081344KB, committed=35752KB)
幾乎保留了1 GB,45 MB保留載入6566個類。
3.5. Thread(執行緒)
這是關於執行緒分配的NMT報告:
Thread (reserved=37018KB, committed=37018KB)
(thread #37)
(stack: reserved=36864KB, committed=36864KB)
(malloc=112KB #190)
(arena=42KB #72)
總共有36 MB的記憶體被分配給37個執行緒的堆疊 - 每個堆疊大約1 MB。 JVM在建立時將記憶體分配給執行緒,因此保留和提交的分配是相等的。
3.6. Code Cache(程式碼緩衝區)
讓我們看看NMT對JIT生成和快取的彙編指令的報告:
Code (reserved=251549KB, committed=14169KB)
(malloc=1949KB #3424)
(mmap: reserved=249600KB, committed=12220KB)
目前,正在快取大約13 MB的程式碼,這個數量可能會達到245 MB。
3.7. GC
以下是有關G1 GC記憶體使用情況的NMT報告:
GC (reserved=61771KB, committed=61771KB)
(malloc=17603KB #4501)
(mmap: reserved=44168KB, committed=44168KB)
我們可以看到,保留和已提交都接近60 MB,致力於幫助G1。
讓我們來看看更簡單的GC的記憶體使用情況,比如Serial GC:
$ java -XX:NativeMemoryTracking=summary -Xms300m -Xmx300m -XX:+UseSerialGC -jar app.jar
Serial GC 幾乎使用不到1 MB:
GC (reserved=1034KB, committed=1034KB)
(malloc=26KB #158)
(mmap: reserved=1008KB, committed=1008KB)
顯然,我們不能僅僅因為其記憶體使用而選擇GC演算法,因為序列GC的暫停回收本質可能會導致效能下降。但是,還有幾個GC可供選擇,它們各自平衡記憶體和效能。
3.8. Symbol(符號)
以下是有關符號分配的NMT報告,例如字串表和常量池:
Symbol (reserved=10148KB, committed=10148KB)
(malloc=7295KB #66194)
(arena=2853KB #1)
將近10 MB分配給符號。
3.9. 隨著時間的推移的NMT
NMT允許我們跟蹤記憶體分配如何隨時間變化。首先,我們應該將應用程式的當前狀態標記為基線:
$ jcmd <pid> VM.native_memory baseline
Baseline succeeded
然後,過了一會兒,我們可以將當前的記憶體使用情況與該基線(baseline)進行比較:
$ jcmd <pid> VM.native_memory summary.diff
NMT使用+和 - 符號將告訴我們在此期間記憶體使用情況如何變化:
Total: reserved=1771487KB +3373KB, committed=491491KB +6873KB
- Java Heap (reserved=307200KB, committed=307200KB)
(mmap: reserved=307200KB, committed=307200KB)
- Class (reserved=1084300KB +2103KB, committed=39356KB +2871KB)
// Truncated
保留和提交的總記憶體分別增加了3 MB和6 MB。可以很容易地發現記憶體分配的其他波動。
3.10. 詳細的NMT
NMT可以提供非常詳細的有關整個儲存空間對映的資訊。要啟用此詳細報告,我們應使用 -XX:NativeMemoryTracking =detail
資訊調整標誌。
4. 結束語
在本文中,我們列舉了JVM中本機記憶體分配的不同使用者。然後,我們學習瞭如何檢查正在執行的應用程式以監視其本機分配。藉助以上這些,我們可以更有效地調整應用程式以及執行時環境的大小。
原文:https://www.baeldung.com/native-memory-tracking-in-jvm
作者:Ali Dehghani
譯者:Emma