compose - java 8 container memory




Java使用比堆大小更多的內存(或正確的Docker內存限制大小) (4)

TL; DR

內存的詳細用法由本機內存跟踪(NMT)詳細信息(主要是代碼元數據和垃圾收集器)提供。 除此之外,Java編譯器和優化器C1 / C2使用摘要中未報告的內存。

使用JVM標誌可以減少內存佔用(但存在影響)。

必須通過測試應用程序的預期負載來完成Docker容器大小調整。

每個組件的詳細信息

可以在容器內禁用 共享類空間, 因為這些類不會被另一個JVM進程共享。 可以使用以下標誌。 它將刪除共享類空間(17MB)。

-Xshare:off

垃圾收集器 序列具有最小的內存佔用,代價是在垃圾收集處理期間更長的暫停時間(參見 一張圖中的GC之間的AlekseyShipilëv比較 )。 可以使用以下標誌啟用它。 它可以節省使用的GC空間(48MB)。

-XX:+UseSerialGC

可以使用以下標誌禁用 C2編譯器 ,以減少用於決定是否優化方法的分析數據。

-XX:+TieredCompilation -XX:TieredStopAtLevel=1

代碼空間減少了20MB。 而且,JVM外部的內存減少了80MB(NMT空間和RSS空間之間的差異)。 優化編譯器C2需要100MB。

可以使用以下標誌禁用 C1和C2編譯器

-Xint

JVM外部的內存現在低於總提交空間。 代碼空間減少了43MB。 請注意,這會對應用程序的性能產生重大影響。 禁用C1和C2編譯器會減少170 MB的內存使用量。

使用 Graal VM編譯器 (替換C2)可以減少內存佔用。 它增加了20MB的代碼內存空間,並從外部JVM內存減少了60MB。

JVM的Java內存管理 文章提供了不同內存空間的一些相關信息。 Oracle在 Native Memory Tracking文檔中 提供了一些細節。 有關 高級編譯策略 和 禁用C2中的 編譯級別的更多詳細信息 將代碼高速緩存大小減少了5倍 。 有關 為什麼JVM報告的內存比Linux進程駐留集大小更多的 一些細節 ? 當兩個編譯器都被禁用時。

對於我的應用程序,Java進程使用的內存遠遠超過堆大小。

容器正在運行的系統開始出現內存問題,因為容器佔用的內存比堆大小多得多。

堆大小設置為128 MB( -Xmx128m -Xms128m ),而容器最多佔用1GB內存。 在正常情況下,它需要500MB。 如果 mem_limit=mem_limit=400MB 容器具有以下限制(例如 mem_limit=mem_limit=400MB ),則進程將被OS的內存不足殺死所殺死。

你能解釋為什麼Java進程使用比堆更多的內存嗎? 如何正確調整Docker內存限制? 有沒有辦法減少Java進程的堆外內存佔用?

我使用 JVM中的本機內存跟踪 命令收集有關該問題的一些詳細信息。

從主機系統,我獲得容器使用的內存。

$ docker stats --no-stream 9afcb62a26c8
CONTAINER ID        NAME                                                                                        CPU %               MEM USAGE / LIMIT   MEM %               NET I/O             BLOCK I/O           PIDS
9afcb62a26c8        xx-xxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.0acbb46bb6fe3ae1b1c99aff3a6073bb7b7ecf85   0.93%               461MiB / 9.744GiB   4.62%               286MB / 7.92MB      157MB / 2.66GB      57

從容器內部,我獲得進程使用的內存。

$ ps -p 71 -o pcpu,rss,size,vsize
%CPU   RSS  SIZE    VSZ
11.2 486040 580860 3814600
$ jcmd 71 VM.native_memory
71:

Native Memory Tracking:

Total: reserved=1631932KB, committed=367400KB
-                 Java Heap (reserved=131072KB, committed=131072KB)
                            (mmap: reserved=131072KB, committed=131072KB) 

