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 资源可能造成本地内存泄漏。典型的例子是 ZipInputStream
和 DirectoryStream
。
JVMTI 代理,尤其是 jdwp 调试代理,也会造成内存消耗过多。
参考:
Java 进程中有哪些组件会占用内存? https://zhuanlan.zhihu.com/p/64823255
Last updated