Java 使用的内存比堆大小(或正确大小的 Docker 内存限制)多得多

新手上路,请多包涵

对于我的应用程序,Java 进程使用的内存远大于堆大小。

运行容器的系统开始出现内存问题,因为容器占用的内存比堆大小多得多。

堆大小设置为 128 MB ( -Xmx128m -Xms128m ),而容器最多占用 1GB 内存。正常情况下需要500MB。如果 docker 容器的限制低于(例如 mem_limit=mem_limit=400MB ),则该进程会被操作系统的内存不足杀手杀死。

你能解释一下为什么 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

该应用程序是一个使用 Jetty/Jersey/CDI 捆绑在 36 MB 内的 Web 服务器。

使用以下版本的 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

原文由 Nicolas Henneaux 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 1.6k
2 个回答

Java 进程使用的虚拟内存远远超出了 Java 堆。要知道,JVM 包含许多子系统:垃圾收集器、类加载、JIT 编译器等,所有这些子系统都需要一定数量的 RAM 才能运行。

JVM 不是 RAM 的唯一消费者。本机库(包括标准 Java 类库)也可以分配本机内存。而这对 Native Memory Tracking 来说甚至是不可见的。 Java 应用程序本身也可以通过直接 ByteBuffers 使用堆外内存。

那么在 Java 进程中是什么占用了内存呢?

JVM 部分(主要由 Native Memory Tracking 显示)

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 的两个主要哈希表:符号表包含名称、签名、标识符等,字符串表包含对内部字符串的引用。如果 Native Memory Tracking 指示 String 表使用了大量内存,则可能意味着应用程序过度调用 String.intern

7. 线程

线程堆栈也负责占用 RAM。堆栈大小由 -Xss 控制。默认值是每个线程 1M,但幸运的是事情并没有那么糟糕。操作系统会延迟分配内存页面,即在第一次使用时,因此实际内存使用量会低得多(通常每个线程堆栈 80-200 KB)。我写了一个 脚本 来估计有多少 RSS 属于 Java 线程堆栈。

还有其他分配本机内存的 JVM 部分,但它们通常不会在总内存消耗中发挥重要作用。

直接缓冲区

应用程序可以通过调用 ByteBuffer.allocateDirect 显式请求堆外内存。默认的堆外限制等于 -Xmx ,但可以用 -XX:MaxDirectMemorySize 覆盖。直接字节缓冲区包含在 Other NMT 输出部分(或 Internal 在 JDK 11 之前)中。

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

缓冲池 MBean

除了直接的 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 分析本机内存分配。

分配器问题

进程通常直接从操作系统(通过 mmap 系统调用)或通过使用 malloc - 标准 libc 分配器请求本机内存。反过来, malloc 使用 mmap 从操作系统请求大块内存,然后根据自己的分配算法管理这些块。问题是——这种算法会导致碎片和 过多的虚拟内存使用

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 Trackingpmapjemallocasync-profiler

更新

这是我的演示文稿 Memory Footprint of a Java Process 的记录。

在本视频中,我将讨论在 Java 进程中可能会消耗内存的情况、如何监视和限制某些内存区域的大小,以及如何分析 Java 应用程序中的本机内存泄漏。

原文由 apangin 发布,翻译遵循 CC BY-SA 4.0 许可协议

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+缓存作为内存使用情况)。很好的讨论和链接: Difference between Resident Set Size (RSS) and Java total commited memory (NMT) for a JVM running in Docker container

(RSS) 内存也可以被容器中的其他一些实用程序占用——shell、进程管理器……我们不知道容器中还在运行什么以及如何在容器中启动进程。

原文由 Jan Garaj 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题