对于我的应用程序,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
该应用程序是一个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
答案 0 :(得分:143)
Java进程使用的虚拟内存远远超出了Java Heap的范畴。您知道,JVM包含许多子系统:垃圾收集器,类加载,JIT编译器等,所有这些子系统都需要一定数量的RAM才能起作用。
JVM不是RAM的唯一使用者。本机库(包括标准Java类库)也可以分配本机内存。而且,本机内存跟踪甚至看不到它。 Java应用程序本身也可以通过直接ByteBuffer使用堆外内存。
那么在Java进程中占用内存的是什么?
Java堆
最明显的部分。这是Java对象所在的位置。堆最多需要-Xmx
的内存量。
垃圾收集器
GC结构和算法需要额外的内存来进行堆管理。这些结构包括标记位图,标记堆栈(用于遍历对象图),记忆集(用于记录区域间引用)等。其中一些是直接可调的,例如-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的两个主要哈希表:Symbol表包含名称,签名,标识符等,而String表包含对内部字符串的引用。如果“本机内存跟踪”通过字符串表指示大量内存使用,则可能意味着应用程序过度调用了String.intern
。
线程
线程堆栈还负责占用RAM。堆栈大小由-Xss
控制。默认值为每个线程1M,但是幸运的是情况还不错。 OS会延迟分配内存页面,即在首次使用时分配内存页面,因此实际内存使用量会低得多(每个线程堆栈通常为80-200 KB)。我写了script来估算有多少RSS属于Java线程堆栈。
还有其他JVM部件分配本机内存,但是它们通常不会在总内存消耗中发挥重要作用。
应用程序可以通过调用ByteBuffer.allocateDirect
显式请求堆外内存。默认的堆外限制等于-Xmx
,但是可以用-XX:MaxDirectMemorySize
覆盖。直接字节缓冲区包含在NMT输出的Other
部分中(或JDK 11之前的Internal
中)。
已使用的直接内存量可以通过JMX看到,例如在JConsole或Java Mission Control中:
除了直接的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
^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^
System.loadLibrary
加载的 JNI代码可以分配所需的堆外内存,而无需JVM进行控制。这也与标准Java类库有关。特别是,未关闭的Java资源可能会成为本机内存泄漏的来源。典型示例为ZipInputStream
或DirectoryStream
。
JVMTI代理,尤其是jdwp
调试代理-也可能导致过多的内存消耗。
This answer介绍了如何使用async-profiler来分析本机内存分配。
进程通常直接从OS(通过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。
答案 1 :(得分:11)
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为内存使用情况)。
好的讨论和链接:Difference between Resident Set Size (RSS) and Java total committed memory (NMT) for a JVM running in Docker container
(RSS)内存也可以被容器中的一些其他实用程序吃掉-外壳程序,进程管理器,……我们不知道容器中还运行着什么,以及如何在容器中启动进程。>
答案 2 :(得分:4)
本机内存跟踪(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的代码存储空间,而从外部JVM内存减少了60MB。
文章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?的一些详细信息。
答案 3 :(得分:0)
以上所有答案都告诉您JVM占用大量内存的原因,但是也许您最需要的是解决方案,这些文章将对您有所帮助:
-https://blogs.oracle.com/java-platform-group/java-se-support-for-docker-cpu-and-memory-limits
-https://royvanrijn.com/blog/2018/05/java-and-docker-memory-limits/
答案 4 :(得分:-1)
如何正确调整Docker内存限制的大小? 通过监视一段时间来检查应用程序。要限制容器的内存,请尝试对docker run命令使用-m,--memory bytes选项-如果不运行,则使用等效选项 像
docker run -d --name my-container --memory 500m <iamge-name>
无法回答其他问题。
答案 5 :(得分:-2)
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
如果您不想花时间在上面,可以简单地分配更多的内存。有时候这是最好的。