-                     Class (reserved=1120142KB, committed=79830KB)
                            (classes #15267)
                            (  instance classes #14230, array classes #1037)
                            (malloc=1934KB #32977) 
                            (mmap: reserved=1118208KB, committed=77896KB) 
                            (  Metadata:   )
                            (    reserved=69632KB, committed=68272KB)
                            (    used=66725KB)
                            (    free=1547KB)
                            (    waste=0KB =0.00%)
                            (  Class space:)
                            (    reserved=1048576KB, committed=9624KB)
                            (    used=8939KB)
                            (    free=685KB)
                            (    waste=0KB =0.00%)

-                    Thread (reserved=24786KB, committed=5294KB)
                            (thread #56)
                            (stack: reserved=24500KB, committed=5008KB)
                            (malloc=198KB #293) 
                            (arena=88KB #110)

-                      Code (reserved=250635KB, committed=45907KB)
                            (malloc=2947KB #13459) 
                            (mmap: reserved=247688KB, committed=42960KB) 

-                        GC (reserved=48091KB, committed=48091KB)
                            (malloc=10439KB #18634) 
                            (mmap: reserved=37652KB, committed=37652KB) 

-                  Compiler (reserved=358KB, committed=358KB)
                            (malloc=249KB #1450) 
                            (arena=109KB #5)

-                  Internal (reserved=1165KB, committed=1165KB)
                            (malloc=1125KB #3363) 
                            (mmap: reserved=40KB, committed=40KB) 

-                     Other (reserved=16696KB, committed=16696KB)
                            (malloc=16696KB #35) 

-                    Symbol (reserved=15277KB, committed=15277KB)
                            (malloc=13543KB #180850) 
                            (arena=1734KB #1)

-    Native Memory Tracking (reserved=4436KB, committed=4436KB)
                            (malloc=378KB #5359) 
                            (tracking overhead=4058KB)

-        Shared class space (reserved=17144KB, committed=17144KB)
                            (mmap: reserved=17144KB, committed=17144KB) 

-               Arena Chunk (reserved=1850KB, committed=1850KB)
                            (malloc=1850KB) 

-                   Logging (reserved=4KB, committed=4KB)
                            (malloc=4KB #179) 

-                 Arguments (reserved=19KB, committed=19KB)
                            (malloc=19KB #512) 

-                    Module (reserved=258KB, committed=258KB)
                            (malloc=258KB #2356) 

$ cat /proc/71/smaps | grep Rss | cut -d: -f2 | tr -d " " | cut -f1 -dk | sort -n | awk '{ sum += $1 } END { print sum }'
491080

該應用程序是一個Web服務器,使用Jetty / Jersey / CDI捆綁在一個36 MB的脂肪中。

使用以下版本的OS和Java(在容器內)。 Docker鏡像基於 openjdk:11-jre-slim

$ java -version
openjdk version "11" 2018-09-25
OpenJDK Runtime Environment (build 11+28-Debian-1)
OpenJDK 64-Bit Server VM (build 11+28-Debian-1, mixed mode, sharing)
$ uname -a
Linux service1 4.9.125-linuxkit #1 SMP Fri Sep 7 08:20:28 UTC 2018 x86_64 GNU/Linux

https://gist.github.com/prasanthj/48e7063cac88eb396bc9961fb3149b58


Java進程使用的虛擬內存遠遠超出了Java堆。 您知道,JVM包含許多子系統:垃圾收集器,類加載,JIT編譯器等,所有這些子系統都需要一定量的RAM才能運行。

JVM不是RAM的唯一消費者。 本機庫(包括標準Java類庫)也可以分配本機內存。 而本機內存跟踪甚至無法看到這一點。 Java應用程序本身也可以通過直接ByteBuffers使用堆外內存。

那麼什麼需要Java進程中的內存?

JVM部件(主要通過本機內存跟踪顯示)

  1. Java堆

    最明顯的部分。 這是Java對象所在的位置。 堆佔用 -Xmx 的內存量。

  2. 垃圾收集器

    GC結構和算法需要額外的內存用於堆管理。 這些結構是Mark Bitmap,Mark Stack(用於遍歷對像圖),Remembered Sets(用於記錄區域間引用)等。 其中一些是直接可調的,例如 -XX:MarkStackSizeMax ,其他依賴於堆佈局,例如較大的是G1區域( -XX:G1HeapRegionSize ),較小的是記憶集。

    GC內存開銷因GC算法而異。 -XX:+UseSerialGC-XX:+UseShenandoahGC 具有最小的開銷。 G1或CMS可以輕鬆使用總堆大小的10%左右。

  3. 代碼緩存

    包含動態生成的代碼:JIT編譯的方法,解釋器和運行時存根。 其大小受 -XX:ReservedCodeCacheSize (默認為240M)限制。 關閉 -XX:-TieredCompilation 以減少編譯代碼的數量,從而減少代碼緩存的使用。

  4. 編譯器

    JIT編譯器本身也需要內存來完成它的工作。 這可以通過關閉分層編譯或減少編譯器線程數來再次減少: -XX:CICompilerCount

  5. 類加載

    類元數據(方法字節碼,符號,常量池,註釋等)存儲在稱為Metaspace的堆外區域中。 加載的類越多 - 使用的元空間就越多。 總使用量可以受 -XX:MaxMetaspaceSize (默認為無限制)和 -XX:CompressedClassSpaceSize (默認為1G)限制。

  6. 符號表

    JVM的兩個主要哈希表:Symbol表包含名稱,簽名,標識符等,String表包含對實習字符串的引用。 如果本機內存跟踪指示String表佔用大量內存,則可能意味著應用程序過度調用 String.intern

  7. 主題

    線程堆棧也負責佔用RAM。 堆棧大小由 -Xss 控制。 每個線程的默認值是1M,但幸運的是事情並沒有那麼糟糕。 操作系統懶惰地分配內存頁面,即在第一次使用時,因此實際內存使用量將低得多(通常每個線程堆棧80-200 KB)。 我編寫了一個 script 來估計RSS有多少屬於Java線程堆棧。

    還有其他JVM部件可以分配本機內存,但它們通常不會在總內存消耗中發揮重要作用。

直接緩衝

應用程序可以通過調用 ByteBuffer.allocateDirect 顯式請求堆外內存。 默認的堆外限制等於 -Xmx ,但可以使用 -XX:MaxDirectMemorySize 覆蓋它。 直接字節緩衝區包含在NMT輸出的 Other 部分(或JDK 11之前的 Internal )中。

通過JMX可以看到使用的直接內存量,例如在JConsole或Java Mission Control中:

除了直接的ByteBuffers,還可以有 MappedByteBuffers - 映射到進程虛擬內存的文件。 NMT不跟踪它們,但MappedByteBuffers也可以佔用物理內存。 而且沒有一種簡單的方法來限制它們可以承受多少。 您可以通過查看進程內存映射來查看實際用法: pmap -x <pid>

Address           Kbytes    RSS    Dirty Mode  Mapping
...
00007f2b3e557000   39592   32956       0 r--s- some-file-17405-Index.db
00007f2b40c01000   39600   33092       0 r--s- some-file-17404-Index.db
                           ^^^^^               ^^^^^^^^^^^^^^^^^^^^^^^^

本地圖書館

System.loadLibrary 加載的JNI代碼可以根據需要分配盡可能多的堆外內存,而無需JVM端的控制。 這也涉及標準的Java類庫。 特別是,未封閉的Java資源可能成為本機內存洩漏的來源。 典型的例子是 ZipInputStreamDirectoryStream

JVMTI代理,特別是 jdwp 調試代理 - 也可能導致過多的內存消耗。

此答案 描述瞭如何使用 async-profiler 配置本機內存分配。

分配器問題

進程通常直接從OS(通過 mmap 系統調用)或使用 malloc 標準libc分配器請求本機內存。 反過來, malloc 使用 mmap 從OS請求大塊內存,然後根據自己的分配算法管理這些塊。 問題是 - 該算法可能導致碎片和 過多的虛擬內存使用

jemalloc ,一個替代分配器,通常看起來比常規的libc malloc 更聰明,因此切換到 jemalloc 可能會導致更小的免費佔用空間。

結論

沒有保證估計Java進程的完整內存使用量的方法,因為有太多因素需要考慮。

Total memory = Heap + Code Cache + Metaspace + Symbol tables +
               Other JVM structures + Thread stacks +
               Direct buffers + Mapped files +
               Native Libraries + Malloc overhead + ...

可以通過JVM標誌縮小或限制某些內存區域(如代碼緩存),但許多其他內存區域完全不受JVM控制。

設置Docker限制的一種可能方法是在進程的“正常”狀態下觀察實際內存使用情況。 有研究Java內存消耗問題的工具和技術: Native Memory Tracking pmap jemalloc async-profiler



https://developers.redhat.com/blog/2017/04/04/openjdk-and-containers/

為什麼當我指定-Xmx = 1g時,我的JVM佔用的內存超過1GB的內存?

指定-Xmx = 1g告訴JVM分配1gb堆。 它沒有告訴JVM將其整個內存使用量限制為1GB。 有卡表,代碼緩存和各種其他的堆外數據結構。 用於指定總內存使用量的參數是-XX:MaxRAM。 請注意,使用-XX:MaxRam = 500m時,您的堆將大約為250mb。

Java看到主機內存大小,並且不知道任何容器內存限制。 它不會產生內存壓力,因此GC也不需要釋放已用內存。 我希望 XX:MaxRAM 可以幫助您減少內存佔用。 最後,您可以調整GC配置( -XX:MinHeapFreeRatio-XX:MaxHeapFreeRatio ,...)

有許多類型的內存指標。 Docker似乎報告了RSS內存大小,這可能與 jcmd 報告的“已提交”內存不同(舊版本的Docker報告RSS +緩存作為內存使用情況)。 良好的討論和鏈接: 在Docker容器中運行的JVM的駐留集大小(RSS)和Java總提交內存(NMT)之間的差異

(RSS)內存也可以被容器中的其他一些實用程序吃掉 - shell,進程管理器......我們不知道容器中還有什麼運行,以及如何在容器中啟動進程。





jvm