JVM学习笔记(6) 第二部分 自动内存管理 第5章节 调优案例分析与实战

张开发
2026/4/8 9:13:09 15 分钟阅读

分享文章

JVM学习笔记(6) 第二部分 自动内存管理 第5章节 调优案例分析与实战
文章目录第5章调优案例分析与实战5.0 个人感悟5.1 概述5.2 案例分析5.2.1 高性能硬件上的程序部署策略5.2.2 集群间同步导致的内存溢出5.2.3 堆外内存导致的溢出错误5.2.4 外部命令导致系统缓慢5.2.5 服务器JVM进程崩溃5.2.6 不恰当数据结构导致内存占用过大5.2.7 由Windows虚拟内存导致的长时间停顿5.2.8 安全点导致长时间停顿5.3 实战Eclipse运行速度调优5.4 常用调优参数5.5 本章小结第5章调优案例分析与实战5.0 个人感悟理论要结合实践书中很多案例可能比较早了但是还是能提供很多思路jdk也越来越新掌握知识工作中多实践JVM问题的解决不是玄学都是是基于数据和逻辑的诊断5.1 概述目的将前面章节的理论内存模型、垃圾回收、监控工具应用到实际问题的解决中内容8个来自真实生产环境的典型案例实战Eclipse运行速度调优有兴趣可以看看原文5.2 案例分析5.2.1 高性能硬件上的程序部署策略场景一个15万PV/天的文档网站硬件升级为4个CPU、16GB内存64位JDK 1.5堆内存设为12GB。升级后网站经常不定期出现长时间失去响应原因GC停顿导致。使用吞吐量优先收集器一次Full GC停顿高达14秒。访问文档时产生大量大对象直接进入老年代内存迅速被消耗解决方案采用逻辑集群方案建立5个32位JDK的逻辑集群每个堆固定1.5GB前端用Apache做负载均衡并改为CMS收集器要点大内存部署需控制Full GC频率64位JDK存在指针膨胀、性能略低、dump难以分析等问题5.2.2 集群间同步导致的内存溢出场景一个基于B/S的MIS系统6个节点的亲合式集群。使用JBossCache构建全局缓存实现节点间数据共享最近频繁出现内存溢出原因服务端有一个安全校验全局Filter每次请求都更新最后操作时间并同步到所有节点。当网络传输不满足要求时JGroups协议栈中的NAKACK重发数据在内存中不断堆积解决方案改进实现方式避免频繁的集群间同步操作或升级JBossCache版本要点集群缓存同步需考虑网络状况频繁写操作会带来很大的网络同步开销5.2.3 堆外内存导致的溢出错误场景一个学校考试系统运行在32位Windows系统4GB内存。使用CometD 1.1.1框架做服务器推送堆内存调到1.6GB后仍不定时抛出OOM且不产生heapdump文件原因CometD框架大量使用NIO操作需要直接内存堆外内存。32位Windows进程最大内存2GB堆占用1.6GB后剩余空间不足以支撑直接内存分配且直接内存只能等Full GC时“顺便”回收解决方案通过-XX:MaxDirectMemorySize限制堆外内存上限或升级到64位JDK。要点除堆和方法区外直接内存、线程栈、Socket缓存区、JNI代码等也会占用较多内存总和受操作系统进程最大内存限制5.2.4 外部命令导致系统缓慢场景一个数字校园应用系统运行在4 CPU的Solaris 10上GlassFish中间件。大并发压力测试时请求响应慢mpstat显示CPU使用率很高但占用CPU资源的并非应用本身原因最消耗CPU资源的是“fork”系统调用。每个用户请求的处理都会通过Runtime.getRuntime().exec()执行外部shell脚本获取系统信息。频繁调用时创建进程的开销非常可观解决方案去掉Shell脚本执行语句改用Java API获取信息后系统很快恢复正常要点Runtime.getRuntime().exec()会先克隆当前进程再执行外部命令频繁调用会消耗大量CPU和内存5.2.5 服务器JVM进程崩溃场景与案例5.2.2相同的MIS系统。正常运行一段时间后频繁出现JVM进程自动关闭留下hs_err_pid###.log文件原因系统最近与OA门户做了集成待办事项变化时通过Web服务同步。OA系统接口响应慢长达3分钟虽然使用了异步调用但累积的未完成Web服务越来越多导致等待的线程和Socket连接超出虚拟机承受能力最终进程崩溃解决方案通知OA门户方修复接口并将异步调用改为生产者/消费者模式的消息队列实现要点异步调用不能解决速度不对等问题消息队列可削峰填谷避免资源累积5.2.6 不恰当数据结构导致内存占用过大场景一个后台RPC服务器64位JDK堆内存4~8GB新生代1GB使用ParNewCMS。平时Minor GC约30毫秒但每10分钟加载一个80MB数据文件到内存分析形成超过100万个HashMapLong,LongEntry此时Minor GC停顿超过500毫秒原因分析数据文件期间Eden空间被大量存活对象填满。ParNew使用复制算法存活对象过多时复制开销沉重。根本原因是HashMapLong,Long的空间效率太低——有效数据仅16字节实际占用约88字节效率约18%解决方案GC调优角度去掉Survivor空间-XX:SurvivorRatio65536、-XX:MaxTenuringThreshold0让存活对象在第一次Minor GC后直接进入老年代治本方案修改程序使用更高效的数据结构如Trove、Eclipse Collections或直接用long原始类型的数组要点HashMapLong,Long存储基本类型数据时对象头和指针的开销远大于有效数据空间效率极低。5.2.7 由Windows虚拟内存导致的长时间停顿场景一个带心跳检测的GUI桌面程序每15秒发送一次心跳信号。程序上线后心跳检测有误报日志显示程序偶尔会间隔约一分钟无日志输出处于停顿状态原因GC停顿导致。GC日志显示真正执行GC的时间不长但从准备开始GC到真正开始GC之间消耗了绝大部分时间。程序最小化时工作内存被操作系统交换到磁盘页面文件中GC时需要恢复页面文件导致异常停顿解决方案加入-Dsun.awt.keepWorkingSetOnMinimizetrue参数保证程序恢复最小化时工作内存不被交换到磁盘。VisualVM等AWT程序也使用此参数要点桌面GUI程序最小化时内存可能被交换到磁盘导致恢复时GC停顿异常增加。5.2.8 安全点导致长时间停顿场景一个离线HBase集群JDK 8使用G1收集器设置了-XX:MaxGCPauseMillis500最大暂停时间500毫秒。运行后发现GC停顿经常超过3秒且实际GC动作只占其中几百毫秒原因安全点等待耗时过长。添加-XX:PrintSafepointStatistics参数后日志显示有两个线程特别慢导致GC线程长时间自旋等待。排查发现HBase的RpcServer线程中有一个连接超时清理函数循环索引是int类型——HotSpot默认不会在可数循环Counted Loop中放置安全点当垃圾收集发生时必须等待该循环全部跑完才能进入安全点解决方案将循环索引的数据类型从int改为long使循环变成不可数循环强制插入安全点问题得以解决要点HotSpot默认使用int或更小范围的循环索引不会放置安全点可数循环若单次执行很慢仍会导致长时间等待JDK 8下-XX:UseCountedLoopSafepoints参数有Bug。5.3 实战Eclipse运行速度调优场景Eclipse启动缓慢约15秒GC时间、类加载时间、JIT编译时间占用了大量用户程序时间且使用过程中经常有不时的停顿感[reference:31]。调优手段升级JDK版本JDK 6比JDK 5有约15%的性能提升[reference:32]。调整堆内存分配参数新生代/老年代比例。调整编译线程数等参数。要点版本升级可带来“免费的”性能提升通过VisualVM和VisualGC插件采集运行数据量化对比调优前后效果。5.4 常用调优参数目标参数示例设置堆大小-Xms4g -Xmx4g新生代大小-Xmn2g堆外内存限制-XX:MaxDirectMemorySize512mOOM时生成dump-XX:HeapDumpOnOutOfMemoryError打印GC停顿时间-XX:PrintGCApplicationStoppedTime -XX:PrintGCDateStamps -Xloggc:gc.log打印安全点统计-XX:PrintSafepointStatistics -XX:PrintSafepointStatisticsCount1安全点超时检测-XX:SafepointTimeout -XX:SafepointTimeoutDelay2000Windows GUI内存保持-Dsun.awt.keepWorkingSetOnMinimizetrue去掉Survivor区治标-XX:SurvivorRatio65536 -XX:MaxTenuringThreshold0最大GC停顿目标G1-XX:MaxGCPauseMillis2005.5 本章小结调优前先确认“需要调优”以量化指标RT、TP99、QPS、GC时间占比为准先排查业务代码再调整JVM参数大多数性能问题源于低效代码如频繁调用外部命令、使用低效数据结构、不合理的集群同步善用监控工具GC日志分析工具GCViewer、GCEasy、VisualVM VisualGC、Arthas等安全点问题容易被忽视注意可数循环中可能导致的长时间停顿32位系统的内存限制进程最大内存通常为2GB需综合考虑堆、直接内存、线程栈、Socket缓存等开销

更多文章