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

2022-08-31 06:53:30

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

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

堆大小设置为 128 MB (),而容器最多占用 1GB 内存。在正常情况下,它需要500MB。如果 docker 容器的限制低于 (例如 )则进程会作系统的内存不足杀手杀死。-Xmx128m -Xms128mmem_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的Web服务器,捆绑在36 MB的胖子中。

使用以下版本的操作系统和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


答案 1

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

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

那么,在Java进程中,什么会占用内存呢?

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

  1. Java 堆

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

  1. 垃圾回收器

GC 结构和算法需要额外的内存来管理堆。这些结构是标记位图,标记堆栈(用于遍历对象图),记住集(用于记录区域间引用)等。其中一些是直接可调的,例如,其他取决于堆布局,例如,较大的是G1区域(),较小的是记住的集合。-XX:MarkStackSizeMax-XX:G1HeapRegionSize

GC 内存开销因 GC 算法而异。 并具有最小的开销。G1 或 CMS 可以轻松使用总堆大小的 10% 左右。-XX:+UseSerialGC-XX:+UseShenandoahGC

  1. 代码缓存

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

  1. 编译器

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

  1. 类加载

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

  1. 符号表

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

  1. 线程

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

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

直接缓冲器

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

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

BufferPool MBean

除了直接的ByteBuffers之外,还可以有 - 映射到进程虚拟内存的文件。NMT不跟踪它们,但是,MappedByteBuffers也可以占用物理内存。而且没有一个简单的方法来限制他们可以拿走的金额。您可以通过查看进程内存映射来查看实际使用情况:MappedByteBufferspmap -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 代码可以根据需要分配任意数量的堆外内存,而无需 JVM 端的控制。这也涉及标准的Java类库。特别是,未关闭的 Java 资源可能会成为本机内存泄漏的来源。典型的例子是 或 。System.loadLibraryZipInputStreamDirectoryStream

JVMTI 代理,特别是调试代理 - 也可能导致过多的内存消耗。jdwp

此答案描述如何使用异步探查器分析本机内存分配。

分配器问题

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

jemalloc是一种替代的分配器,通常看起来比常规的libc更聪明,因此切换到可能会免费减少占用空间。mallocjemalloc

结论

没有保证的方法来估计 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内存消耗问题:本机内存跟踪pmapjemalloc异步分析器

更新

这是我的演讲的Java进程的内存足迹的录音。

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


答案 2

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也不需要释放已用内存。我希望能帮助您减少内存占用。最终,您可以调整 GC 配置(,, ...)XX:MaxRAM-XX:MinHeapFreeRatio-XX:MaxHeapFreeRatio


有许多类型的内存指标。Docker 似乎在报告 RSS 内存大小,这可能与报告的“已提交”内存不同(旧版本的 Docker 报告 RSS+缓存作为内存使用情况)。良好的讨论和链接:在 Docker 容器中运行的 JVM 的驻留集大小 (RSS) 和 Java 总提交内存 (NMT) 之间的差异jcmd

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