Java进程内存占用
Last updated
Was this helpful?
Last updated
Was this helpful?
Was this helpful?
我们查看Java进程占用的内存时,发现总是大于给堆内存分配的大小。这是因为JVM 包括许多子系统,垃圾回收器、类装载器、JIT 编译器等等。所有这些子系统运行都需要占用内存。并且JVM 不是内存唯一的消费者,Java Class Library 在内的所有 Native Library 也会占用内存。对于内存跟踪工具来说这些开销甚至无法跟踪。Java 应用程序本身还可以通过直接 ByteBuffers
使用堆外内存。
那么一个Java进程都有哪些组件会占用系统内存呢?
它是 Java 对象存在的地方。它会占用 -Xmx
参数指定大小的内存。
线程堆栈也会申请内存。堆栈大小由 -Xss
选项指定,默认每个线程1M,幸运的是情况并非那么糟糕。操作系统会以延迟分配的方式分配内存页面,比如在第一次使用时分配,因此实际使用的内存要低得多,通常每个线程堆栈占用80至200KB。
类的元数据存储在 Metaspace 堆外区域中,包括方法字节码、符号、常量池、注解等。加载的类越多,使用的元数据就越多。可以通过 -XX:MaxMetaspaceSize
(默认无上限)和 -XX:CompressedClassSpaceSize
(默认1G)选项控制元数据总大小。
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%的堆内存。
代码缓存包含动态生成的代码,JIT 编译生成的方法、解释器以及运行时 stub 代码。代码大小受 -XX:ReservedCodeCacheSize
选项限制(默认为240M)。关闭 -XX:-TieredCompilation
可以减少已编译代码的数量,从而减小代码缓存。
JIT 编译器本身工作时也需要内存。可以通过关闭 Tiered Compilation 或者 -XX:CICompilerCount
减少编译使用的线程数。
JVM 有两个主要的 hashtable:符号表包含名称、签名、标识符等,String 表包含对 interned String 引用。如果 Native Memory Tracking 显示 String 表使用了大量内存,这可能意味着应用程序调用 String.intern 过于频繁。
还有其他 JVM 部件会占用本地内存,但它们在总内存消耗中通常比例不大。
下面说说非JVM占用的内存。
应用程序可以通过 ByteBuffer.allocateDirect 调用直接请求非堆内存。默认的非堆内存大小限制由 -XX:MaxDirectMemorySize指定,除了 Direct ByteBuffer,还有 MappedByteBuffer
映射到进程虚拟内存中的文件。虽然 Native Memory Tracking 不对它跟踪,但是 MappedByteBuffer
也会占用物理内存,而且没有一种简单的方法限制它申请的内存大小。可以通过查看进程内存映射了解实际的内存使用情况:pmap-x
。
System.Loadlibrary
加载的 JNI 代码可以不受 JVM 控制分配堆外内存,标准 Java Class Library 也是如此。尤其是未关闭的 Java 资源可能造成本地内存泄漏。典型的例子是 ZipInputStream
和 DirectoryStream
。
JVMTI 代理,尤其是 jdwp 调试代理,也会造成内存消耗过多。
参考:
Java 进程中有哪些组件会占用内存? https://zhuanlan.zhihu.com/p/64823255