Java使用比堆大小更多的内存(或正确的Docker内存限制大小)

Nic*_*aux 95 java linux memory jvm docker

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

容器正在运行的系统开始出现内存问题,因为容器占用的内存比堆大小多得多.

堆大小设置为128 MB(-Xmx128m -Xms128m),而容器最多占用1 GB内存.在正常情况下,它需要500MB.如果docker容器具有以下限制(例如mem_limit=mem_limit=400MB),则该进程被OS的内存不足杀死所杀死.

你能解释为什么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
Run Code Online (Sandbox Code Playgroud)

从容器内部,我获得进程使用的内存.

$ ps -p 71 -o pcpu,rss,size,vsize
%CPU   RSS  SIZE    VSZ
11.2 486040 580860 3814600
Run Code Online (Sandbox Code Playgroud)
$ 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
Run Code Online (Sandbox Code Playgroud)

该应用程序是一个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
Run Code Online (Sandbox Code Playgroud)

https://gist.github.com/prasanthj/48e7063cac88eb396bc9961fb3149b58

apa*_*gin 163

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

JVM不是RAM的唯一消费者.本机库(包括标准Java类库)也可以分配本机内存.而本机内存跟踪甚至无法看到这一点.Java应用程序本身也可以通过直接ByteBuffers使用堆外内存.

那么什么需要Java进程中的内存?

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

  1. Java堆

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

  2. 垃圾收集器

    GC结构和算法需要额外的内存用于堆管理.这些结构是Mark Bitmap,Mark Stack(用于遍历对象图),Remembered Sets(用于记录区域间引用)等.其中一些是直接可调的,例如-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. 类加载

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

  6. 符号表

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

  7. 主题

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

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

直接缓冲

应用程序可以通过调用显式请求堆外内存ByteBuffer.allocateDirect.默认的堆外限制等于-Xmx,但可以覆盖它-XX:MaxDirectMemorySize.Direct ByteBuffers包含在OtherNMT输出部分(或InternalJDK 11之前).

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

BufferPool MBean

除了直接的ByteBuffers,还可以有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
                           ^^^^^               ^^^^^^^^^^^^^^^^^^^^^^^^
Run Code Online (Sandbox Code Playgroud)

本地图书馆

加载的JNI代码System.loadLibrary可以根据需要分配尽可能多的堆外内存,而无需JVM端的控制.这也涉及标准的Java类库.特别是,未封闭的Java资源可能成为本机内存泄漏的来源.典型的例子是ZipInputStreamDirectoryStream.

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

此答案描述了如何使用async-profiler配置本机内存分配.

分配器问题

进程通常直接从OS(通过mmap系统调用)或使用malloc- 标准libc分配器请求本机内存.反过来,malloc要求OS使用大块内存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 + ...
Run Code Online (Sandbox Code Playgroud)

可以通过JVM标志缩小或限制某些内存区域(如代码缓存),但许多其他内存区域完全不受JVM控制.

设置Docker限制的一种可能方法是在进程的"正常"状态下观察实际内存使用情况.有研究Java内存消耗问题的工具和技术:Native Memory Tracking,pmap,jemalloc,async-profiler.

  • @j-keck字符串对象在堆中,但是哈希表(桶和带引用和哈希码的条目)位于堆外内存中.我把这句话改写得更精确.谢谢你指出. (4认同)
  • 自 jdk7 以来,堆中不是实习字符串吗?(https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6962931) - 也许我错了。 (2认同)

Jan*_*raj 13

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,进程管理器......我们不知道容器中还有什么运行,以及如何在容器中启动进程.


Nic*_*aux 12

TL; 博士

内存的详细使用情况由 Native Memory Tracking (NMT) 详细信息(主要是代码元数据和垃圾收集器)提供。除此之外,Java 编译器和优化器 C1/C2 消耗了摘要中未报告的内存。

使用 JVM 标志可以减少内存占用(但有影响)。

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


每个组件的详细信息

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

-Xshare:off
Run Code Online (Sandbox Code Playgroud)

所述垃圾收集器串行具有垃圾收集处理期间,在较长的暂停时间成本最小的存储器占用(参见在一个画面GC之间阿列克谢Shipilëv比较)。可以使用以下标志启用它。它最多可以节省已使用的 GC 空间 (48MB)。

-XX:+UseSerialGC
Run Code Online (Sandbox Code Playgroud)

所述C2编译器可以用下面的标志被禁用,以减少用于决定是否优化与否的方法分析数据。

-XX:+TieredCompilation -XX:TieredStopAtLevel=1
Run Code Online (Sandbox Code Playgroud)

代码空间减少了 20MB。而且JVM外的内存减少了80MB(NMT空间和RSS空间的区别)。优化编译器 C2 需要 100MB。

C1和C2的编译器可以用下面的标志被禁用。

-Xint
Run Code Online (Sandbox Code Playgroud)

JVM 外部的内存现在低于总提交空间。代码空间减少了 43MB。请注意,这会对应用程序的性能产生重大影响。禁用 C1 和 C2 编译器可减少 170 MB 使用的内存。

使用Graal VM 编译器(替代 C2)可以减少内存占用。它增加了 20MB 的代码内存空间,减少了 60MB 的外部 JVM 内存。

本文为JVM Java的内存管理提供了一些相关信息的不同的存储空间。Oracle 在Native Memory Tracking 文档中提供了一些详细信息。有关高级编译策略禁用 C2 中编译级别的更多详细信息,将代码缓存大小减少 5 倍。关于为什么 JVM 报告的提交内存比 Linux 进程驻留集大小多的一些细节当两个编译器都被禁用时。


归档时间:

查看次数:

14992 次

最近记录:

6 年,2 月 前