【JAVA】如何监控和诊断JVM堆内和堆外内存使用?

本文涉及的产品
文本翻译,文本翻译 100万字符
图片翻译,图片翻译 100张
文档翻译,文档翻译 1千页
简介: 本博文将结合 JVM 参数、工具等方面,进一步分析 JVM 内存结构,包括外部资料相对较少的堆外部分。本篇博文的重点是,如何监控和诊断JVM堆内和堆外内存使用?

前言

在上一篇博文:【JAVA】JVM 内存区域的划分 中介绍了 JVM 内存区域的划分,总结了相关的一些概念,本博文将结合 JVM 参数、工具等方面,进一步分析 JVM 内存结构,包括外部资料相对较少的堆外部分。

本篇博文的重点是,如何监控和诊断JVM堆内和堆外内存使用?

概述

了解 JVM 内存的方法有很多,具体能力范围也有区别,简单总结如下:

  • 可以使用综合性的图形化工具,如 JConsole、VisualVM(注意,从 Oracle JDK 9 开始,VisualVM 已经不再包含在 JDK 安装包中)等。这些工具具体使用起来相对比较直观,直接连接到 Java 进程,然后就可以在图形化界面里掌握内存使用情况。

以 JConsole 为例,其内存页面可以显示常见的堆内存各种堆外部分使用状态。

  • 也可以使用命令行工具进行运行时查询,如 jstat 和 jmap 等工具都提供了一些选项,可以查看堆、方法区等使用数据。
  • 或者,也可以使用 jmap 等提供的命令,生成堆转储(Heap Dump)文件,然后利用 jhat 或 Eclipse MAT 等堆转储分析工具进行详细分析。
  • 如果你使用的是 Tomcat、Weblogic 等 Java EE 服务器,这些服务器同样提供了内存管理相关的功能。
  • 另外,从某种程度上来说,GC 日志等输出,同样包含着丰富的信息。

这里有一个相对特殊的部分,就是是堆外内存中的直接内存,前面的工具基本不适用,可以使用 JDK 自带的 Native Memory Tracking(NMT)特性,它会从 JVM 本地内存分配的角度进行解读。

正文

首先,堆内部是什么结构?

对于堆内存,在上一篇博文:【JAVA】JVM 内存区域的划分 中介绍了最常见的新生代和老年代的划分,其内部结构随着 JVM 的发展和新 GC 方式的引入,可以有不同角度的理解,下图就是年代视角的堆结构示意图。

image.png

可以看到,按照通常的 GC 年代方式划分,Java 堆内分为:

  1. 新生代

新生代是大部分对象创建和销毁的区域,在通常的 Java 应用中,绝大部分对象生命周期都是很短暂的。其内部又分为 Eden 区域,作为对象初始分配的区域;两个 Survivor,有时候也叫 from、to 区域,被用来放置从 Minor GC 中保留下来的对象。

  • JVM 会随意选取一个 Survivor 区域作为 “to”,然后会在 GC 过程中进行区域间拷贝,也就是将 Eden 中存活下来的对象和 from 区域的对象,拷贝到这个 “to” 区域。这种设计主要是为了防止内存的碎片化,并进一步清理无用对象。
  • 从内存模型而不是垃圾收集的角度,对 Eden 区域继续进行划分,Hotspot JVM 还有一个概念叫做 Thread Local Allocation Buffer(TLAB),据我所知所有 OpenJDK 衍生出来的 JVM 都提供了 TLAB 的设计。这是 JVM 为每个线程分配的一个私有缓存区域,否则,多线程同时分配内存时,为避免操作同一地址,可能需要使用加锁等机制,进而影响分配速度,你可以参考下面的示意图。从图中可以看出,TLAB 仍然在堆上,它是分配在 Eden 区域内的。其内部结构比较直观易懂,start、end 就是起始地址,top(指针)则表示已经分配到哪里了。所以我们分配新对象,JVM 就会移动 top,当 top 和 end 相遇时,即表示该缓存已满,JVM 会试图再从 Eden 里分配一块儿。

image.png

  1. 老年代

放置长生命周期的对象,通常都是从 Survivor 区域拷贝过来的对象。当然,也有特殊情况,我们知道普通的对象会被分配在 TLAB 上;如果对象较大,JVM 会试图直接分配在 Eden 其他位置上;如果对象太大,完全无法在新生代找到足够长的连续空闲空间,JVM 就会直接分配到老年代。

  1. 永久代

