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

标签 java linux docker memory jvm

对于我的应用程序,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

最佳答案

Java进程使用的虚拟内存远远超出了Java Heap的范畴。您知道,JVM包括许多子系统:垃圾收集器,类加载,JIT编译器等,所有这些子系统都需要一定数量的RAM才能起作用。
JVM不是RAM的唯一使用者。 native 库(包括标准Java类库)也可以分配 native 内存。这对于 native 内存跟踪甚至是不可见的。 Java应用程序本身也可以通过直接ByteBuffers使用堆外内存。
那么,什么需要占用Java进程中的内存呢?
JVM部分(主要由 native 内存跟踪显示)

  • Java堆

  • 最明显的部分。这是Java对象所在的位置。堆占用-Xmx内存量。
  • 垃圾收集器

  • GC结构和算法需要额外的内存来进行堆管理。这些结构包括标记位图,标记堆栈(用于遍历对象图), memset (用于记录区域间引用)等。其中一些是直接可调的,例如-XX:MarkStackSizeMax,其他取决于堆布局,例如G1区域(-XX:G1HeapRegionSize)越大,记住的集合就越小。
    GC内存开销因GC算法而异。 -XX:+UseSerialGC-XX:+UseShenandoahGC具有最小的开销。 G1或CMS可能会轻易使用大约总堆大小的10%。
  • 代码缓存

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

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

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

  • JVM的两个主要哈希表:Symbol表包含名称,签名,标识符等,而String表包含对嵌入字符串的引用。如果“ native 内存跟踪”通过字符串表指示大量内存使用,则可能意味着应用程序过度调用了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中:
    BufferPool MBean
    除了直接的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
                               ^^^^^               ^^^^^^^^^^^^^^^^^^^^^^^^
    
    native 库
    System.loadLibrary加载的JNI代码可以分配所需的尽可能多的堆外内存,而无需JVM端的控制。这也涉及标准的Java类库。特别是,未关闭的Java资源可能会成为 native 内存泄漏的来源。典型的示例是ZipInputStreamDirectoryStream
    JVMTI代理(尤其是jdwp调试代理)也可能导致过多的内存消耗。
    This answer描述了如何使用async-profiler分析 native 内存分配。
    分配器问题
    进程通常直接从OS(通过mmap系统调用)或通过使用malloc(标准libc分配器)来请求 native 内存。反过来,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 Trackingpmapjemallocasync-profiler
    更新
    这是我的演示文稿Memory Footprint of a Java Process的记录。
    在本视频中,我讨论了Java进程中可能消耗内存的内容,如何监视和限制某些内存区域的大小以及如何分析Java应用程序中的 native 内存泄漏。

    关于Java使用的内存多于堆大小(或正确大小的Docker内存限制),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54713829/

    相关文章:

    linux - 如何安装相同 rpm 的两个不同版本并使它们并行工作

    java - 如何使用 vim 猛拉 java 代码中的函数

    java - Spring @Controller 单元测试 @RequestMapping

    java - 如何使用jSTL、servlet在jsp中显示输出

    php - 从 php 调用 shell 脚本 - 无法正常工作

    drupal - 如何在Docker中具有正确权限的共享卷内创建文件夹?

    java - 单语句抛出异常

    linux - 使用 bash 合并多个 SQLite 数据库?

    mysql - docker-compose 无法启动 mysql :8 correctly

    docker - 使用 Ansible 停止所有现有的 docker 容器