Java进程内存占用

我们查看Java进程占用的内存时,发现总是大于给堆内存分配的大小。这是因为JVM 包括许多子系统,垃圾回收器、类装载器、JIT 编译器等等。所有这些子系统运行都需要占用内存。并且JVM 不是内存唯一的消费者,Java Class Library 在内的所有 Native Library 也会占用内存。对于内存跟踪工具来说这些开销甚至无法跟踪。Java 应用程序本身还可以通过直接 ByteBuffers 使用堆外内存。

那么一个Java进程都有哪些组件会占用系统内存呢?

1. Java堆

它是 Java 对象存在的地方。它会占用 -Xmx 参数指定大小的内存。

2.java栈

线程堆栈也会申请内存。堆栈大小由 -Xss 选项指定,默认每个线程1M,幸运的是情况并非那么糟糕。操作系统会以延迟分配的方式分配内存页面,比如在第一次使用时分配,因此实际使用的内存要低得多,通常每个线程堆栈占用80至200KB。

3. 方法区

类的元数据存储在 Metaspace 堆外区域中,包括方法字节码、符号、常量池、注解等。加载的类越多,使用的元数据就越多。可以通过 -XX:MaxMetaspaceSize(默认无上限)和 -XX:CompressedClassSpaceSize(默认1G)选项控制元数据总大小。

4. 垃圾收集器

GC 需要额外的内存进行堆管理,主要用于 GC 自身的结构与算法。这些结构包括 Mark Bitmap、Mark Stack(遍历对象关系图)、Remembered Set(记录 region 之间引用)等等。其中一些可以直接调优,例如 -XX: MarkStackSizeMax 选项,另一些依赖于堆布局。其中 G1 region (-XX:G1HeapRegionSize)占用内存较大,Remembered Set 占用内存较小。

GC 的内存开销因算法而异,其中 -XX:+UseSerialGC-XX:+UseShenandoahGC 的开销最小,而 G1 或 CMS 则会轻松占用大约10%的堆内存。

5. 代码缓存

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

6. 编译器

JIT 编译器本身工作时也需要内存。可以通过关闭 Tiered Compilation 或者 -XX:CICompilerCount 减少编译使用的线程数。

7. 符号表

JVM 有两个主要的 hashtable:符号表包含名称、签名、标识符等,String 表包含对 interned String 引用。如果 Native Memory Tracking 显示 String 表使用了大量内存,这可能意味着应用程序调用 String.intern 过于频繁。

还有其他 JVM 部件会占用本地内存,但它们在总内存消耗中通常比例不大。

下面说说非JVM占用的内存。

1. Direct Buffer

应用程序可以通过 ByteBuffer.allocateDirect 调用直接请求非堆内存。默认的非堆内存大小限制由 -XX:MaxDirectMemorySize指定,除了 Direct ByteBuffer,还有 MappedByteBuffer 映射到进程虚拟内存中的文件。虽然 Native Memory Tracking 不对它跟踪,但是 MappedByteBuffer 也会占用物理内存,而且没有一种简单的方法限制它申请的内存大小。可以通过查看进程内存映射了解实际的内存使用情况:pmap-x

2. Native Library

System.Loadlibrary 加载的 JNI 代码可以不受 JVM 控制分配堆外内存,标准 Java Class Library 也是如此。尤其是未关闭的 Java 资源可能造成本地内存泄漏。典型的例子是 ZipInputStreamDirectoryStream

JVMTI 代理,尤其是 jdwp 调试代理,也会造成内存消耗过多。

参考:

Java 进程中有哪些组件会占用内存? https://zhuanlan.zhihu.com/p/64823255

Last updated