这部分就是早期 Hotspot JVM 的方法区实现方式了,储存 Java 类元数据、常量池、Intern 字符串缓存,在 JDK 8 之后就不存在永久代这块儿了。

那么,我们如何利用 JVM 参数,直接影响堆和内部区域的大小呢?简单总结一下:

  • 最大堆体积 -Xmx value
  • 初始的最小堆体积 -Xms value
  • 老年代和新生代的比例 -XX:NewRatio=value

默认情况下,这个数值是 2,意味着老年代是新生代的 2 倍大;换句话说,新生代是堆大小的 1/3。

  • 当然,也可以不用比例的方式调整新生代的大小,直接指定下面的参数,设定具体的内存大小数值。 -XX:NewSize=value
  • Eden 和 Survivor 的大小是按照比例设置的,如果 SurvivorRatio 是 8,那么 Survivor 区域就是 Eden 的 1/8 大小,也就是新生代的 1/10,因为 YoungGen=Eden + 2*Survivor,JVM 参数格式是 -XX:SurvivorRatio=value
  • TLAB 当然也可以调整,JVM 实现了复杂的适应策略,如果你有兴趣可以参考这篇说明

不知道你有没有注意到,我在年代视角的堆结构示意图也就是第一张图中,还标记出了 Virtual 区域,这是块儿什么区域呢?

在 JVM 内部,如果 Xms 小于 Xmx,堆的大小并不会直接扩展到其上限,也就是说保留的空间(reserved)大于实际能够使用的空间(committed)。当内存需求不断增长的时候,JVM 会逐渐扩展新生代等区域的大小,所以 Virtual 区域代表的就是暂时不可用(uncommitted)的空间。

第二,分析完堆内空间,我们一起来看看 JVM 堆外内存到底包括什么?

在 JMC 或 JConsole 的内存管理界面,会统计部分非堆内存,但提供的信息相对有限,下图就是 JMC 活动内存池的截图。

image.png

接下来我会依赖 NMT 特性对 JVM 进行分析,它所提供的详细分类信息,非常有助于理解 JVM 内部实现。

首先来做些准备工作,开启 NMT 并选择 summary 模式,

-XX:NativeMemoryTracking=summary

为了方便获取和对比 NMT 输出,选择在应用退出时打印 NMT 统计信息:

-XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics

然后,执行一个简单的在标准输出打印 HelloWorld 的程序,就可以得到下面的输出:

image.png

我来仔细分析一下,NMT 所表征的 JVM 本地内存使用:

  • 第一部分非常明显是 Java 堆,我已经分析过使用什么参数调整,不再赘述。
  • 第二部分是 Class 内存占用,它所统计的就是 Java 类元数据所占用的空间,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 问题千奇百怪,如果你能快速将问题缩小,大致就能清楚问题可能出在哪里,例如如果定位到问题可能是堆内存泄漏,往往就已经有非常清晰的思路和工具可以去解决了。

📝 上篇精讲: 【JAVA】Java 内存模型中的 happen-before
💖 我是  𝓼𝓲𝓭𝓲𝓸𝓽,期待你的关注;
👍 创作不易,请多多支持;
🔥 系列专栏:  面试精讲 JAVA
目录
相关文章
|
3月前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
576 1
|
2月前
|
存储 Java 程序员
【JVM】——JVM运行机制、类加载机制、内存划分
JVM运行机制,堆栈,程序计数器,元数据区,JVM加载机制,双亲委派模型
|
2月前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
3月前
|
Arthas Prometheus 监控
监控堆外使用JVM工具
监控堆外使用JVM工具
61 7
|
3月前
|
Java
JVM内存参数
-Xmx[]:堆空间最大内存 -Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的 -Xmn[]:新生代的最大内存 -xx[use 垃圾回收器名称]:指定垃圾回收器 -xss:设置单个线程栈大小 一般设堆空间为最大可用物理地址的百分之80
|
3月前
|
存储 监控 Java
合理设置JVM堆大小
合理设置JVM堆大小
71 4
|
3月前
|
Java
JVM运行时数据区(内存结构)
1)虚拟机栈:每次调用方法都会在虚拟机栈中产生一个栈帧,每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后释放栈帧 (2)本地方法栈:为native修饰的本地方法提供的空间,在HotSpot中与虚拟机合二为一 (3)程序计数器:保存指令执行的地址,方便线程切回后能继续执行代码
34 3
|
3月前
|
存储 缓存 监控
Elasticsearch集群JVM调优堆外内存
Elasticsearch集群JVM调优堆外内存
69 1
|
3月前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
4月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
160 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS