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

Posted

技术标签:

【中文标题】Java 使用的内存比堆大小(或正确大小的 Docker 内存限制)多得多【英文标题】:Java using much more memory than heap size (or size correctly Docker memory limit) 【发布时间】:2019-04-26 07:50:33 【问题描述】:

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

容器运行所在的系统开始出现内存问题,因为容器占用的内存远大于堆大小。

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

您能解释一下为什么 Java 进程使用的内存比堆多得多吗?如何正确调整 Docker 内存限制?有没有办法减少 Java 进程的堆外内存占用?


我使用来自Native memory tracking in 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 的 Web 服务器,捆绑在 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

【问题讨论】:

堆是分配对象的地方,但是 JVM 有许多其他内存区域,包括共享库、直接内存缓冲区、线程堆栈、GUI 组件、元空间。您需要查看 JVM 可以有多大,并将限制设置得足够高,以至于您宁愿进程死掉也不愿再使用。 看起来 GC 正在使用大量内存。您可以尝试改用 CMS 收集器。看起来 ~125 MB 用于元空间 + 代码,但是如果不缩小代码库,您不太可能将其缩小。承诺的空间接近你的极限,所以它被杀死也就不足为奇了。 在哪里/如何设置 -Xms 和 -Xmx 配置? 注:Java 8 u212+ has now (Apr. 2019) a better Docker support when it comes to memory 您的程序是否执行许多文件操作(例如创建千兆字节大小的文件)?如果是这样,您应该知道cgroups 将磁盘缓存添加到已使用的内存中——即使它由内核处理并且对用户程序是不可见的。 (请注意,命令 psdocker stats 不算磁盘缓存。) 【参考方案1】:

Java 需要大量内存。 JVM 本身需要大量内存才能运行。堆是虚拟机内部可用的内存,可供您的应用程序使用。因为 JVM 是一个包含所有好东西的大包,所以它需要大量内存才能加载。

从 java 9 开始,您有一个名为 project Jigsaw 的东西,它可能会减少您启动 java 应用程序时使用的内存(以及启动时间)。项目拼图和一个新的模块系统不一定是为了减少必要的内存而创建的,但如果它很重要,你可以试一试。

你可以看看这个例子:https://steveperkins.com/using-java-9-modularization-to-ship-zero-dependency-native-apps/。通过使用模块系统,它产生了 21MB 的 CLI 应用程序(嵌入了 JRE)。 JRE 占用超过 200mb。当应用程序启动时,这应该会转化为更少的分配内存(将不再加载许多未使用的 JRE 类)。

这是另一个不错的教程:https://www.baeldung.com/project-jigsaw-java-modularity

如果您不想花时间在这上面,您可以简单地分配更多内存。有时它是最好的。

【讨论】:

使用jlink 非常有限制,因为它需要将应用程序模块化。不支持自动模块,所以没有简单的方法去那里。【参考方案2】:

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

为什么当我指定 -Xmx=1g 时,我的 JVM 占用的内存超过 1gb 内存?

指定 -Xmx=1g 是告诉 JVM 分配一个 1gb 的堆。它不是 告诉 JVM 将其整个内存使用限制为 1gb。有 卡表、代码缓存和各种其他堆外数据 结构。您用来指定总内存使用量的参数是 -XX:最大内存。请注意,使用 -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 committed memory (NMT) for a JVM running in Docker container

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

【讨论】:

-XX:MaxRam 确实更好。我认为它仍然使用超过定义的最大值,但它更好,谢谢! 也许您确实需要更多内存来存储这个 Java 实例。有15267个类,56个线程。 提供的 RSS 来自容器内部,仅用于 Java 进程 ps -p 71 -o pcpu,rss,size,vsize,Java 进程的 pid 为 71。实际上 -XX:MaxRam 没有帮助,但您提供的链接有助于串行 GC。 【参考方案3】:

如何正确调整 Docker 内存限制? 通过监视一段时间来检查应用程序。要限制容器的内存,请尝试对 docker run 命令使用 -m、--memory bytes 选项 - 或者如果您正在运行它,则使用其他等效项 喜欢

docker run -d --name my-container --memory 500m <iamge-name>

无法回答其他问题。

【讨论】:

【参考方案4】:

TL;DR

