《深入理解Java虚拟机》(五) JVM调优案例
问题
我们公司的程序是的B/S架构,工作中碰到客户提出一个问题,他们的系统最近突然会用着用着就卡死掉--浏览器访问服务器一开始会卡顿,直至最终会完全卡死没有响应。
并且客户反馈的是最近才变卡的,之前一直没有问题,现在一旦系统卡住就需要重启,对正常使用造成了严重影响。
客户的服务器配置如下(应用程序服务器以及数据库服务器都是如下配置)
- 内存:32G
- 磁盘:机械 2 T
- CPT: 两颗4核 CPU
由于我司产品是客户内部部门间使用,所以并发量并不大,上述配置已经完全足够开销。
排查问题经过了如下的过程:
排除是否数据库卡顿造成
一开始虽然就可以确定不是数据库问题(因为不是一直卡顿,而是某段时间间歇性卡顿),还是看了一下Oracle的 awr 报告,得到的结论也确实不是数据库问题。
任务管理器
简单粗暴的直接看任务管理器,这次发现了问题的端倪了,每次系统卡死前,程序服务器的内存、CPU的占用率都接近拉满,而数据库服务器则没有什么变化。
与客户沟通
经过再次与客户沟通,得知最近一段时间,他们大量的部门在使用系统中一个通过excel导入数据的功能。(该功能通过使用HSSFWorkbook达到批量从excel中导入数据),基本上可以确定问题跟使用HSSFWorkbook息息相关。结合内存几乎耗尽,程序无响应的问题,那么极有可能是Full GC时间太久导致了卡顿。(如果收集器的GC 线程 与用户程序线程 串行,当进行GC 时必须停掉所有用户线程,当堆很大的时候就会导致GC时间过久。)
至此开始通过JVM排查问题:
首先要得到JVM日志,我们在客户服务器的tomcat的bin/catalina.bat中添加了如下参数:
set "JAVA_OPTS=-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\DUMP_FILES\"
set "JAVA_OPTS=%JAVA_OPTS% -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:D:\\gc.log"

JVM参数介绍
-XX:+HeapDumpOnOutOfMemoryError //当堆内存溢出自动打印dump日志
-XX:HeapDumpPath=D:\DUMP_FILES\ //dump日志路径
-verbose:gc // 开启GC 日志功能
-XX:+PrintGCDetails // 打印详细GC信息
-XX:+PrintGCDateStamps // GC 日志打印时间戳
-XX:+PrintHeapAtGC //Full GC 前后打印 堆的总览信息
-Xloggc:D:\\gc.log //GC 日志路径
完事具备,让客服重启服务器,然后等待凶手浮出水面,来一个守株待兔。
客户服务器卡死,没有dump日志,但是GC日志 gc.log 已经有内容了,其中部分内容如下截图:
第一次Full GC
老年代一切正常GC 后占用变小,Full GC实际时间0.08秒正常,截图如下:

第二次Full GC截图
发现了问题,实际耗时0.22秒,总的来说正常;需要关注的是,GC 后 老年代总空间变大了,并且占用率也变大;(大量对象进入了老年代)

第三次Full GC 截图
Full GC 实际耗时 0.17秒,正常。回收后老年代从52%变成了16%,正常。

- 分析前三次Full GC:
GC耗时正常,唯一不正常的是三次Full GC间隔时间较短,对比观察第一次Full GC之后和第三次Full GC之后:发现,老年代和新生代总空间大小都发生了变化,这时可以推测这是JVM在自动调整内存。
直至开始出现异常的Full GC
后续几次Full GC 正常,直到第5次Full GC,这次Full GC实际耗时0.59秒,开始异常。

此时时间已经来到了下午15点,对比上午11:30左右发生的Full GC,此时老年代和新生代内存再一次变大了,此次Full GC eden变化不大回收了60M左右空间。
Full GC 后老年代空间再次变大,此时大致可以推论用户操作高峰期到来,大量对象开始向老年代进军。
如下是第6次Full GC,这次Full GC实际耗时1.94秒,异常它来了。

第六次 Full GC 后
eden 约 200 M左右
from space 约 300M
to space 约 300M
Old 约 1 G左右
Full GC耗时1.94秒,这已经不正常了,同时发现此次Full GC后老年代不减反增;再看Full GC发生时间:紧邻第五次Full GC,可知此时收集速度已经慢于内存分配速度,需要触发Full GC 得到足够内存空间进行内存分配。
继续分析:
第七次Full GC到来
相距第六次Full GC时间只过去了16秒,此次Full GC实际耗时2.25秒,此时Full GC开始高频率发生,且造成时间停顿较久:已经达到秒级,可以预见客户说的系统卡死就在即将到来。

最终程序卡死时的Full GC
第69~第71次Full GC,时间间隔极短,且耗时极长(3 ~ 4 秒),此时系统已经卡死,此时老年代占用率已经99%,已经没有空间可以给对象分配内存。

分析
此时用户服务器的32 G内存已经占用了99%,并且已经没有什么回收的余地了。虽然此时老年代和新生代占用内存总和也不超过10个G,那么剩下的20多个G内粗去哪里了呢?为什么内存占用了99%呢,估计是因为HSSFWorkbook 操作 Excel 时 占用了堆外内存(没深究这个,有懂行的小伙伴望不吝赐教)。
可以得到的结论是,当对象分配速度过快,GC收集的速度已经跟不上内存分配的速度,老年代很快被占满,这样就会导致频繁的Full GC; 另外由于Java堆不断自行拓展占用内存大小, 最后造成堆占用空间越来越大,而堆越大导致Full GC时间越来越久,如此形成恶性循环停顿时间越来越长;直至最终,服务器内存被占满,Full GC无法回收内存空间,程序卡死。
结合分析,那么需要做的就是事情大致围绕两个核心:
- 选择 并发 方式 的 GC 收集器(CMS 、G1),减少STW操作(Stop The Word);
- JVM调优,尽量避免发生Full GC;
处理方法
拟处理方案如下:
1.服务器扩大内存为64G 32核
- 客户财大气粗不愁资源
2.修改JVM默认的垃圾收集器(Parallel GC),改为G1 或者 CMS。
- 如果选用G1收集器, 参数设置如下
#开启G1
-XX:+UseG1GC
#指定堆大小最大为6个G
-Xmx6144m
# 设置region大小2的24次方(16M) 可选范围:1M ~ 32 M
-XX:G1HeapRegionSize=16777216
# STW (stop the word) 工作线程数 STW_Thread_num = num_of_cup > 8 ? 5/num_of_cup : num_of_cup; (三目运算符不解释)
-XX:ParallelGCThreads=20 ###客户服务器CPU逻辑处理器数:32 * 5 / 8 = 20
# 设置并行标记的线程数 : num of STW_Thread_num / 4
-XX:ConcGCThreads=5 ##### 20/4 = 5
# 触发标记周期的堆占用率阈值:当达到80% 时触发一次Mixed GC
-XX:InitiatingHeapOccupancyPercent=60
- 假如选用CMS 收集器,参数如下:
# 启用 CMS
-XX:+UseConcMarkSweepGC
# 指定堆大小最大为6个G
-Xmx6144m
# 指定 新生代和老年代大小比例 1:2 CMS可以使用,但是G1 不适用
# 如果G1 中指定新生代和老年代比例,会对G1的时间停顿模型产生破坏
-XX:NewRatio=2
# 年轻代为并行收集
-XX:+UseParNewGC
# 降低标记停顿
-XX:+CMSParallelRemarkEnabled
3. 针对CMS 收集器
由于大量的HSSFWorkbook对象进入了老年代,那么肯定带来了大量的跨代引用(新生代对象和老年代对象之间的相互引用),那么此时需要如下指令,强制每次清理前,先进行一次新生代的收集,那么在标记阶段的STW操作耗时必将大大改善。
# 注意,只能CMS 收集器使用
-XX:+ScavengeBeforeFullGC
-XX:+CMSScavengeBeforeRemark
4. 结合GC 日志,我们还可以延长对象在eden驻留时间,减小老年代压力
# 通用指令 CMS 和 G1 都可使用
-XX:MaxTenuringThreshold=15
处理原因
或许有人就要问了,为什么要改垃圾收集器,只加内存不行么?
Parallel 收集器,用户线程与GC线程串行,收集时用户线程必须停下,当老年代不断膨胀,那么收集的时间不断变长就造成了恶果。
最终我们选择了G1 收集器。
了解G1的小伙伴应该知道,G1收集的步骤是:
A( 初始标记 )
B( 并发标记 )
C( 最终标记 )
D( 筛选回收 )
A -->B -->C-->D
其中耗时的操作都是并发进行的,并不会停掉用户线程;众所周知,G1 收集器追寻的是程序的最小停顿,所以用在此处挺好。至于G1会占用JVM 10~20%的内存,那么为什么不用CMS收集,因为客户有钱啊(滑稽),后续客户把程序服务器配置加到了64G 32核心。小伙伴碰到类似问题可以试试 CMS 是否也有奇效。
最终,换成G1收集器后,用户程序逐渐正常,得到了如下GC 日志:
G1 堆内存 约 1G 毫无压力,可以关注截图,最后一行的Time: user 主要与服务器CPU收集相关暂不关注,主要看的是,real = 0.05 secs ,0.05 秒 :这个才是咱要看到的效果。

G1 堆内存2G+,依旧无压力,real = 0.03 secs

最终
后续从GC 日志里也没发现别的异常,问题基本解决。G1收集器的日志里没发现Full GC记录(在G1垃圾收集器中,最好的优化状态就是通过不断调整分区空间,避免进行full gc,可以大幅减少延时),并且G1堆内存峰值也就在2G左右,那么可以得到的结论是:或许不用拓展硬件,G1收集器再适当调优就能解决该问题。
写在 2023-10-14
不知不觉中两年多过去,再回过头来反刍的时候,还是发现了曾经的不成熟;
其实文中分析的原因都对,就是 大量导入 excel 导致分配内存过快,并且如果文件较大、客户端并发较高时,很容易就内存溢出了,毕竟 HSSFWorkbook 读取 excel 是要读取文件到内存中的。
如果了解 NIO 的话,就知道前文提到的 20G 内存去哪了,答案就是 <直接内存> 它不受堆管辖。
文中提到的解决之道其实也尚可, 加大内存、使用G1 可以解决燃眉之急,但是治标不治本,随着并发量持续上升,同样有耗尽的时候。
正确的姿势应当是,优化 HSSFWorkbook 读取excel的部分,应当使用 CSV 支持按行读取,而非全部载入内存 <直接内存>,这样可能读取效率变低了,但是起码可以最有效的避免内存溢出。
《深入理解Java虚拟机》(五) JVM调优案例的更多相关文章
- 深入理解Java虚拟机(六)——JVM调优分析与实战
大内存硬件上的程序部署策略 单个虚拟机管理大内存 出现问题 如果JVM中的堆内存太小,就会频繁地出发GC,而每次GC会将用户线程暂停,所以,频繁地GC会导致长时间的停顿.如果扩大计算的内存的大小,就能 ...
- java虚拟机学习-JVM调优总结-分代垃圾回收详述(9)
为什么要分代 分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的.因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率. 在Java程序运行的过程中,会产生大量的对象, ...
- java虚拟机学习-JVM调优总结-调优方法(12)
JVM调优工具 Jconsole,jProfile,VisualVM Jconsole : jdk自带,功能简单,但是可以在系统有一定负荷的情况下使用.对垃圾回收算法有很详细的跟踪.详细说明参考这里 ...
- 【java虚拟机】jvm调优原则
转自:https://www.cnblogs.com/xiaopaipai/p/10522794.html 合理规划jvm性能调优 JVM性能调优涉及到方方面面的取舍,往往是牵一发而动全身,需要全盘考 ...
- java虚拟机学习-JVM调优总结-新一代的垃圾回收算法(11)
垃圾回收的瓶颈 传统分代垃圾回收方式,已经在一定程度上把垃圾回收给应用带来的负担降到了最小,把应用的吞吐量推到了一个极限.但是他无法解决的一个问题,就是Full GC所带来的应用暂停.在一些对实时性要 ...
- java虚拟机学习-JVM调优总结-典型配置举例(10)
以下配置主要针对分代垃圾回收算法而言. 堆大小设置 年轻代的设置很关键 JVM中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制:系统的可用虚拟内存限制:系统的可用物理 ...
- java虚拟机学习-JVM调优总结(5)
数据类型 Java虚拟机中,数据类型可以分为两类:基本类型和引用类型.基本类型的变量保存原始值,即:他代表的值就是数值本身:而引用类型的变量保存引用值.“引用值”代表了某个对象的引用,而不是对象本身, ...
- 【java虚拟机】jvm调优
转自:https://www.cnblogs.com/starhu/p/6400348.html?utm_source=itdadao&utm_medium=referral 堆大小设置JVM ...
- java虚拟机学习-JVM调优总结(6)
1.Java对象的大小 基本数据的类型的大小是固定的,这里就不多说了.对于非基本类型的Java对象,其大小就值得商榷. 在Java中,一个空Object对象的大小是8byte,这个大小只是保存堆中一个 ...
- java虚拟机学习-JVM调优总结-垃圾回收面临的问题(8)
如何区分垃圾 上面说到的“引用计数”法,通过统计控制生成对象和删除对象时的引用数来判断.垃圾回收程序收集计数为0的对象即可.但是这种方法无法解决循环引用.所以,后来实现的垃圾判断算法中,都是从程序运行 ...
随机推荐
- [转帖]oracle ZHS16GBK的数据库导入到字符集为AL32UTF8的数据库(转载+自己经验总结)
字符集子集向其超集转换是可行的,如此例 ZHS16GBK转换为AL32UTF8. 导出使用的字符集将会记录在导出文件中,当文件导入时,将会检查导出时使用的字符集设置,如果这个字符集不同于导入客户端的N ...
- [转帖]细说ASCII、GB2312/GBK/GB18030、Unicode、UTF-8/UTF-16/UTF-32编码
参考: <编码标准-GB2312 GBK GB18030> <字符编码笔记:ASCII,Unicode 和 UTF-8> <字体编辑用中日韩汉字Unicode编码表> ...
- [转帖]Prometheus 监控之 Blackbox_exporter黑盒监测 [icmp、tcp、http(get\post)、dns、ssl证书过期时间]
Blackbox_exporter 主动监测主机与服务状态 Prometheus 官方提供的 exporter 之一,可以提供 http.dns.tcp.icmp 的监控数据采集 官方github: ...
- [转帖]被误解的CPU利用率、超线程、动态调频 —— CPU 性能之迷 Part 1
https://blog.mygraphql.com/zh/notes/hw/hyper-threading/ 引 性能测试.压力测试.业务系统性能容量评估.这 3 件事,可以认为是大部分程序员/软件 ...
- [转帖]Shell编程之免交互
目录 交互的概念与Linux中的运用 Here Document 免交互 tee命令重定向输出加标准输出 支持变量替换 多行注释 Expect 实例操作 免交互预设值修改用户密码 创建用户并设置密码 ...
- 为什么Kubernetes和容器与机器学习密不可分?
原文出自infosecurity 作者:Rebecca James 京东云开发者社区编译 当前,数字化转型的热潮在IT领域发展的如火如荼,越来越多的企业投身其中,机器学习和人工智能等现代技术的融合在公 ...
- Windows 核心编程笔记 [1] Windows 错误处理
[1] Windows 错误处理 1. 关于windows系统函数的返回值错误处理 VOID:这个函数不可能失败 BOOL:如果函数调用失败,返回值为0,即为FALSE,否则为非0值,即为TRUE H ...
- 从零开始配置 vim(11)——插件管理
之前我们介绍了基础配置部分和快捷键配置部分.如果你配置了这两个部分,vim已经算是比较好用了.但是作为代码编辑器来讲还是显的比较简陋,用这些配置来完成日常的编码任务会显得力不从心.vim比较强大的一点 ...
- 【主流技术】实战之 Spring Boot 中集成微信支付(小程序)
前言 微信支付是企业级项目中经常使用到的功能,作为后端开发人员,完整地掌握该技术是十分有必要的. 以下是经过真实商业项目实践的集成步骤,包括注册流程.调用过程.代码demo(经过脱敏)等,希望我的分享 ...
- SqlSugar的查询函数SqlFunc
用法 我们可以使用SqlFunc这个类调用Sql函数,用法如下: db.Queryable<Student>().Where(it => SqlFunc.ToLower(it.Nam ...