性能優化 - Docker 容器中的 Java 內存使用分析


參考文檔:https://cloud.tencent.com/developer/article/1887538

 

Docker 下運行的 Java 應用程序中的內存消耗時遇到了一個有趣的問題。該XMX參數被設置為256M,但Docker監控工具顯示幾乎兩倍多使用的內存

下面我們將嘗試了解這種奇怪行為的原因,並找出應用程序實際上消耗了多少內存。

Docker和內存


首先,讓我們看一下我用來啟動應用程序的 docker 容器參數:

docker run -d --restart=always \ -p {{service_port}}:8080 -p {{jmx_port}}:{{jmx_port}} \ -e JAVA_OPTS=' -Xmx{{java_memory_limit}} -XX:+UseConcMarkSweepGC -XX:NativeMemoryTracking=summary -Djava.rmi.server.hostname={{ansible_default_ipv4.address}} -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port={{jmx_port}} -Dcom.sun.management.jmxremote.rmi.port={{jmx_port}} -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false ' \ -m={{container_memory_limit}} --memory-swap={{container_memory_limit}} \ --name {{service_name}} \ {{private_registry}}/{{image_name}}:{{image_version}} 

其中java_memory_limit= 256m。當您開始嘗試解釋docker stats my-app命令的結果時,問題就開始了:

CONTAINER    CPU % MEM USAGE/LIMIT MEM % NET I/O my-app 1.67% 504 MB/536.9 MB 93.85% 555.4 kB/159.4 kB 

所以,我們只需運行以下命令:

[mkrestyaninov@xxx ~]$ docker exec my-app ps -o rss,vsz,sz 1 RSS VSZ SZ 375824 4924048 1231012 

嗯……好奇怪!

PS 說我們的應用程序只消耗375824K / 1024 = 367M。似乎我們的問題多於答案

為什么 docker statsinfo 與ps數據不同?

第一個問題的答案非常簡單 - Docker 有一個錯誤(或一個功能 - 取決於您的心情):它將文件緩存包含在總內存使用信息中。所以,我們可以避免這個指標並使用ps關於 RSS 的信息,並認為我們的應用程序使用367M,而不是 504M (因為文件緩存可以在內存不足的情況下輕松刷新)。

好吧 - 但為什么 RSS 比 Xmx 高?這是一個非常有趣的問題!讓我們試着找出來。

有JMX


分析 Java 進程最簡單的方法是 JMX(這就是我們在容器中啟用它的原因)。

理論上,在java應用程序的情況下

 RSS = Heap size + MetaSpace + OffHeap size 

其中 OffHeap 由線程堆棧、直接緩沖區、映射文件(庫和 jar)和 JVM 代碼本身組成;

根據jvisualvm,承諾的堆大小為136M(而只有67M被“使用”)

MetaSapce 大小為68M(使用了 67M)

換句話說,我們必須解釋367M - (136M + 67M) = 164M的 OffHeap 內存。

我的應用程序(平均)有30 個實時線程:

這些線程中的每一個都消耗 1M:

[ root@fac6d0dfbbb4:/data ]$ java -XX:+PrintFlagsFinal -version |grep ThreadStackSize intx CompilerThreadStackSize = 0 intx ThreadStackSize = 1024 intx VMThreadStackSize = 1024 

所以,這里我們可以再增加30M

應用程序使用 DirectBuffer 的唯一地方是 NIO。就我從 JMX 中看到的而言,它不會消耗大量資源 - 只有98K

但是根據 pmap

[mkrestyaninov@xxx ~]$ docker exec my-app pmap -x 1 | grep ".so.*" | awk '{sum+=$3} END {print sum}' 12664 

[mkrestyaninov@xxx ~]$ docker exec my-app pmap -x 1 | grep ".jar" | awk '{sum+=$3} END {print sum}' 8428 

我們這里只有20M。

在這里,您應該記住,當您使用 Docker(或任何其他虛擬化)時,“共享”庫(libc.so、libjvm.so 等)並不是那么共享的——每個容器都有自己的這些庫的副本。

因此,我們仍然需要解釋164M - (30M + 20M) = 114M

 

本機內存跟蹤


上面的所有操作都暗示我們 JMX 不是我們想要的工具 :)

希望從 JDK 1.8.40 開始,我們有了Native Memory Tracker!

我已經-XX:NativeMemoryTracking=summary向 JVM添加了屬性,因此我們可以從命令行調用它:

[mkrestyaninov@xxx ~]$ docker exec my-app jcmd 1 VM.native_memory summary Native Memory Tracking: Total: reserved=1754380KB, committed=371564KB - Java Heap (reserved=262144KB, committed=140736KB) (mmap: reserved=262144KB, committed=140736KB) - Class (reserved=1113555KB, committed=73811KB) (classes #13295) (malloc=1491KB #17749) (mmap: reserved=1112064KB, committed=72320KB) - Thread (reserved=50587KB, committed=50587KB) (thread #50) (stack: reserved=50372KB, committed=50372KB) (malloc=158KB #256) (arena=57KB #98) - Code (reserved=255257KB, committed=34065KB) (malloc=5657KB #8882) (mmap: reserved=249600KB, committed=28408KB) - GC (reserved=13777KB, committed=13305KB) (malloc=12917KB #338) (mmap: reserved=860KB, committed=388KB) - Compiler (reserved=178KB, committed=178KB) (malloc=47KB #233) (arena=131KB #3) - Internal (reserved=2503KB, committed=2503KB) (malloc=2471KB #16052) (mmap: reserved=32KB, committed=32KB) - Symbol (reserved=17801KB, committed=17801KB) (malloc=13957KB #137625) (arena=3844KB #1) - Native Memory Tracking (reserved=2846KB, committed=2846KB) (malloc=11KB #126) (tracking overhead=2836KB) - Arena Chunk (reserved=187KB, committed=187KB) (malloc=187KB) - Unknown (reserved=35544KB, committed=35544KB) (mmap: reserved=35544KB, committed=35544KB) 

有關 JVM 進程內存的所有信息都在您的屏幕上!如果這不明顯,您可以在此處找到有關每個點含義的信息。不要擔心“未知”部分 - 似乎 NMT 是一個不成熟的工具,無法處理 CMS GC(當您使用另一個 GC 時,此部分會消失)。

請記住,NMT 顯示“已提交”的內存,而不是“常駐”(您通過ps命令獲得)。換句話說,一個內存頁可以在不考慮為常駐者的情況下被提交(直到它被直接訪問)。這意味着非堆區域(堆始終預初始化)的 NMT 結果可能大於 RSS 值

總結


結果,盡管我們將 jvm 堆限制設置為256m,但我們的應用程序消耗了367M。“其他” 164M主要用於存儲類元數據、編譯代碼、線程和 GC 數據。

前三點通常是應用程序的常量,因此唯一隨堆大小增加的就是 GC 數據。這種依賴性是線性的,但“k”系數 ( y = kx + b) 遠小於 1。例如,在我們的應用程序中,對於 380M的已提交堆,GC 使用78M(在當前示例中,我們有140M 對 48M)。

我能說些什么作為結論?嗯……永遠不要把“java”和“micro”放在同一個句子中:) 我在開玩笑——請記住,在 java、linux 和 docker 的情況下處理內存比起初看起來要棘手一些。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM