有沒有想過爲何Java應用程序經過衆所周知的-Xms和-Xmx調優標誌消耗的內存比指定數量多得多?出於各類緣由和可能的優化,JVM能夠分配額外的本機內存。這些額外的分配最終會使消耗的內存超出-Xmx限制。java
在本教程中,咱們將列舉JVM中的一些常見內存分配源,以及它們的大小調整標誌,而後學習如何使用本機內存跟蹤監視它們。算法
堆一般是Java應用程序中最大的內存使用者,但還有其餘人。除了堆以外,JVM還從本機內存中分配出一個至關大的塊來維護類的元數據,應用程序代碼,JIT生成的代碼,內部數據結構等。在下面的部分中,咱們將探討其中的一些分配。緩存
爲了維護有關已加載類的一些元數據,JVM使用名爲Metaspace的專用非堆區域。在Java 8以前,被稱爲PermGen或Permanent Generation。 Metaspace或PermGen包含有關已加載類的元數據,而不是它們的實例,它們保存在堆中。數據結構
這裏重要的是堆大小配置不會影響元空間大小,由於Metaspace是一個堆外數據區。爲了限制Metaspace大小,咱們使用其餘調優標誌:app
JVM中最耗費內存的數據區之一是堆棧,與每一個線程同時建立。堆棧存儲局部變量和部分結果,在方法調用中起着重要做用。jvm
默認的線程堆棧大小取決於平臺,但在大多數現代64位操做系統中,它大約爲1 MB。此大小可經過-Xss調整標誌進行配置。性能
與其餘數據區域相比,當對線程數沒有限制時,分配給堆棧的總內存其實是無限制的。值得一提的是,JVM自己須要一些線程來執行其內部操做,如GC或即時編譯。學習
爲了在不一樣平臺上運行JVM字節碼,須要將其轉換爲機器指令。執行程序時,JIT編譯器負責此編譯。優化
當JVM將字節碼編譯爲彙編指令時,它會將這些指令存儲在稱爲代碼緩存的特殊非堆數據區中。能夠像管理JVM中的其餘數據區同樣管理代碼緩存。 -XX:InitialCodeCacheSize
和-XX:ReservedCodeCacheSize
調整標誌肯定代碼緩存的初始值和可能最大值。ui
JVM附帶了一些GC算法,每一個算法適用於不一樣的用例。全部這些GC算法都有一個共同的特色:他們須要使用一些堆外數據結構來執行他們的任務。這些內部數據結構消耗更多本機內存。
讓咱們從 Strings 開始,這是應用程序和庫代碼中最經常使用的數據類型之一。因爲它們無處不在,它們一般佔據堆的很大一部分。若是大量的這些字符串包含相同的內容,那麼堆的很大一部分將被浪費。
爲了節省一些堆空間,咱們能夠存儲每一個 String 的一個版本,並讓其餘版本引用存儲的版本。此過程稱爲 String Interning 。因爲JVM只能內部編譯時間字符串常量,咱們能夠手動調用字符串的intern方法來獲取內部編譯字符串。
JVM將實際存儲的字符串存儲在本機特殊固定大小並稱爲字符串表的哈希表中,也稱爲字符串池。咱們能夠經過-XX:StringTableSize
調整標誌配置表大小(即桶的數量)。
除了字符串表以外,還有另外一個稱爲運行時常量池的本機數據區域。 JVM使用此池來存儲常量,如編譯時數字文字或必須在運行時解析的方法和字段引用。
JVM一般有大量分配本機內存的嫌疑,但有時開發人員也能夠直接分配本機內存。最多見的方法是被JNI調用的malloc和NIO中可直接調用的ByteBuffers。
在本節中,咱們針對不一樣的優化方案使用了少許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
如今咱們已經瞭解了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算法。
啓用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輸出。
NMT報告所有保留和提交的內存以下:
Native Memory Tracking: Total: reserved=1731124KB, committed=448152KB
保留內存表示咱們的應用程序可能使用的內存總量。相反,提交的內存表示咱們的應用程序如今使用的內存量。
儘管分配了300MB的堆,咱們的應用程序的總預留內存幾乎是1.7 GB,遠遠超過它。相似地,提交的內存大約爲440 MB,這再次遠遠超過300 MB。
在總體瞭解以後,NMT報告每一個分配源的內存分配。因此,讓咱們深刻探討每一個來源。
NMT按咱們的預期報告堆分配:
Java Heap (reserved=307200KB, committed=307200KB) (mmap: reserved=307200KB, committed=307200KB)
300 MB的保留和已提交內存,與咱們的堆大小設置相匹配。
這是NMT關於加載類的元數據的報告:
Class (reserved=1091407KB, committed=45815KB) (classes #6566) (malloc=10063KB #8519) (mmap: reserved=1081344KB, committed=35752KB)
幾乎保留了1 GB,45 MB保留加載6566個類。
這是關於線程分配的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在建立時將內存分配給線程,所以保留和提交的分配是相等的。
讓咱們看看NMT對JIT生成和緩存的彙編指令的報告:
Code (reserved=251549KB, committed=14169KB) (malloc=1949KB #3424) (mmap: reserved=249600KB, committed=12220KB)
目前,正在緩存大約13 MB的代碼,這個數量可能會達到245 MB。
如下是有關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可供選擇,它們各自平衡內存和性能。
如下是有關符號分配的NMT報告,例如字符串表和常量池:
Symbol (reserved=10148KB, committed=10148KB) (malloc=7295KB #66194) (arena=2853KB #1)
將近10 MB分配給符號。
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。能夠很容易地發現內存分配的其餘波動。
NMT能夠提供很是詳細的有關整個存儲空間映射的信息。要啓用此詳細報告,咱們應使用 -XX:NativeMemoryTracking =detail
信息調整標誌。
在本文中,咱們列舉了JVM中本機內存分配的不一樣使用者。而後,咱們學習瞭如何檢查正在運行的應用程序以監視其本機分配。藉助以上這些,咱們能夠更有效地調整應用程序以及運行時環境的大小。
原文:https://www.baeldung.com/native-memory-tracking-in-jvm
做者:Ali Dehghani
譯者:Emma