前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >性能优化 - Docker 容器中的 Java 内存使用分析

性能优化 - Docker 容器中的 Java 内存使用分析

作者头像
码农架构
发布2021-10-12 10:58:25
3.8K0
发布2021-10-12 10:58:25
举报
文章被收录于专栏:码农架构码农架构

Docker 下运行的 Java 应用程序中的内存消耗时遇到了一个有趣的问题。该XMX参数被设置为256M,但Docker监控工具显示几乎两倍多使用的内存

下面我们将尝试了解这种奇怪行为的原因,并找出应用程序实际上消耗了多少内存。

Docker和内存


首先,让我们看一下我用来启动应用程序的 docker 容器参数:

代码语言:javascript
复制
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命令的结果时,问题就开始了:

代码语言:javascript
复制
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

所以,我们只需运行以下命令:

代码语言:javascript
复制
[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应用程序的情况下

代码语言:javascript
复制
 RSS = Heap size + MetaSpace + OffHeap size

其中 OffHeap 由线程堆栈、直接缓冲区、映射文件(库和 jar)和 JVM 代码本身组成;

根据jvisualvm,承诺的堆大小为136M(而只有67M被“使用”)

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

换句话说,我们必须解释367M - (136M + 67M) = 164M的 OffHeap 内存。

我的应用程序(平均)有30 个实时线程:

这些线程中的每一个都消耗 1M:

代码语言:javascript
复制
[ root@fac6d0dfbbb4:/data ]$ java -XX:+PrintFlagsFinal -version |grep ThreadStackSize   
intx CompilerThreadStackSize             = 0
intx ThreadStackSize                     = 1024
intx VMThreadStackSize                   = 1024

所以,这里我们可以再增加30M

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

但是根据 pmap

代码语言:javascript
复制
[mkrestyaninov@xxx ~]$ docker exec my-app pmap -x 1 | grep ".so.*" | awk '{sum+=$3} END {print sum}'

   12664

代码语言:javascript
复制
[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添加了属性,因此我们可以从命令行调用它:

代码语言:javascript
复制
[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 的情况下处理内存比起初看起来要棘手一些。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-09-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码农架构 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Docker和内存
  • 有JMX
  • 本机内存跟踪
  • 总结
相关产品与服务
容器镜像服务
容器镜像服务(Tencent Container Registry,TCR)为您提供安全独享、高性能的容器镜像托管分发服务。您可同时在全球多个地域创建独享实例,以实现容器镜像的就近拉取,降低拉取时间,节约带宽成本。TCR 提供细颗粒度的权限管理及访问控制,保障您的数据安全。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档