对于我的应用程序,Java进程使用的内存远远大于堆大小。
容器运行所在的系统开始出现内存问题,因为容器占用的内存比堆大小大得多。
堆大小设置为128 MB(-Xmx128m -Xms128m),而容器最多占用1GB的内存。正常情况下需要500MB。如果docker容器的限制低于(例如mem_limit=mem_limit=400MB),则该进程将被操作系统的内存不足杀手杀死。
Xmx128m -Xms128m
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。
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
Java进程使用的虚拟内存远远超出了Java堆。你知道,JVM包含许多子系统:垃圾收集器,类加载,JIT编译器等,所有这些子系统都需要一定数量的RAM才能起作用。
JVM不是RAM的唯一使用者。本机库(包括标准Java类库)也可以分配本机内存。这对于本机内存跟踪甚至是不可见的。Java应用程序本身也可以通过直接ByteBuffer使用堆外内存。
那么,什么需要占用Java进程中的内存呢?
JVM部分(主要由本机内存跟踪显示) 1. Java Heap
最明显的部分。这是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。
类元数据(方法字节码,符号,常量池,注释等)存储在称为元空间的堆外区域中。加载的类越多-使用的元空间越多。总使用量可以受到限制-XX:MaxMetaspaceSize(默认情况下无限制)和 -XX:CompressedClassSpaceSize(默认情况下为1G)。
JVM的两个主要哈希表:Symbol表包含名称,签名,标识符等,而String表包含对嵌入字符串的引用。如果本机内存跟踪通过String表指示大量内存使用,则可能意味着应用程序过度调用String.intern。
线程堆栈还负责占用RAM。堆栈大小由控制-Xss。默认值为每个线程1M,但幸运的是情况还不错。OS会延迟分配内存页,即在首次使用时分配内存页,因此实际的内存使用量会低得多(每个线程堆栈通常为80-200 KB)。我编写了一个脚本来估计有多少RSS属于Java线程堆栈。
还有其他JVM部分分配本机内存,但是它们通常在总内存消耗中不起作用。
直接缓冲区
应用程序可以通过调用显式请求堆外内存ByteBuffer.allocateDirect。默认的堆外限制等于-Xmx,但可以用覆盖-XX:MaxDirectMemorySize。直接字节缓冲区包含在OtherNMT输出部分中(或Internal在JDK 11之前)。
ByteBuffer.allocateDirect
-XX:MaxDirectMemorySize
通过JMX可以查看已使用的直接内存量,例如在JConsole或Java Mission Control中:
除了直接的ByteBuffer外,还可以将MappedByteBuffers文件映射到进程的虚拟内存。NMT不会跟踪它们,但是,MappedByteBuffers也可以占用物理内存。而且没有简单的方法来限制他们可以服用多少。你可以通过查看进程内存映射来查看实际用法:pmap -x
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资源可能会成为本机内存泄漏的来源。典型示例为ZipInputStream或DirectoryStream。
ZipInputStream
DirectoryStream
JVMTI代理(尤其是jdwp调试代理)也可能导致过多的内存消耗。
JVMTI
jdwp
此答案描述了如何使用async-profiler来分析本机内存分配。
分配者问题
进程通常直接从OS(通过mmap系统调用)或通过使用malloc标准libc分配器来请求本机内存。依次malloc使用mmap,从OS请求大块内存,然后根据其自己的分配算法管理这些大块。问题是-该算法可能导致碎片和过多的虚拟内存使用。
jemalloc,替代分配器,通常看起来比常规libc更智能malloc,因此切换为jemalloc可能会导致免费占用的空间较小。
jemalloc
libc
malloc
结论
由于有太多因素需要考虑,因此无法保证估算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内存消耗问题的工具和技术:本机内存跟踪,pmap,jemalloc和async-profiler。
更新资料
这是我对Java进程的内存占用的演示的记录。
在本视频中,我讨论了Java进程中可能消耗内存的内容,如何监视和限制某些内存区域的大小以及如何分析Java应用程序中的本机内存泄漏。