1. 程式人生 > 其它 >本地記憶體跟蹤NMT詳解

本地記憶體跟蹤NMT詳解

出處

原文連結

1. Overview

為什麼java程式消耗的記憶體,遠超-Xms、-Xmx的限制?因為各種原因,或是為了進行某些優化,JVM會額外分配記憶體。這些額外的分配,會導致java程式佔用的記憶體,超出-Xmx的限制。

本文件列舉了通常情況下,JVM會分配哪幾部分記憶體,以及各部分調整大小的方法。然後,瞭解如何使用Native Memory Tracking工具進行監控。

2. Native Allocations

通常,heap是java程式佔用記憶體中的最大的一部分,但也有特例。除了heap,JVM會分配很大一塊記憶體,用於儲存metadata、application code、the code generated by JIT、internal data structures等等。如下章節,對各部分進行詳述。

2.1. Metaspace

JVM使用專用的non-heap區域,儲存已載入類的元資料。java 8版本以前,此區域稱為PermGen or Permanent Generation。此區域儲存的是類的元資料,而不是類的例項,例項是儲存在heap記憶體中的。
對heap記憶體的限制,是無法影響Metaspace的。如果要調整Metaspace,要使用如下標誌:
-XX:MetaspaceSize,最小值
-XX:MaxMetaspaceSize,最大值

Java 8版本以前,使用-XX:PermSize、-XX:MaxPermSize,含義是一樣的。

2.2. Threads

另一個消耗記憶體較大的部分是stack。建立執行緒時,同時建立stack。stack儲存區域性變數和中間結果,在方法呼叫中扮演重要角色。
執行緒stack的預設大小與平臺相關,但是,對於現在大多的64位作業系統來說,大約是1MB。此值可以通過-Xss調整。
建立的執行緒越多,此部分佔用的記憶體越多。
另外需要注意的是,JVM本身也需要一些執行緒,用於執行內部操作,如:GC、JIT編譯。

2.3. Code Cache

為了在不同平臺執行JVM位元組碼,需要將其轉換成機器指令。程式執行時,JIT編譯器負責這個編譯工作。當JVM將位元組碼編譯為彙編指令時,它將這些指令儲存在一個特殊non-heap區域:Code Cache。Code Cache可以像JVM的其他資料區域一樣被管理。控制此區域的大小,使用如下兩個指令:
-XX:InitialCodeCacheSize,初始值
-XX:ReservedCodeCacheSize,最大值

2.4. Garbage Collection

JVM附帶了一些GC演算法,每個演算法都適用於不同的用例。所有這些GC演算法都有一個共同的特徵:他們需要一些堆外資料結構來執行任務。這些內部資料結構,會消耗一些記憶體。

2.5. Symbols

讓我們從Strings開始,這是最常用的資料型別之一。因為其使用頻率很高,Strings通常會佔用較大一部分heap記憶體。如果大量的Strings包含相同的內容,那麼會造成heap記憶體的浪費。
為了節省記憶體,對於每一個String可以僅存一個副本,然後其他的指向該副本。這個過程稱為字串駐留(String Interning)。JVM僅可以駐留編譯時的字串常量(Compile Time String Constants),我們可以對strings手動呼叫intern()方法以實現駐留。
JVM將駐留的strings儲存在一個專用的固定大小的hashtable中,稱為String Table,也稱為String Pool。可以通過如下標誌調節其大小:-XX:StringTableSize。
除了String Table,還有一個記憶體區域稱為執行時常量池(Runtime Constant Pool),JVM使用這個池來儲存一些必須在執行時解析的常量,如編譯時數值常量或方法和欄位引用。

2.6. Native Byte Buffers

JVM通常是大量記憶體佔用的可疑物件,但有時開發人員也可以直接分配記憶體。最常見的方式是:

  1. malloc call by JNI;
  2. NIO's directByteBuffers;

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. Native Memory Tracking (NMT)

我們知道了JVM中消耗記憶體的幾個源頭,現在就來看看如何監視它們。首先,啟用NMT,在啟動命令中加入如下標誌即可:

-XX:NativeMemoryTracking=off|sumary|detail

NMT預設是關閉的。
假設,我們想要跟蹤一個典型的SpringBoot應用程式:

java -XX:NativeMemoryTracking=summary -Xms300m -Xmx300m -XX:+UseG1GC -jar app.jar

3.1. Instant Snapshots

開啟NMT後,可以使用如下命令,隨時獲取本地記憶體佔用資訊。其中表示java程序的id。

jcmd <pid> VM.native_memory

下面詳解NMT命令的輸出內容。

3.2. Total Allocations

NMT顯示總的預留記憶體、已提交記憶體:

Native Memory Tracking:
Total: reserved=1731124KB, committed=448152KB

預留記憶體表示我們的應用程式可能使用的記憶體總量。已提交記憶體表示應用程式當前使用的記憶體。
儘管僅為應用程式分配了300MB記憶體,但總的預留記憶體近1.7GB。類似的,已提交記憶體近440M。這兩個資料都比300MB多了很多。
除了整體的記憶體佔用資訊外,NMT還報告了各個源頭佔用記憶體的情況。下面章節詳述。

3.3. Heap

heap記憶體佔用情況顯示如下:

Java Heap (reserved=307200KB, committed=307200KB)
          (mmap: reserved=307200KB, committed=307200KB)

