转载

为什么Java占用的RAM比Xmx多得多?

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部件 (主要通过本机内存跟踪显示)

  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(默认为240M)。关闭-XX:-TieredCompilation以减少编译代码的数量,从而减少代码缓存的使用。
  4. 编译器JIT编译器本身也需要内存来完成它的工作。这可以通过关闭分层编译或减少编译器线程数来再次减少:-XX:CICompilerCount。
  5. 类加载类元数据(方法字节码,符号,常量池,注释等)存储在称为Metaspace的堆外区域中。加载的类越多 - 使用的元空间越多。总使用量可以受限-XX:MaxMetaspaceSize(默认为无限制)和 -XX:CompressedClassSpaceSize(默认为1G)。
  6. 符号表JVM的两个主要哈希表:Symbol表包含名称,签名,标识符等,String表包含interned字符串。如果本机内存跟踪指示String表占用大量内存,则可能意味着应用程序过度调用String.intern。
  7. 主题线程堆栈也负责获取RAM。堆栈大小由-Xss。每个线程的默认值是1M,但幸运的是事情并没有那么糟糕。操作系统懒惰地分配内存页面,即在第一次使用时,因此实际内存使用量将低得多(通常每个线程堆栈80-200 KB)。我写了一个 脚本 来估计RSS属于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 。

原文  https://www.jdon.com/50904
正文到此结束
Loading...