jvm Java使用的内存远远超过堆大小(或大小正确的Docker内存限制)

vu8f3i0k  于 2022-11-07  发布在  Java
关注(0)|答案(5)|浏览(526)

对于我的应用程序,Java进程使用的内存远远大于堆大小。
运行容器的系统开始出现内存问题,因为容器占用的内存远远超过堆大小。
堆大小设置为128 MB(-Xmx128m -Xms128m),而容器占用1GB内存。在正常情况下,它需要500 MB。如果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

从容器内部,我得到了进程使用的内存。
第一个
该应用程序是一个使用Jetty/Jersey/CDI的Web服务器,捆绑在一个36 MB的胖far中。
使用了以下版本的操作系统和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

pdkcd3nj

pdkcd3nj1#

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(默认为240 M)的限制。关闭-XX:-TieredCompilation可减少编译的代码量,从而减少代码缓存的使用。

4.编译器

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

5.类加载

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

6.符号表

JVM的两个主要散列表:符号表包含名称、签名、标识符等,字符串表包含对被保留字符串的引用。如果本地内存跟踪指示字符串表使用了大量内存,则可能意味着应用程序过度调用String.intern

7.线程

线程堆栈也负责占用RAM。堆栈大小由-Xss控制。默认值是每个线程1 M,但幸运的是情况并不那么糟糕。操作系统会延迟分配内存页,即在第一次使用时,因此实际的内存使用量会低得多我写了一个script来估计RSS中有多少属于Java线程堆栈。
还有其他JVM部分分配本机内存,但它们通常在总内存消耗中不起很大作用。

直接缓冲区

应用程序可以通过调用ByteBuffer.allocateDirect来显式请求堆外内存。默认堆外限制等于-Xmx,但可以使用-XX:MaxDirectMemorySize覆盖它。NMT输出的Other部分(或JDK 11之前的Internal)中包含直接ByteBuffers。
使用中的直接内存量可通过JMX查看,例如在JConsole或Java使命Control中:

除了直接的ByteBuffers之外,还有MappedByteBuffers-Map到进程虚拟内存的文件。NMT不会跟踪它们,但是,MappedByteBuffers也可以占用物理内存。而且没有一个简单的方法来限制它们可以占用多少内存。你可以通过查看进程内存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
                           ^^^^^               ^^^^^^^^^^^^^^^^^^^^^^^^

本机库

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内存消耗的问题:一个五个六个七个八个。

更新

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

oogrdqng

oogrdqng2#

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= 500 m,您的堆将大约为250 mb。
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、进程管理器等)占用。我们不知道容器中还运行着什么,也不知道如何启动容器中的进程。

hfsqlsce

hfsqlsce3#

TL;DR

本机内存跟踪(NMT)详细信息(主要是代码元数据和垃圾收集器)提供了内存的详细使用情况。除此之外,Java编译器和优化器C1/C2消耗的内存未在摘要中报告。
使用JVM标志可以减少内存占用量(但会产生影响)。
Docker容器大小必须通过测试应用程序的预期负载来确定。

每个组件的详细信息

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

-Xshare:off

垃圾收集器串行有一个最小的内存占用,代价是在垃圾收集处理过程中有更长的暂停时间(见Aleksey Shipilëv在一张图片中对GC的比较)。它可以通过下面的标志启用。它可以保存多达所用的GC空间(48 MB)。

-XX:+UseSerialGC

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

-XX:+TieredCompilation -XX:TieredStopAtLevel=1

代码空间减少20 MB,JVM外内存减少80 MB(NMT空间和RSS空间的差值)优化编译器C2需要100 MB

C1和C2编译器可使用以下标志禁用。

-Xint

JVM外部的内存现在低于提交的总空间。代码空间减少了43 MB。请注意,这会对应用程序的性能产生重大影响。禁用C1和C2编译器会减少170 MB的内存使用。
使用Graal VM compiler(替换C2)会导致内存占用量更小。它增加了20 MB的代码内存空间,并从JVM内存外部减少了60 MB。
文章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?中提供了一些详细信息。

zyfwsgd6

zyfwsgd64#

Java需要大量的内存。JVM本身也需要大量的内存来运行。堆是虚拟机内部的可用内存,可用于您的应用程序。因为JVM是一个包含了所有可能的好东西的大捆绑包,它需要大量的内存来加载。
从java 9开始,你有一个叫做project Jigsaw的东西,它可以减少你启动java应用程序时所用的内存(沿着启动时间)。jigsaw项目和一个新的模块系统不一定是为了减少所需的内存而创建的,但是如果这很重要的话,你可以给予。
你可以看一下这个例子:https://steveperkins.com/using-java-9-modularization-to-ship-zero-dependency-native-apps/。通过使用模块系统,CLI应用程序的大小为21MB(嵌入JRE)。JRE占用的内存超过200MB。这将在应用程序启动时转化为更少的分配内存(许多未使用的JRE类将不再加载)。
下面是另一个不错的教程:https://www.baeldung.com/project-jigsaw-java-modularity
如果你不想花时间在这上面,你可以简单地分配更多的内存。有时这是最好的。

vngu2lb8

vngu2lb85#

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

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

不能回答其他问题。

相关问题