在上一篇博文:【JAVA】JVM 内存区域的划分 中介绍了 JVM 内存区域的划分,总结了相关的一些概念,本博文将结合 JVM 参数、工具等方面,进一步分析 JVM 内存结构,包括外部资料相对较少的堆外部分。
本篇博文的重点是,如何监控和诊断JVM堆内和堆外内存使用?
了解 JVM 内存的方法有很多,具体能力范围也有区别,简单总结如下:
以 JConsole 为例,其内存页面可以显示常见的堆内存和各种堆外部分使用状态。
这里有一个相对特殊的部分,就是是堆外内存中的直接内存,前面的工具基本不适用,可以使用 JDK 自带的 Native Memory Tracking(NMT)特性,它会从 JVM 本地内存分配的角度进行解读。
首先,堆内部是什么结构?
对于堆内存,在上一篇博文:【JAVA】JVM 内存区域的划分 中介绍了最常见的新生代和老年代的划分,其内部结构随着 JVM 的发展和新 GC 方式的引入,可以有不同角度的理解,下图就是年代视角的堆结构示意图。
可以看到,按照通常的 GC 年代方式划分,Java 堆内分为:
新生代是大部分对象创建和销毁的区域,在通常的 Java 应用中,绝大部分对象生命周期都是很短暂的。其内部又分为 Eden 区域,作为对象初始分配的区域;两个 Survivor,有时候也叫 from、to 区域,被用来放置从 Minor GC 中保留下来的对象。
放置长生命周期的对象,通常都是从 Survivor 区域拷贝过来的对象。当然,也有特殊情况,我们知道普通的对象会被分配在 TLAB 上;如果对象较大,JVM 会试图直接分配在 Eden 其他位置上;如果对象太大,完全无法在新生代找到足够长的连续空闲空间,JVM 就会直接分配到老年代。
这部分就是早期 Hotspot JVM 的方法区实现方式了,储存 Java 类元数据、常量池、Intern 字符串缓存,在 JDK 8 之后就不存在永久代这块儿了。
那么,我们如何利用 JVM 参数,直接影响堆和内部区域的大小呢?简单总结一下:
-Xmx value
-Xms value
-XX:NewRatio=value
默认情况下,这个数值是 2,意味着老年代是新生代的 2 倍大;换句话说,新生代是堆大小的 1/3。
-XX:NewSize=value
-XX:SurvivorRatio=value
不知道你有没有注意到,我在年代视角的堆结构示意图也就是第一张图中,还标记出了 Virtual 区域,这是块儿什么区域呢?
在 JVM 内部,如果 Xms 小于 Xmx,堆的大小并不会直接扩展到其上限,也就是说保留的空间(reserved)大于实际能够使用的空间(committed)。当内存需求不断增长的时候,JVM 会逐渐扩展新生代等区域的大小,所以 Virtual 区域代表的就是暂时不可用(uncommitted)的空间。
第二,分析完堆内空间,我们一起来看看 JVM 堆外内存到底包括什么?
在 JMC 或 JConsole 的内存管理界面,会统计部分非堆内存,但提供的信息相对有限,下图就是 JMC 活动内存池的截图。
接下来我会依赖 NMT 特性对 JVM 进行分析,它所提供的详细分类信息,非常有助于理解 JVM 内部实现。
首先来做些准备工作,开启 NMT 并选择 summary 模式,
-XX:NativeMemoryTracking=summary
为了方便获取和对比 NMT 输出,选择在应用退出时打印 NMT 统计信息:
-XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics
然后,执行一个简单的在标准输出打印 HelloWorld 的程序,就可以得到下面的输出:
我来仔细分析一下,NMT 所表征的 JVM 本地内存使用:
-XX:MaxMetaspaceSize=value
对于本例,因为没有什么用户类库,所以其内存占用主要是启动类加载器(Bootstrap)加载的核心类库。你可以使用下面的小技巧,调整启动类加载器元数据区,这主要是为了对比以加深理解,也许只有在 hack JDK 时才有实际意义。
-XX:InitialBootClassLoaderMetaspaceSize=30720
接下来是 Code 统计信息,显然这是 CodeCache 相关内存,也就是 JIT compiler 存储编译热点方法等信息的地方,JVM 提供了一系列参数可以限制其初始值和最大值等,例如:
-XX:InitialCodeCacheSize=value
-XX:ReservedCodeCacheSize=value
你可以设置 JVM 参数,也可以只设置其中一个,进一步判断不同参数对 CodeCache 大小的影响。
以上就是 如何监控和诊断JVM堆内和堆外内存使用? 的所有内容了;
结合 JVM 参数和特性,系统地分析了 JVM 堆内和堆外内存结构,相信你一定对 JVM 内存结构有了比较深入的了解,在定制 Java 运行时或者处理 OOM 等问题的时候,思路也会更加清晰。JVM 问题千奇百怪,如果你能快速将问题缩小,大致就能清楚问题可能出在哪里,例如如果定位到问题可能是堆内存泄漏,往往就已经有非常清晰的思路和工具可以去解决了。