ChatGPT解决这个技术问题 Extra ChatGPT

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

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

该应用程序是一个使用 Jetty/Jersey/CDI 捆绑在 36 MB 内的 Web 服务器。

使用以下版本的 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

堆是分配对象的地方,但是 JVM 有许多其他内存区域,包括共享库、直接内存缓冲区、线程堆栈、GUI 组件、元空间。您需要查看 JVM 可以有多大,并将限制设置得足够高,以至于您宁愿进程死掉也不愿再使用。
看起来 GC 正在使用大量内存。您可以尝试改用 CMS 收集器。看起来 ~125 MB 用于元空间 + 代码,但是如果不缩小代码库,您不太可能将其缩小。承诺的空间接近你的极限,所以它被杀死也就不足为奇了。
您在哪里/如何设置 -Xms 和 -Xmx 配置?
您的程序是否执行许多文件操作(例如创建千兆字节大小的文件)?如果是这样,您应该知道 cgroups 会将磁盘缓存添加到已用内存中 - 即使它由内核处理并且对用户程序不可见。 (请注意,命令 psdocker stats 不计算磁盘缓存。)

a
apangin

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

JVM 不是 RAM 的唯一消费者。本机库(包括标准 Java 类库)也可以分配本机内存。而这对 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)越大,记忆集越小。

GC 内存开销因 GC 算法而异。 -XX:+UseSerialGC-XX:+UseShenandoahGC 的开销最小。 G1 或 CMS 可能很容易使用大约 10% 的总堆大小。

代码缓存

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

编译器

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

类加载

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

符号表

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

线程

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

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

直接缓冲区

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

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

https://i.stack.imgur.com/Wmszn.png

除了直接的 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 资源可能会成为本机内存泄漏的来源。典型示例是 ZipInputStreamDirectoryStream

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

This answer 描述了如何使用 async-profiler 分析本机内存分配。

分配器问题

进程通常直接从操作系统(通过 mmap 系统调用)或使用 malloc - 标准 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 Trackingpmapjemallocasync-profiler

更新

这是我的演示文稿 Memory Footprint of a Java Process 的录音。

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


自 jdk7 以来,堆中没有被实习过的字符串吗? (bugs.java.com/bugdatabase/view_bug.do?bug_id=6962931) - 也许我错了。
@j-keck 字符串对象在堆中,但哈希表(桶和具有引用和哈希码的条目)在堆外内存中。我把这句话改写得更准确。感谢您指出。
除此之外,即使您使用非直接 ByteBuffers,JVM 也会在本机内存中分配临时直接缓冲区,而不会施加内存限制。参照。 evanjones.ca/java-bytebuffer-leak.html
J
Jan Garaj

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+缓存报告为内存使用情况)。很好的讨论和链接:Difference between Resident Set Size (RSS) and Java total committed memory (NMT) for a JVM running in Docker container

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


-XX:MaxRam 确实更好。我认为它仍然使用超过定义的最大值,但它更好,谢谢!
也许您真的需要更多内存来存储这个 Java 实例。有15267个类,56个线程。
提供的 RSS 来自仅用于 Java 进程的容器内部 ps -p 71 -o pcpu,rss,size,vsize,Java 进程的 pid 为 71。实际上 -XX:MaxRam 没有帮助,但您提供的链接有助于串行 GC。
N
Nicolas Henneaux

TL;博士

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

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

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

每个组件的详细信息

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

-Xshare:off

垃圾收集器序列号具有最小的内存占用,但代价是垃圾收集处理期间的暂停时间较长(请参阅Aleksey Shipilëv comparison between GC in one picture)。可以使用以下标志启用它。它最多可以节省使用的 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。

使用 Graal VM compiler(替换 C2)会导致内存占用更小。它增加了 20MB 的代码内存空间,减少了 60MB 的外部 JVM 内存。

文章 Java Memory Management for JVM 提供了不同内存空间的一些相关信息。 Oracle 在 Native Memory Tracking documentation 中提供了一些详细信息。 advanced compilation policydisable C2 reduce code cache size by a factor 5 中有关编译级别的更多详细信息。禁用两个编译器时有关 Why does a JVM report more committed memory than the Linux process resident set size? 的一些详细信息。


a
adiian

Java 需要大量内存。 JVM 本身需要大量内存才能运行。堆是虚拟机内部可用的内存,可供您的应用程序使用。因为 JVM 是一个包含所有好东西的大包,所以它需要大量内存才能加载。

从 java 9 开始,您有一个称为 project Jigsaw 的东西,它可能会减少启动 java 应用程序时使用的内存(以及启动时间)。项目拼图和一个新的模块系统不一定是为了减少必要的内存而创建的,但如果它很重要,你可以尝试一下。

您可以看一下这个例子:https://steveperkins.com/using-java-9-modularization-to-ship-zero-dependency-native-apps/。通过使用模块系统,它产生了 21MB 的 CLI 应用程序(嵌入了 JRE)。 JRE 占用超过 200mb。当应用程序启动时,这应该会转化为更少的分配内存(许多未使用的 JRE 类将不再被加载)。

这是另一个不错的教程:https://www.baeldung.com/project-jigsaw-java-modularity

如果您不想花时间在这上面,您可以简单地分配更多内存。有时它是最好的。


使用 jlink 具有很大的限制性,因为它要求将应用程序模块化。不支持自动模块,因此没有简单的方法可以去那里。
v
v_sukt

如何正确调整 Docker 内存限制?通过监视一段时间来检查应用程序。要限制容器的内存,请尝试对 docker run 命令使用 -m、--memory bytes 选项 - 或者如果您正在运行它,则使用其他等效项,例如

docker run -d --name my-container --memory 500m <iamge-name>

无法回答其他问题。