java 進程佔用系統內存過高分析

JVM的內存

先放一張JVM的內存劃分圖,總體上可以分爲堆和非堆(粗略劃分,基於java8)

 

 

那麼一個Java進程最大佔用的物理內存爲:

Max Memory = eden + survivor + old + String Constant Pool + Code cache + compressed class space + Metaspace + Thread stack(*thread num) + Direct + Mapped + JVM + Native Memory

堆和非堆內存

堆和非堆內存有以下幾個概念:

init

表示JVM在啓動時從操作系統申請內存管理的初始內存大小(以字節爲單位)。JVM可能從操作系統請求額外的內存,也可以隨着時間的推移向操作系統釋放內存(經實際測試,這個內存並沒有過主動釋放)。這個init的值可能不會定義。

used

表示當前使用的內存量(以字節爲單位)

committed

表示保證可供 Jvm使用的內存大小(以字節爲單位)。 已提交內存的大小可能隨時間而變化(增加或減少)。 JVM也可能向系統釋放內存,導致已提交的內存可能小於 init,但是committed永遠會大於等於used。

max

表示可用於內存管理的最大內存(以字節爲單位)。

NMT追蹤內存

NMT(Native Memory tracking)是一種Java HotSpot VM功能,可跟蹤Java HotSpot VM的內部內存使用情況(jdk8+)。

本文簡單介紹下該工具的使用,主要用來解釋Java中的內存

開啓

在啓動參數中添加-XX:NativeMemoryTracking=detail

查看

jcmd 進程id VM.native_memory summary scale=MB

輸出結果

Native Memory Tracking:
Total: reserved=6988749KB, committed=3692013KB
 堆內存
- Java Heap (reserved=5242880KB, committed=3205008KB)
 (mmap: reserved=5242880KB, committed=3205008KB)
 類加載信息
