java使用的内存比堆大小(或docker内存限制)多得多

mwecs4sa  于 2021-07-26  发布在  Java
关注(0)|答案(3)|浏览(326)

对于我的应用程序,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捆绑在一个36mb的fat far中。
使用以下版本的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

mm9b1k5b

mm9b1k5b1#

java进程使用的虚拟内存远远超出了java堆。要知道,jvm包含许多子系统:垃圾收集器、类加载、jit编译器等等,所有这些子系统都需要一定数量的ram才能正常工作。
jvm不是ram的唯一消费者。本机库(包括标准java类库)也可以分配本机内存。本地内存跟踪甚至看不到这个。java应用程序本身也可以通过直接bytebuffers使用堆外内存。
那么,在java进程中什么需要内存呢?

jvm部件(主要通过本机内存跟踪显示)

java堆
最明显的部分。这就是java对象所在的地方。堆占用多达 -Xmx 内存量。
垃圾收集器
gc结构和算法需要额外的内存来进行堆管理。这些结构包括标记位图、标记堆栈(用于遍历对象图)、记忆集(用于记录区域间引用)等。其中一些是直接可调的,例如。 -XX:MarkStackSizeMax ,其他依赖于堆布局,例如,较大的是g1区域( -XX:G1HeapRegionSize ),较小的是记忆集。
gc内存开销因gc算法而异。 -XX:+UseSerialGC 以及 -XX:+UseShenandoahGC 开销最小。g1或cms可能很容易使用总堆大小的10%左右。
代码缓存
包含动态生成的代码:jit编译的方法、解释器和运行时存根。它的大小受到 -XX:ReservedCodeCacheSize (默认为240米)。关掉 -XX:-TieredCompilation 以减少编译代码的数量,从而减少代码缓存的使用。
编译程序
jit编译器本身也需要内存来完成它的工作。通过关闭分层编译或减少编译器线程的数量,可以再次减少这种情况: -XX:CICompilerCount .
类加载
类元数据(方法字节码、符号、常量池、注解等)存储在称为元空间的堆外区域中。加载的类越多,使用的元空间就越多。总使用量可能受到以下限制: -XX:MaxMetaspaceSize (默认情况下不受限制)和 -XX:CompressedClassSpaceSize (默认为1g)。
符号表
jvm的两个主要哈希表:符号表包含名称、签名、标识符等,字符串表包含对内部字符串的引用。如果本机内存跟踪指示字符串表占用大量内存,则可能意味着应用程序调用过多 String.intern .
线程
线程堆栈还负责获取ram。堆栈大小由 -Xss . 默认值是每个线程1m,但幸运的是情况并没有那么糟。操作系统会延迟分配内存页,即第一次使用时,因此实际内存使用量会低得多(通常每个线程堆栈80-200KB)。我编写了一个脚本来估计有多少rss属于java线程栈。
还有其他jvm部分分配本机内存,但它们通常在总内存消耗中不起很大作用。

直接缓冲器

应用程序可以通过调用 ByteBuffer.allocateDirect . 默认堆外限制等于 -Xmx ,但可以用 -XX:MaxDirectMemorySize . 直接副缓冲器包括在 Other nmt输出段(或 Internal jdk 11之前)。
使用的直接内存量可以通过jmx看到,例如在jconsole或java任务控制中:

除了直接的副缓冲器 MappedByteBuffers -Map到进程的虚拟内存的文件。nmt不跟踪它们,但是mappedbytebuffer也可以占用物理内存。而且没有一个简单的方法来限制他们能吃多少。您可以通过查看进程内存Map来查看实际使用情况: 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资源可能会成为本机内存泄漏的来源。典型的例子有 `ZipInputStream` 或者 `DirectoryStream` .
jvmti代理商, `jdwp` 调试代理-还可能导致内存消耗过多。
这个答案描述了如何使用异步探查器分析本机内存分配。

#### 分配器问题

进程通常直接从操作系统请求本机内存(通过 `mmap` 系统调用)或使用 `malloc` -标准libc分配器。反过来, `malloc` 使用从操作系统请求大块内存 `mmap` ,然后根据自己的分配算法管理这些块。问题是-此算法可能导致碎片和过度的虚拟内存使用。 `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内存消耗问题:本机内存跟踪、pmap、jemalloc、async profiler。

#### 更新

下面是一个java进程的演示内存占用记录。
在本视频中,我将讨论java进程中可能消耗的内存,如何监视和限制某些内存区域的大小,以及如何分析java应用程序中的本机内存泄漏。
hkmswyz6

hkmswyz62#

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+缓存报告为内存使用情况)。很好的讨论和链接:docker容器中运行的jvm的驻留集大小(rss)和java总提交内存(nmt)之间的差异
(rss)内存也可以被容器中的一些其他实用程序占用—shell、进程管理器。。。我们不知道容器中还运行着什么,以及如何在容器中启动进程。

7gs2gvoe

7gs2gvoe3#

热释光;博士

内存的详细使用情况由本机内存跟踪(nmt)详细信息(主要是代码元数据和垃圾收集器)提供。除此之外,java编译器和优化器c1/c2会消耗摘要中未报告的内存。
使用jvm标志可以减少内存占用(但是会有影响)。
docker容器的大小必须通过应用程序预期负载的测试来完成。

各部件详图

可以在容器内禁用共享类空间,因为其他jvm进程不会共享这些类。可以使用以下标志。它将删除共享类空间(17mb)。

-Xshare:off

垃圾收集器序列具有最小的内存占用,但在垃圾收集处理过程中需要较长的暂停时间(请参阅aleksey shipil)ëv一张图片中gc之间的比较)。可以使用以下标志启用它。它可以节省最多使用的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的内存使用。
使用graalvm编译器(取代c2)可以减少内存占用。它增加了20mb的代码内存空间,减少了60mb的外部jvm内存。
java memory management for jvm一文提供了有关不同内存空间的一些相关信息。o

相关问题