Java使用的内存远大于堆大小(或正确大小的Docker内存限制)


118

对于我的应用程序,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

该应用程序是一个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


6
堆是分配对象的地方,但是JVM还有许多其他内存区域,包括共享库,直接内存缓冲区,线程堆栈,GUI组件,元空间。您需要查看JVM的大小,并使限制足够高,以至于您宁愿进程死掉也不愿再使用它。
彼得·劳瑞

2
看来GC正在使用大量内存。您可以尝试使用CMS收集器。看起来〜125 MB用于元空间+代码,但是如果不缩小代码库,就不太可能将其缩小。承诺的空间接近您的极限,因此被杀死并不奇怪。
彼得·劳瑞

在哪里/如何设置-Xms和-Xmx配置?
米克


1
您编程执行许多文件操作(例如,创建千兆字节的文件)吗?如果是这样,您应该知道这cgroups会将磁盘高速缓存添加到已使用的内存中,即使它由内核处理并且对于用户程序是不可见的。(请记住,命令ps并且docker stats不要计算磁盘缓存。)
Lorinczy Zsigmond

Answers:


204

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

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

那么,什么需要占用Java进程中的内存呢?

JVM部分(主要由本机内存跟踪显示)

  1. Java堆

    最明显的部分。这就是Java对象所在的位置。堆占用-Xmx大量内存。

  2. 垃圾收集器

    GC结构和算法需要额外的内存来进行堆管理。这些结构包括标记位图,标记堆栈(用于遍历对象图),记忆集(用于记录区域间引用)等。其中一些是直接可调的,例如-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. 类加载

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

  6. 符号表

    JVM的两个主要哈希表:Symbol表包含名称,签名,标识符等,而String表包含对嵌入字符串的引用。如果本机内存跟踪通过字符串表指示大量内存使用,则可能意味着应用程序过度调用String.intern

  7. 线程数

    线程堆栈还负责占用RAM。堆栈大小由控制-Xss。默认值为每个线程1M,但是幸运的是情况还不错。OS会延迟分配内存页,即在第一次使用时,因此实际内存使用量会低得多(每个线程堆栈通常为80-200 KB)。我编写了一个脚本来估计有多少RSS属于Java线程堆栈。

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

直接缓冲区

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

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

缓冲池MBean

除了直接的ByteBuffer外,还可以将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
                           ^^^^^               ^^^^^^^^^^^^^^^^^^^^^^^^

本机库

加载的JNI代码System.loadLibrary可以分配所需的尽可能多的堆外内存,而无需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内存消耗问题的工具和技术:本机内存跟踪pmapjemallocasync-profiler

更新资料

这是我对Java进程的内存占用的演示的记录。

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


1
自jdk7以来,难道不是在堆中保留字符串吗?(bugs.java.com/bugdatabase/view_bug.do?bug_id=6962931)-也许我错了。
j-keck

5
@ j-keck字符串对象位于堆中,但哈希表(存储桶以及具有引用和哈希码的条目)位于堆外内存中。我将句子改写得更准确。感谢您指出。
apangin '18

除此之外,即使您使用非直接ByteBuffer,JVM也会在本机内存中分配临时直接缓冲区,而不施加任何内存限制。cf. evanjones.ca/java-bytebuffer-leak.html
Cpt。Senkfuss

16

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 + cache为内存使用情况)。良好的讨论和链接:在Docker容器中运行的JVM的居民集大小(RSS)和Java总承诺内存(NMT)之间的差异

(RSS)内存也可以被容器中的某些其他实用程序吃掉-外壳程序,进程管理器等。。。我们不知道容器中还有什么正在运行,以及如何在容器中启动进程。


的确更好-XX:MaxRam。我认为它仍在使用超过定义的最大值,但是更好,谢谢!
Nicolas Henneaux

也许您确实为此Java实例需要更多的内存。有15267个类,56个线程。
Jan Garaj '18

1
这里有更多详细信息,Java参数-Xmx128m -Xms128m -Xss228k -XX:MaxRAM=256m -XX:+UseSerialGC,产生Docker 428.5MiB / 600MiBjcmd 58 VM.native_memory -> Native Memory Tracking: Total: reserved=1571296KB, committed=314316KBJVM占用约300MB,而容器需要430MB。JVM报告和OS报告之间的130MB在哪里?
Nicolas Henneaux

1
添加了有关RSS内存的信息/链接。
Jan Garaj

所提供的RSS仅来自ps -p 71 -o pcpu,rss,size,vsizeJava进程为pid 71 的Java进程的容器内部。实际上-XX:MaxRam并没有帮助,但您提供的链接对串行GC有所帮助。
Nicolas Henneaux

8

TL; DR

内存的详细用法由本机内存跟踪(NMT)详细信息(主要是代码元数据和垃圾收集器)提供。除此之外,Java编译器和优化器C1 / C2占用摘要中未报告的内存。

可以使用JVM标志减少内存占用量(但是会产生影响)。

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


每个组件的细节

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

-Xshare:off

所述垃圾收集器串行具有垃圾收集处理期间,在较长的暂停时间成本最小的存储器占用(参见在一个画面GC之间阿列克谢Shipilë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在本机内存跟踪文档中提供了一些详细信息。有关高级编译策略禁用C2中的编译级别的更多详细信息,请将代码缓存大小减小5倍。有关JVM为什么报告的已提交内存比Linux进程常驻集大小更多的一些详细信息当两个编译器都被禁用时。


-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非常严格,因为它要求将应用程序模块化。不支持自动模块,因此没有简单的方法去那里。
Nicolas Henneaux

-1

如何正确调整Docker内存限制的大小? 通过监视一段时间来检查应用程序。要限制容器的内存,请尝试对docker run命令使用-m,--memory bytes选项-如果正在运行,则使用与之相当的选项

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

无法回答其他问题。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.