# 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>
