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

news/2024/5/14 8:20:14/文章来源:https://blog.csdn.net/qq_44590469/article/details/129611548

第26讲 | 如何监控和诊断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 的内部结构;第二,具体到特定内存区域,应该使用什么工具或者特性去定位,可以用什么参数调整。

对于 JConsole 等工具的使用细节,我在专栏里不再赘述,如果你还没有接触过,你可以参考JConsole 官方教程(https://docs.oracle.com/javase/7/docs/technotes/guides/management/jconsole.html)。我这里特别推荐Java Mission Control(JMC)(https://www.oracle.com/java/technologies/jdk-mission-control.html),这是一个非常强大的工具,不仅仅能够使用JMX(https://en.wikipedia.org/wiki/Java_Management_Extensions)进行普通的管理、监控任务,还可以配合Java Flight Recorder(JFR)(https://docs.oracle.com/javacomponents/jmc-5-4/jfr-runtime-guide/about.htm#JFRUH171)技术,以非常低的开销,收集和分析 JVM 底层的 Profiling 和事件等信息。目前, Oracle 已经将其开源,如果你有兴趣请可以查看 OpenJDK 的Mission Control项目(https://openjdk.org/projects/jmc/)。

关于内存监控与诊断,我会在知识扩展部分结合 JVM 参数和特性,尽量从庞杂的概念和 JVM 参数选项中,梳理出相对清晰的框架:

细化对各部分内存区域的理解,堆内结构是怎样的?如何通过参数调整?

堆外内存到底包括哪些部分?具体大小受哪些因素影响?

知识扩展

今天的分析,我会结合相关 JVM 参数和工具,进行对比以加深你对内存区域更细粒度的理解。

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

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

在这里插入图片描述

你可以看到,按照通常的 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 里分配一块儿。

在这里插入图片描述

  1. 老年代

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

  2. 永久代

    这部分就是早期 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 实现了复杂的适应策略,如果你有兴趣可以参考这篇说明。(https://blogs.oracle.com/jonthecollector/the-real-thing)

不知道你有没有注意到,我在年代视角的堆结构示意图也就是第一张图中,还标记出了 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 本地内存使用:

第一部分非常明显是 Java 堆,我已经分析过使用什么参数调整,不再赘述。

第二部分是 Class 内存占用,它所统计的就是 Java 类元数据所占用的空间,JVM 可以通过类似下面的参数调整其大小:

-XX:MaxMetaspaceSize=value

对于本例,因为 HelloWorld 没有什么用户类库,所以其内存占用主要是启动类加载器(Bootstrap)加载的核心类库。你可以使用下面的小技巧,调整启动类加载器元数据区,这主要是为了对比以加深理解,也许只有在 hack JDK 时才有实际意义。

-XX:InitialBootClassLoaderMetaspaceSize=30720

下面是 Thread,这里既包括 Java 线程,如程序主线程、Cleaner 线程等,也包括 GC 等本地线程。你有没有注意到,即使是一个 HelloWorld 程序,这个线程数量竟然还有 25。似乎有很多浪费,设想我们要用 Java 作为 Serverless 运行时,每个 function 是非常短暂的,如何降低线程数量呢?如果你充分理解了专栏讲解的内容,对 JVM 内部有了充分理解,思路就很清晰了:JDK 9 的默认 GC 是 G1,虽然它在较大堆场景表现良好,但本身就会比传统的 Parallel GC 或者 Serial GC 之类复杂太多,所以要么降低其并行线程数目,要么直接切换 GC 类型;JIT 编译默认是开启了 TieredCompilation 的,将其关闭,那么 JIT 也会变得简单,相应本地线程也会减少。我们来对比一下,这是默认参数情况的输出:

在这里插入图片描述

下面是替换了默认 GC,并关闭 TieredCompilation 的命令行

在这里插入图片描述

得到的统计信息如下,线程数目从 25 降到了 17,消耗的内存也下降了大概 1/3。
在这里插入图片描述

接下来是 Code 统计信息,显然这是 CodeCache 相关内存,也就是 JIT compiler 存储编译热点方法等信息的地方,JVM 提供了一系列参数可以限制其初始值和最大值等,例如:

-XX:InitialCodeCacheSize=value
-XX:ReservedCodeCacheSize=value

你可以设置下列 JVM 参数,也可以只设置其中一个,进一步判断不同参数对 CodeCache 大小的影响。

在这里插入图片描述

在这里插入图片描述

很明显,CodeCache 空间下降非常大,这是因为我们关闭了复杂的 TieredCompilation,而且还限制了其初始大小。

下面就是 GC 部分了,就像我前面介绍的,G1 等垃圾收集器其本身的设施和数据结构就非常复杂和庞大,例如 Remembered Set 通常都会占用 20%~30% 的堆空间。如果我把 GC 明确修改为相对简单的 Serial GC,会有什么效果呢?

使用命令:

-XX:+UseSerialGC

在这里插入图片描述

可见,不仅总线程数大大降低(25 → 13),而且 GC 设施本身的内存开销就少了非常多。据我所知,AWS Lambda 中 Java 运行时就是使用的 Serial GC,可以大大降低单个 function 的启动和运行开销。

Compiler 部分,就是 JIT 的开销,显然关闭 TieredCompilation 会降低内存使用。

其他一些部分占比都非常低,通常也不会出现内存使用问题,请参考官方文档(https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr022.html#BABCBGFA)。唯一的例外就是 Internal(JDK 11 以后在 Other 部分)部分,其统计信息包含着 Direct Buffer 的直接内存,这其实是堆外内存中比较敏感的部分,很多堆外内存 OOM 就发生在这里,请参考专栏第 12 讲的处理步骤。原则上 Direct Buffer 是不推荐频繁创建或销毁的,如果你怀疑直接内存区域有问题,通常可以通过类似 instrument 构造函数等手段,排查可能的问题。

JVM 内部结构就介绍到这里,主要目的是为了加深理解,很多方面只有在定制或调优 JVM 运行时才能真正涉及,随着微服务和 Serverless 等技术的兴起,JDK 确实存在着为新特征的工作负载进行定制的需求。

今天我结合 JVM 参数和特性,系统地分析了 JVM 堆内和堆外内存结构,相信你一定对 JVM 内存结构有了比较深入的了解,在定制 Java 运行时或者处理 OOM 等问题的时候,思路也会更加清晰。JVM 问题千奇百怪,如果你能快速将问题缩小,大致就能清楚问题可能出在哪里,例如如果定位到问题可能是堆内存泄漏,往往就已经有非常清晰的思路和工具(https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/memleaks004.html#CIHIEEFH)可以去解决了。

一课一练

关于今天我们讨论的题目你做到心中有数了吗?今天的思考题是,如果用程序的方式而不是工具,对 Java 内存使用进行监控,有哪些技术可以做到?

请你在留言区写写你对这个问题的思考,我会选出经过认真思考的留言,送给你一份学习奖励礼券,欢迎你与我一起讨论。

ps://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/memleaks004.html#CIHIEEFH)可以去解决了。

一课一练

关于今天我们讨论的题目你做到心中有数了吗?今天的思考题是,如果用程序的方式而不是工具,对 Java 内存使用进行监控,有哪些技术可以做到?

请你在留言区写写你对这个问题的思考,我会选出经过认真思考的留言,送给你一份学习奖励礼券,欢迎你与我一起讨论。

你的朋友是不是也在准备面试呢?你可以“请朋友读”,把今天的题目分享给好友,或许你能帮到他。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_273931.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java栈和队列·下

Java栈和队列下2. 队列(Queue)2.1 概念2.2 实现2.3 相似方法的区别2.4 循环队列3. 双端队列 (Deque)3.1 概念4.java中的栈和队列5. 栈和队列面试题大家好,我是晓星航。今天为大家带来的是 Java栈和队列下 的讲解!😀 继上一个讲完的栈后&…

视听场景理解经典任务

文章目录1. 视听场景理解简介2. 主要任务2.1 Audio-visual Event Localization (AVE) 2.2 Audio-visual Video Parsing (AVVP)2.3 Audio-visual Question Answering (AVQA)2.4 Audio-visual Segmentation (AVS&#xf…

STM32中systick中断的优先级

1、systick中断的优先级 systick为内核外设中断,与普通外设中断的优先级有些区别,并没有抢占优先级和子优先级的说法。 对于M3来说内核外设的中断优先级由内核SCB这个外设的寄存器:SHPRx(x1.2.3)来配置。 内核外设的中…

佳明安夺(Garmin Enduro)续航简单测试

文章目录(一)结论(二)测试条件(2.1)Garmin Connect APP 日历(2.2)具体运动记录(2.3)步数情况(三)补充和探讨(3.1&#xff…

信捷PLC通过EtherCat与松下伺服通讯时的断电重启时会产生巨大异响的Bug原因及解决办法

信捷PLC支持ethercat通讯协议,可以和支持ethercat的从站通讯,像伺服驱动器或IO站点等。 其中,信捷XLH系列PLC在与松下伺服驱动器通讯时,有一个比较严重的问题,就是PLC断电再上电时,有时候会出现bug,这个bug的现象是,使用PLC的指令方式去控制伺服轴动作时,会产生巨大的…

kali内置超好用的代理工具proxychains

作者:Eason_LYC 悲观者预言失败,十言九中。 乐观者创造奇迹,一次即可。 一个人的价值,在于他所拥有的。所以可以不学无术,但不能一无所有! 技术领域:WEB安全、网络攻防 关注WEB安全、网络攻防。…

Mybatis的课程总结

1.mybatis Mybatis主要是对代码进行少写,分别加入核心配置文件和mapper映射文件, 核心配置文件主要是为了连接数据库,mapper映射文件是为了编写sql语句 1.如何配置mybatis ①先创建一个moudle ②然后配置jar包 ③然后进行mybatis的分层 bean…

pcb成型板aoi检测,6种PCB板常用的检测方法

6种PCB板常用的检测方法,主要包括:PCB板人工目测、PCB板在线测试、PCB板功能测试、自动光学检测、自动X光检查、激光检测系统1、PCB板人工目测使用放大镜或校准的显微镜,利用操作人员视觉检查来确定电路板合不合格,并确定什么时候…

我们再次看看 ARB 女巫空投策略,做到知彼知己,不敢说百战不殆

女巫检测选项该项目旨在从 Arbitrum 空投中删除 Sybil 地址,确保只有合法用户才能收到空投代币。方法我们使用链上数据来识别同一用户拥有的相关地址,并使用来自 Nansen、Hop 和 OffChain Labs 的数据删除实体地址,例如网桥、交易所和智能合约…

Verilog学习之触发器与modelsim仿真

目录 一、前言 二、触发器介绍 三、测试文件代码 一、前言 ​ ​本文将学习常见类型触发的verilog编写,结合仿真结果来熟悉。 二、触发器介绍 ​ ​触发器在verilog中的作用主要是具有存储作用,由时钟信号来触发改变存储内容,较常见…

银河麒麟v10系统硬盘挂载

一、查看磁盘 近期由于centos系统停止更新用户服务器要更换银河麒麟v10,拿到服务器后使用lsblk -f或fdisk -l命令查看磁盘名称 可以看到sdb200G就是要挂载的硬盘,还没有uuid需要初始化才可以挂载。 二、分区 分区命令: fdisk /dev/【你的…

【LeetCode每日一题】——面试题17.21.直方图的水量

文章目录一【题目类别】二【题目难度】三【题目编号】四【题目描述】五【题目示例】六【解题思路】七【时间频度】八【代码实现】九【提交结果】一【题目类别】 双指针 二【题目难度】 困难 三【题目编号】 面试题17.21.直方图的水量 四【题目描述】 给定一个直方图(也称…

Java解题--练习解题阶段(无序阶段)-ALGO-1006 拿金币

题目算法训练 拿金币资源限制内存限制:256.0MB C/C时间限制:1.0s Java时间限制:3.0s Python时间限制:5.0s问题描述有一个N x N的方格,每一个格子都有一些金币,只要站在格子里就能拿到里面的金币。你站在最左上角的格子里,每次可以…

什么是装箱?什么是拆箱?装箱和拆箱的执行过程?常见问题?

参考答案 1、什么是装箱?什么是拆箱? 装箱:基本类型转变为包装器类型的过程。 拆箱:包装器类型转变为基本类型的过程。 //JDK1.5之前是不支持自动装箱和自动拆箱的,定义Integer对象,必须 Integer i new…

MATLAB | R2023a更新了哪些好玩的东西

R2023a来啦!!废话不多说看看新版本有啥有趣的玩意和好玩的特性叭!!把绘图放最前面叭,有图的内容看的人多。。 1 区域填充 可以使用xregion及yregion进行区域填充啦!! x -10:0.25:10; y x.^…

<Linux>环境变量

环境变量 文章目录环境变量一、基本概念二、常见环境变量三、查看环境变量的方法四、测试PATH五、测试HOME六、测试SHELL七、环境变量相关的命令八、环境变量的组织方式九、命令行参数十、通过代码获得环境变量十一、通过系统调用获取环境变量十二、环境变量通常是具有全局属性…

Docker简单上手

Docker 笔记 文章目录Docker 笔记[toc]一、Docker简介docker版本docker 架构二、Docker常用命令docker镜像命令docker容器命令提交docker镜像到阿里云仓库搭建私有docker镜像库三、容器数据卷四、阿里云容器部署1.Tomcat部署2.MySQL部署3.Redis部署一、Docker简介 ​ Docker是…

Linux- 系统随你玩之--玩出花活的命令浏览器-双生姐妹花

文章目录1、背景2、命令浏览器-双生姐妹花2.1、姐妹花简介2.2 、验名正身2.3、常用功能选项3、常用实操3.1、发送请求获取文件3.1.1、抓取页面内容到一个文件中3.1.2、多个文件下载3.1.3、下载ftp文件3.1.4、断点续传3.1.5、上传文件3.1.6、内容输出3.2 、利用curl测试接口3.3 …

Oracle导出AWR报告

一、使用root用户登录Linux服务器 二、切换至oracle用户 执行命令:su – oracle,然后回车 三、使用管理员权限连接数据库 执行命令:sqlplus / as sysdba,然后回车 四、生成报告快照 执行脚本:exec DBMS_WORKLOAD_RE…

达梦数据库(dm8)管理工具不会自动提交执行sql的几种方式

大多数数据库管理工具,默认情况下,是开启了自动事务提交的,即执行了一句 Select 、Insert 、 Delete 、Update 之后会自动执行 commit 操作, 但达梦数据库管理工具不会,无论是命令行工具disql,还是可视化管…