- Class (reserved=1114618KB, committed=74642KB)
 (classes #10657)
 (malloc=4602KB #32974)
 (mmap: reserved=1110016KB, committed=70040KB)
 線程棧
- Thread (reserved=255213KB, committed=255213KB)
 (thread #248)
 (stack: reserved=253916KB, committed=253916KB)
 (malloc=816KB #1242)
 (arena=481KB #494)
 代碼緩存
- Code (reserved=257475KB, committed=46551KB)
 (malloc=7875KB #10417)
 (mmap: reserved=249600KB, committed=38676KB)
 垃圾回收
- GC (reserved=31524KB, committed=23560KB)
 (malloc=17180KB #2113)
 (mmap: reserved=14344KB, committed=6380KB)
 編譯器
- Compiler (reserved=598KB, committed=598KB)
 (malloc=467KB #1305)
 (arena=131KB #3)
 內部
- Internal (reserved=6142KB, committed=6142KB)
 (malloc=6110KB #23691)
 (mmap: reserved=32KB, committed=32KB)
 符號
- Symbol (reserved=11269KB, committed=11269KB)
 (malloc=8544KB #89873)
 (arena=2725KB #1)
 nmt
- Native Memory Tracking (reserved=2781KB, committed=2781KB)
 (malloc=199KB #3036)
 (tracking overhead=2582KB)
- Arena Chunk (reserved=194KB, committed=194KB)
 (malloc=194KB)
- Unknown (reserved=66056KB, committed=66056KB)
 (mmap: reserved=66056KB, committed=66056KB)

nmt返回結果中有reserved和committed兩個值,這裏解釋一下:

reserved

reserved memory 是指JVM 通過mmaped PROT_NONE 申請的虛擬地址空間,在頁表中已經存在了記錄(entries),保證了其他進程不會被佔用。

在堆內存下,就是xmx值,jvm申請的最大保留內存。

committed

committed memory 是JVM向操做系統實際分配的內存(malloc/mmap),mmaped PROT_READ | PROT_WRITE,相當於程序實際申請的可用內存。

在堆內存下,就是xms值,最小堆內存,heap committed memory。

注意,committed申請的內存並不是說直接佔用了物理內存,由於操作系統的內存管理是惰性的,對於已申請的內存雖然會分配地址空間,但並不會直接佔用物理內存,真正使用的時候纔會映射到實際的物理內存。所以committed > res也是很可能的

Linux內存與JVM內存

再來說說JVM內存與該進程的內存。

現在有一個Java進程,JVM所有已使用內存區域加起來才2G(不包括Native Memory,也沒有顯式調用JNI的地方),但從top/pmap上看該進程res已經2.9G了

#heap + noheap
Memory used total max usage
heap 1921M 2822M 4812M 39.93%
par_eden_space 1879M 2457M 2457M 76.47%
par_survivor_space 4M 307M 307M 1.56% 
cms_old_gen 37M 57M 2048M 1.84% 
nonheap 103M 121M -1 85.00%
code_cache 31M 37M 240M 13.18%
metaspace 63M 74M -1 85.51%
compressed_class_space 7M 9M 1024M 0.75%
direct 997K 997K - 100.00
mapped 0K 0K - NaN%

 

#top
top -p 6267
top - 17:39:40 up 140 days, 5:39, 5 users, load average: 0.00, 0.01, 0.00
Tasks: 1 total, 0 running, 1 sleeping, 0 stopped, 0 zombie
Cpu(s): 0.2%us, 0.1%sy, 0.0%ni, 99.7%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Mem: 8059152k total, 5255384k used, 2803768k free, 148872k buffers
Swap: 0k total, 0k used, 0k free, 1151812k cached
 PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
 6267 root 20 0 8930m 2.9g 17m S 0.0 37.6 4:13.31 java

那麼其餘的0.9G內存去哪了呢?

這時候就要介紹下JVM與Linux內存的聯繫了

當Java程序啓動後,會根據Xmx爲堆預申請一塊保留內存,並不會直接使用,也不會佔用物理內存

然後申請(malloc之類的方法)Xms大小的虛擬內存,但是由於操作系統的內存管理是惰性的,有一個內存延遲分配的概念。malloc雖然會分配內存地址空間,但是並沒有映射到實際的物理內存,只有當對該地址空間賦值時,纔會真正的佔用物理內存,纔會影響RES的大小。

所以可能會出現進程所用內存大於當前堆+非堆的情況。

比如說該Java程序在5分鐘前,有一定活動,佔用了2.6G堆內存(無論堆中的什麼代),經過GC之後,雖然堆內存已經被回收了,堆佔用很低,但GC的回收只是針對Jvm申請的這塊內存區域,並不會調用操作系統釋放內存。所以該進程的內存並不會釋放,這時就會出現進程內存遠遠大於堆+非堆的情況。

至於Oracle文檔上說的,Jvm可能會向操作系統釋放內存,經過測試沒有發現釋放的情況。不過就算有主動釋放的情況,也不太需要我們程序關心了。

RES(Resident Set Size)是常駐內存的意思,進程實際使用的物理內存

私信回覆“資料”獲取面試寶典《Java核心知識點整理.pdf》“,覆蓋了JVM、鎖、高併發、反射、Spring原理

 

 

jmap命令

首先看一下一個java進程的jmap輸出:

 代碼如下  

[lex@chou ~]$ jmap -heap 837
Attaching to process ID 837, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 20.10-b01

using thread-local object allocation.
Parallel GC with 2 thread(s)

Heap Configuration:
   MinHeapFreeRatio = 40
   MaxHeapFreeRatio = 70
   MaxHeapSize      = 4294967296 (4096.0MB)
   NewSize          = 1310720 (1.25MB)
   MaxNewSize       = 17592186044415 MB
   OldSize          = 5439488 (5.1875MB)
   NewRatio         = 2
   SurvivorRatio    = 8
   PermSize         = 21757952 (20.75MB)
   MaxPermSize      = 85983232 (82.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 41025536 (39.125MB)
   used     = 18413552 (17.560531616210938MB)
   free     = 22611984 (21.564468383789062MB)
   44.883147900858624% used
From Space:
   capacity = 4325376 (4.125MB)
   used     = 3702784 (3.53125MB)
   free     = 622592 (0.59375MB)
   85.60606060606061% used
To Space:
   capacity = 4521984 (4.3125MB)
   used     = 0 (0.0MB)
   free     = 4521984 (4.3125MB)
   0.0% used
PS Old Generation
   capacity = 539820032 (514.8125MB)
   used     = 108786168 (103.74657440185547MB)
   free     = 431033864 (411.06592559814453MB)
   20.152302906758376% used
PS Perm Generation
   capacity = 85983232 (82.0MB)
   used     = 60770232 (57.95500946044922MB)
   free     = 25213000 (24.04499053955078MB)
   70.67684080542588% used

ps命令  

然後再用ps看看:

 代碼如下  
[lex@chou ~]$ ps -p 837 -o vsz,rss
   VSZ   RSS
7794992 3047320

  關於這裏的幾個generation網上資料一大把就不細說了,這裏算一下求和可以得知前者總共給Java環境分配了644M的內存,而ps輸出的VSZ和RSS分別是7.4G和2.9G,這到底是怎麼回事呢?

  前面jmap輸出的內容裏,MaxHeapSize 是在命令行上配的,-Xmx4096m,這個java程序可以用到的最大堆內存。

  VSZ是指已分配的線性空間大小,這個大小通常並不等於程序實際用到的內存大小,產生這個的可能性很多,比如內存映射,共享的動態庫,或者向系統申請了更多的堆,都會擴展線性空間大小

pmap命令

要查看一個進程有哪些內存映射,可以使用 pmap 命令來查看:

 代碼如下  
[lex@chou ~]$ pmap -x 837
837:   java
Address           Kbytes     RSS   Dirty Mode   Mapping
0000000040000000      36       4       0 r-x--  java
0000000040108000       8       8       8 rwx--  java
00000000418c9000   13676   13676   13676 rwx--    [ anon ]
00000006fae00000   83968   83968   83968 rwx--    [ anon ]
0000000700000000  527168  451636  451636 rwx--    [ anon ]
00000007202d0000  127040       0       0 -----    [ anon ]
...
...
00007f55ee124000       4       4       0 r-xs-  az.png
00007fff017ff000       4       4       0 r-x--    [ anon ]
ffffffffff600000       4       0       0 r-x--    [ anon ]
----------------  ------  ------  ------
total kB         7796020 3037264 3023928

  

       Address: 內存分配地址

       Kbytes:   實際分配的內存大小

       RSS:       程序實際佔用的內存大小

       Mapping: 分配該內存的模塊的名稱

 

      這裏可以看到很多anon,這些表示這塊內存是由mmap分配的。

  RSZ是Resident Set Size,常駐內存大小,即進程實際佔用的物理內存大小, 在現在這個例子當中,RSZ和實際堆內存佔用差了2.3G,這2.3G的內存組成分別爲:

  JVM本身需要的內存,包括其加載的第三方庫以及這些庫分配的內存

  NIO的DirectBuffer是分配的native memory

  內存映射文件,包括JVM加載的一些JAR和第三方庫,以及程序內部用到的。上面 pmap 輸出的內容裏,有一些靜態文件所佔用的大小不在Java的heap裏,因此作爲一個Web服務器,趕緊把靜態文件從這個Web服務器中人移開吧,放到nginx或者CDN裏去吧。

  JIT, JVM會將Class編譯成native代碼,這些內存也不會少,如果使用了Spring的AOP,CGLIB會生成更多的類,JIT的內存開銷也會隨之變大,而且Class本身JVM的GC會將其放到Perm Generation裏去,很難被回收掉,面對這種情況,應該讓JVM使用ConcurrentMarkSweep GC,並啓用這個GC的相關參數允許將不使用的class從Perm Generation中移除, 參數配置: -XX:+UseConcMarkSweepGC -X:+CMSPermGenSweepingEnabled -X:+CMSClassUnloadingEnabled,如果不需要移除而Perm Generation空間不夠,可以加大一點: -X:PermSize=256M -X:MaxPermSize=512M

  JNI,一些JNI接口調用的native庫也會分配一些內存,如果遇到JNI庫的內存泄露,可以使用valgrind等內存泄露工具來檢測

  線程棧,每個線程都會有自己的棧空間,如果線程一多,這個的開銷就很明顯了

  jmap/jstack 採樣,頻繁的採樣也會增加內存佔用,如果你有服務器健康監控,記得這個頻率別太高,否則健康監控變成致病監控了。

jstat命令

  關於JVM的幾個GC堆和GC的情況,可以用jstat來監控,例如監控進程837每隔1000毫秒刷新一次,輸出20次:

 代碼如下  
[lex@chou ~]$ jstat -gcutil 837 1000 20
  S0     S1     E      O      P     YGC     YGCT    FGC    FGCT     GCT   
  0.00  80.43  24.62  87.44  98.29   7101  119.652    40   19.719  139.371
  0.00  80.43  33.14  87.44  98.29   7101  119.652    40   19.719  139.371

  幾個字段分別含義如下:

  S0

  年輕代中第一個survivor(倖存區)已使用的佔當前容量百分比

  S1

  年輕代中第二個survivor(倖存區)已使用的佔當前容量百分比

  E

  年輕代中Eden(伊甸園)已使用的佔當前容量百分比

  O

  old代已使用的佔當前容量百分比

  P

  perm代已使用的佔當前容量百分比

  YGC

  從應用程序啓動到採樣時年輕代中gc次數

  YGCT

  從應用程序啓動到採樣時年輕代中gc所用時間(s)

  FGC

  從應用程序啓動到採樣時old代(全gc)gc次數

  FGCT

  從應用程序啓動到採樣時old代(全gc)gc所用時間(s)

  GCT

  從應用程序啓動到採樣時gc用的總時間(s)

結論

  因此如果正常情況下jmap輸出的內存佔用遠小於 RSZ,可以不用太擔心,除非發生一些嚴重錯誤,比如PermGen空間滿了導致OutOfMemoryError發生,或者RSZ太高導致引起系統公憤被OOM Killer給幹掉,就得注意了,該加內存加內存,沒錢買內存加交換空間,或者按上面列的組成部分逐一排除。

  這幾個內存指標之間的關係是:VSZ >> RSZ >> Java程序實際使用的堆大小

 

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