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 堆。要知道,JVM 包括许多子系统:垃圾收集器、类加载、JIT 编译器等,所有这些子系统都需要一定数量的 RAM 才能运行。
JVM 不是 RAM 的唯一消费者。 native 库(包括标准 Java 类库)也可以分配 native 内存。这甚至对 Native Memory Tracking 都看不到。 Java 应用程序本身也可以通过直接 ByteBuffers 使用堆外内存。
那么在 Java 进程中什么需要内存呢?
JVM 部分(主要由 Native Memory Tracking 显示)

  • Java堆

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

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

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

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

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

  • JVM 的两个主要哈希表:符号表包含名称、签名、标识符等,字符串表包含对内部字符串的引用。如果 native 内存跟踪表明字符串表使用了大量内存,则可能意味着应用程序过度调用 String.intern .
  • 线程

  • 线程堆栈还负责占用 RAM。堆栈大小由 -Xss 控制.默认是每个线程 1M,不过好在事情并没有那么糟糕。操作系统延迟分配内存页面,即在第一次使用时,因此实际内存使用量会低得多(通常每个线程堆栈 80-200 KB)。我写了一个 script估计有多少 RSS 属于 Java 线程堆栈。
    还有其他 JVM 部分会分配 native 内存,但它们通常不会在总内存消耗中发挥重要作用。
    直接缓冲区
    应用程序可以通过调用 ByteBuffer.allocateDirect 显式请求堆外内存。 .默认的堆外限制等于 -Xmx ,但它可以被 -XX:MaxDirectMemorySize 覆盖.直接 ByteBuffers 包含在 Other 中NMT 输出的部分(或 JDK 11 之前的 Internal)。
    使用的直接内存量通过 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
                               ^^^^^               ^^^^^^^^^^^^^^^^^^^^^^^^
    
    本地库System.loadLibrary加载的JNI代码可以根据需要分配尽可能多的堆外内存,而不受 JVM 端的控制。这也涉及标准 Java 类库。特别是,未关闭的 Java 资源可能成为 native 内存泄漏的来源。典型的例子是 ZipInputStreamDirectoryStream .
    JVMTI 代理,尤其是 jdwp调试代理 - 也会导致内存消耗过多。
    This answer描述如何使用 async-profiler 分析 native 内存分配.
    分配器问题
    进程通常直接从操作系统(通过 mmap 系统调用)或使用 malloc 请求 native 内存。 - 标准的 libc 分配器。反过来,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 Tracking , pmap , jemalloc , async-profiler .
    更新
    这是我的演讲录音Memory Footprint of a Java Process .
    在本视频中,我将讨论 Java 进程中哪些可能会消耗内存、如何监控和限制某些内存区域的大小,以及如何分析 Java 应用程序中的 native 内存泄漏。

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

    相关文章:

    mongodb - k8s中的mongo设置不使用持久卷

    java - 你会在 MySQL 中映射 Java/Hibernate 中的 BigDecimal 什么类型?

    java - 如何减少 Java 日志样板代码?

    linux - 如何在多个命令中优先选择一个命令

    linux - 如何将 docker 镜像指向我的 .m2 目录,以便在 mac 上的 docker 中运行 maven?

    linux - 循环运行 shell 脚本时出错

    linux - docker swarm 在检查节点详细信息时抛出 404 页面未找到错误

    java - JPA - 无法获取在一对一关系中生成的主键

    java - 修改listView的switch方法,删除手机内存中的文件

    nginx - 在 Docker Swarm 1.12 访问服务时记录客户端的 "real"IP 地址