Java为什么使用比堆中规定的大小还要多的内存,如何正确设置Docker内存大小限制?Java进程使用的内存远远超过堆大小?
堆大小设置为128 MB(-Xmx128m -Xms128m),而容器最多占用1 GB内存。在正常情况下,它需要500MB。如果docker容器设置限制(例如mem_limit=mem_limit=400MB),则该进程被OS的内存不足杀手杀死。
为什么Java进程使用比堆更多的内存吗?如何正确调整Docker内存限制?有没有办法减少Java进程的offheap内存占用?
Java进程使用的虚拟内存远远超出了Java堆。JVM包含许多子系统:垃圾收集器,类加载,JIT编译器等,所有这些子系统都需要一定量的RAM才能运行。
JVM不是RAM的唯一消费者。本机库(包括标准Java类库)也可以分配本机内存。而本机内存跟踪甚至无法看到这一点。Java应用程序本身也可以通过直接ByteBuffers使用堆外内存。
那么Java进程是如何消耗内存的?
JVM部件 (主要通过本机内存跟踪显示)
直接缓冲
应用程序可以通过调用显式请求堆外内存ByteBuffer.allocateDirect。默认的堆外限制等于-Xmx,但可以覆盖它-XX:MaxDirectMemorySize。Direct ByteBuffers包含在OtherNMT输出部分(或InternalJDK 11之前)。
在JConsole或Java Mission Control中通过JMX可以看到使用的直接内存量。
除了直接的ByteBuffers,还可以有MappedByteBuffers- 映射到进程虚拟内存的文件。NMT不跟踪它们,但MappedByteBuffers也可以占用物理内存。而且没有一种简单的方法来限制它们可以承受多少。您可以通过查看进程内存映射来查看实际使用情况:pmap -x <pid>
本地库包
加载的JNI代码System.loadLibrary可以根据需要分配尽可能多的堆外内存,而无需JVM端的控制。这也涉及标准的Java类库。特别是,未封闭的Java资源可能成为本机内存泄漏的来源。典型的例子是ZipInputStream或DirectoryStream。
JVMTI代理,特别是jdwp调试代理 - 也可能导致过多的内存消耗。
分配器问题
进程通常直接从OS(通过mmap系统调用)或使用malloc- 标准libc分配器请求本机内存。反过来,malloc请求OS使用大块内存mmap,然后根据自己的分配算法管理这些块。问题是 - 该算法可能导致碎片和 过多的虚拟内存使用 。
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 。