对于我的应用程序,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
5条答案
按热度按时间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>
本机库
由
System.loadLibrary
加载的JNI代码可以分配任意多的堆外内存,而不受JVM端的控制。这也涉及到标准Java类库。特别是,未关闭的Java资源可能成为本机内存泄漏的来源。典型的示例是ZipInputStream
或DirectoryStream
。JVMTI代理--特别是
jdwp
调试代理--也会导致内存消耗过大。This answer说明如何使用async-profiler分析原生内存配置。
分配器问题
进程通常直接从操作系统(通过
mmap
系统调用)或通过使用malloc
(标准libc分配器)请求本机内存。反过来,malloc
使用mmap
从操作系统请求大块内存,然后根据自己的分配算法管理这些内存块。问题是-此算法可能导致碎片和excessive virtual memory usage。jemalloc
是一种替代的分配器,它通常比常规的libcmalloc
更智能,因此切换到jemalloc
可能会免费获得更小的内存占用。结论
没有可靠的方法来估计Java进程的全部内存使用情况,因为要考虑的因素太多了。
可以通过JVM标志缩小或限制某些内存区域(如代码缓存),但许多其他区域完全不受JVM控制。
设置Docker限制的一个可能的方法是观察进程“正常”状态下的实际内存使用情况。有一些工具和技术可以调查Java内存消耗的问题:一个五个六个七个八个。
更新
这是我的演讲录音。
在本视频中,我将讨论在Java进程中哪些内容可能会消耗内存、如何监视和限制某些内存区域的大小,以及如何分析Java应用程序中的本机内存泄漏。
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、进程管理器等)占用。我们不知道容器中还运行着什么,也不知道如何启动容器中的进程。
hfsqlsce3#
TL;DR
本机内存跟踪(NMT)详细信息(主要是代码元数据和垃圾收集器)提供了内存的详细使用情况。除此之外,Java编译器和优化器C1/C2消耗的内存未在摘要中报告。
使用JVM标志可以减少内存占用量(但会产生影响)。
Docker容器大小必须通过测试应用程序的预期负载来确定。
每个组件的详细信息
共享类空间可以在容器内禁用,因为类不会被其他JVM进程共享。可以使用以下标志。它将删除共享类空间(17 MB)。
垃圾收集器串行有一个最小的内存占用,代价是在垃圾收集处理过程中有更长的暂停时间(见Aleksey Shipilëv在一张图片中对GC的比较)。它可以通过下面的标志启用。它可以保存多达所用的GC空间(48 MB)。
C2编译器可以使用以下标志禁用,以减少用于决定是否优化方法的分析数据。
代码空间减少20 MB,JVM外内存减少80 MB(NMT空间和RSS空间的差值)优化编译器C2需要100 MB
C1和C2编译器可使用以下标志禁用。
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 policy和disable C2 reduce code cache size by a factor 5中提供了有关编译级别的更多详细信息。在两个编译器都被禁用时,在Why does a JVM report more committed memory than the Linux process resident set size?中提供了一些详细信息。
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
如果你不想花时间在这上面,你可以简单地分配更多的内存。有时这是最好的。
vngu2lb85#
**如何正确调整Docker内存限制的大小?**通过监视应用程序一段时间来检查它。要限制容器的内存,请尝试为Docker运行命令使用-m,--memory bytes选项-或者其他等效的命令(如果您正在运行它),例如
不能回答其他问题。