内存的详细使用情况由 Native Memory Tracking (NMT) 详细信息(主要是代码元数据和垃圾收集器)提供。除此之外,Java 编译器和优化器 C1/C2 会消耗摘要中未报告的内存。

使用 JVM 标志可以减少内存占用(但有影响)。

Docker 容器的大小必须通过测试应用程序的预期负载来完成。


每个组件的详细信息

共享类空间可以在容器内禁用,因为这些类不会被另一个 JVM 进程共享。可以使用以下标志。它将删除共享类空间 (17MB)。

-Xshare:off

垃圾收集器序列号具有最小的内存占用,但代价是垃圾收集处理期间的暂停时间较长(请参阅Aleksey Shipilëv comparison between GC in one picture)。可以使用以下标志启用它。它最多可以节省 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 compiler(替换C2)会导致内存占用更小。它增加了 20MB 的代码内存空间,减少了 60MB 的外部 JVM 内存。

文章Java Memory Management for JVM 提供了不同内存空间的一些相关信息。 Oracle 在Native Memory Tracking documentation 中提供了一些详细信息。 advanced compilation policy 和 disable C2 reduce code cache size by a factor 5 中有关编译级别的更多详细信息。禁用两个编译器时有关Why does a JVM report more committed memory than the Linux process resident set size? 的一些详细信息。

【讨论】:

【参考方案5】:

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

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

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

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

    Java 堆

最明显的部分。这是 Java 对象所在的地方。堆最多占用-Xmx 的内存量。

    垃圾收集器

GC 结构和算法需要额外的内存来进行堆管理。这些结构是 Mark Bitmap、Mark Stack(用于遍历对象图)、Remembered Sets(用于记录区域间引用)等。其中一些是直接可调的,例如-XX:MarkStackSizeMax,其他依赖于堆布局,例如G1 区域 (-XX:G1HeapRegionSize) 越大,记忆集越小。

GC 内存开销因 GC 算法而异。 -XX:+UseSerialGC-XX:+UseShenandoahGC 的开销最小。 G1 或 CMS 可能很容易使用大约 10% 的总堆大小。

    代码缓存

包含动态生成的代码:JIT 编译的方法、解释器和运行时存根。它的大小受-XX:ReservedCodeCacheSize(默认为240M)的限制。关闭-XX:-TieredCompilation 以减少编译代码的数量,从而减少代码缓存的使用。

    编译器

JIT 编译器本身也需要内存来完成其工作。这可以通过关闭分层编译或减少编译器线程的数量再次减少:-XX:CICompilerCount

    类加载

类元数据(方法字节码、符号、常量池、注释等)存储在称为 Metaspace 的堆外区域中。加载的类越多 - 使用的元空间就越多。总使用量可以通过-XX:MaxMetaspaceSize(默认无限制)和-XX:CompressedClassSpaceSize(默认1G)来限制。

    符号表

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

    线程

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

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

直接缓冲区

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

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

除了直接的 ByteBuffers 之外,还可以有 MappedByteBuffers - 映射到进程虚拟内存的文件。 NMT 不会跟踪它们,但是,MappedByteBuffers 也可以占用物理内存。并且没有简单的方法来限制他们可以服用多少。您可以通过查看进程内存映射来查看实际使用情况:pmap -x &lt;pid&gt;

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 调试代理 - 也可能导致过多的内存消耗。

This answer 描述了如何使用async-profiler 分析本机内存分配。

分配器问题

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

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。

更新

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

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

【讨论】:

自 jdk7 以来,堆中的字符串不是被留存的吗? (bugs.java.com/bugdatabase/view_bug.do?bug_id=6962931) - 也许我错了。 @j-keck 字符串对象在堆中,但哈希表(桶和带有引用和哈希码的条目)在堆外内存中。我把这句话改写得更准确。感谢您指出。 补充一点,即使你使用非直接 ByteBuffers,JVM 也会在本机内存中分配临时直接缓冲区,而不会施加内存限制。参照。 evanjones.ca/java-bytebuffer-leak.html

以上是关于Java 使用的内存比堆大小(或正确大小的 Docker 内存限制)多得多的主要内容,如果未能解决你的问题,请参考以下文章

java-oop 堆栈

java中堆和栈的区别

如何动态监控 Java 堆大小?

关于java虚拟机

java堆栈堆栈的区别

java 堆栈 理解