預留記憶體、已提交記憶體均為300MB,符合我們對heap記憶體的設定。

3.4. Metaspace

已載入類的元資料記憶體佔用資訊如下:

Class (reserved=1091407KB, committed=45815KB)
      (classes #6566)
      (malloc=10063KB #8519) 
      (mmap: reserved=1081344KB, committed=35752KB)

載入了6566個class,預留記憶體差不多1G,提交記憶體45M。

3.5. Thread

執行緒記憶體分配情況如下:

Thread (reserved=37018KB, committed=37018KB)
       (thread #37)
       (stack: reserved=36864KB, committed=36864KB)
       (malloc=112KB #190) 
       (arena=42KB #72)

37個執行緒,stack記憶體共計36M,差不多每個執行緒的stack佔用1M。JVM在建立執行緒時,同時分配stack記憶體,所以預留記憶體和提交記憶體是一樣的。

3.6. Code Cache

JIT生成並快取的彙編指令的記憶體佔用情況:

Code (reserved=251549KB, committed=14169KB)
     (malloc=1949KB #3424) 
     (mmap: reserved=249600KB, committed=12220KB)

當前,大概13M的記憶體佔用,可能會增加到大約245M(預留記憶體)。

3.7. GC

G1 GC記憶體佔用情況如下:

GC (reserved=61771KB, committed=61771KB)
   (malloc=17603KB #4501) 
   (mmap: reserved=44168KB, committed=44168KB)

預留記憶體大概60M。
Serial GC是一個簡單的多的方法,當使用此方法時,配置方法如下:

java -XX:NativeMemoryTracking=summary -Xms300m -Xmx300m -XX:+UseSerialGC -jar app.jar

記憶體佔用情況如下,僅僅用了1M:

GC (reserved=1034KB, committed=1034KB)
   (malloc=26KB #158) 
   (mmap: reserved=1008KB, committed=1008KB)

當然,我們不能僅根據記憶體消耗來決定選擇什麼GC演算法,因為Serial GC的“stop-the-world”特性,可能會導致效能下降。

3.8. Symbol

symbol記憶體佔用情況如下,如string table和constant pool:

Symbol (reserved=10148KB, committed=10148KB)
       (malloc=7295KB #66194) 
       (arena=2853KB #1)

大概佔用10M。

3.9. NMT over Time

NMT使得我們可以跟蹤記憶體佔用情況。首先,要記錄應用程式當前的記憶體佔用情況,做為基線。命令如下:

$ jcmd <pid> VM.native_memory baseline
Baseline succeeded

然後,過一段時間,可以將當前記憶體佔用與基線做比較:

$ 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

預留記憶體、提交記憶體分別增長了3M、6M。記憶體分配中的其他波動也可以很容易地發現。

3.10. Detailed NMT

NMT可以提供關於整個記憶體空間佔用情況的非常詳細的資訊。要顯示詳細資訊,要使用如下標誌:

-XX:NativeMemoryTracking=detail

4. Conclusion

我們列舉了JVM中記憶體佔用的不同類別。然後,我們學習瞭如何監控一個正在執行的應用程式的記憶體佔用情況。有了這些,我們可以更有效地調整執行時環境的大小。
關於JIT
對於Java語言:
一、你可以說它是編譯型的:因為所有的Java程式碼都是要編譯的,.java不經過編譯就什麼用都沒有。
二、你可以說它是解釋型的:因為java程式碼編譯後不能直接執行,它是解釋執行在JVM上的,所以它是解釋執行的,那也就算是解釋的了。
三、但是,現在的JVM為了效率,都有一些JIT優化。它又會把.class的二進位制程式碼編譯為本地的程式碼(彙編)直接執行,所以,又是編譯的。

像C、C++ 他們經過一次編譯之後直接可以編譯成作業系統瞭解的型別,可以直接執行的,所以他們是編譯型的語言。沒有經過第二次的處理。
而Java不一樣,他首先由編譯器編譯成.class型別的檔案,這個是java自己型別的檔案 然後再通過虛擬機器(JVM)從.class檔案中讀一行解釋執行一行,所以他是解釋型的語言,而由於java對於多種不同的作業系統有不同的JVM,所以,Java實現了真正意義上的跨平臺!

JIT:Just In Time Compiler,一般翻譯為即時編譯器,這是是針對解釋型語言而言的,而且並非虛擬機器必須,是一種優化手段,Java的商用虛擬機器HotSpot就有這種技術手段,Java虛擬機器標準對JIT的存在沒有作出任何規範,所以這是虛擬機器實現的自定義優化技術。
HotSpot虛擬機器的執行引擎在執行Java程式碼是可以採用【解釋執行】和【編譯執行】兩種方式的,如果採用的是編譯執行方式,那麼就會使用到JIT,而解釋執行就不會使用到JIT,所以,早期說Java是解釋型語言,是沒有任何問題的,而在擁有JIT的Java虛擬機器環境下,說Java是解釋型語言嚴格意義上已經不正確了。
HotSpot中的編譯器是javac,他的工作是將原始碼編譯成位元組碼,這部分工作是完全獨立的,完全不需要執行時參與,所以Java程式的編譯是半獨立的實現。有了位元組碼,就有直譯器來進行解釋執行,這是早期虛擬機器的工作流程,後來,虛擬機器會將執行頻率高的方法或語句塊通過JIT編譯成本地機器碼,提高了程式碼執行的效率。
--結束--