文章目录
- 第5章:调优案例分析与实战
-
- [5.0 个人感悟](#5.0 个人感悟)
- [5.1 概述](#5.1 概述)
- [5.2 案例分析](#5.2 案例分析)
-
- [5.2.1 高性能硬件上的程序部署策略](#5.2.1 高性能硬件上的程序部署策略)
- [5.2.2 集群间同步导致的内存溢出](#5.2.2 集群间同步导致的内存溢出)
- [5.2.3 堆外内存导致的溢出错误](#5.2.3 堆外内存导致的溢出错误)
- [5.2.4 外部命令导致系统缓慢](#5.2.4 外部命令导致系统缓慢)
- [5.2.5 服务器JVM进程崩溃](#5.2.5 服务器JVM进程崩溃)
- [5.2.6 不恰当数据结构导致内存占用过大](#5.2.6 不恰当数据结构导致内存占用过大)
- [5.2.7 由Windows虚拟内存导致的长时间停顿](#5.2.7 由Windows虚拟内存导致的长时间停顿)
- [5.2.8 安全点导致长时间停顿](#5.2.8 安全点导致长时间停顿)
- [5.3 实战:Eclipse运行速度调优](#5.3 实战:Eclipse运行速度调优)
- [5.4 常用调优参数](#5.4 常用调优参数)
- [5.5 本章小结](#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),使用ParNew+CMS。平时Minor GC约30毫秒,但每10分钟加载一个80MB数据文件到内存分析,形成超过100万个HashMap<Long,Long> Entry,此时Minor GC停顿超过500毫秒
原因 :分析数据文件期间,Eden空间被大量存活对象填满。ParNew使用复制算法,存活对象过多时复制开销沉重。根本原因是HashMap<Long,Long>的空间效率太低------有效数据仅16字节,实际占用约88字节,效率约18%
解决方案 (GC调优角度):去掉Survivor空间(-XX:SurvivorRatio=65536、-XX:MaxTenuringThreshold=0),让存活对象在第一次Minor GC后直接进入老年代
治本方案 :修改程序,使用更高效的数据结构(如Trove、Eclipse Collections,或直接用long原始类型的数组)
要点 :HashMap<Long,Long>存储基本类型数据时,对象头和指针的开销远大于有效数据,空间效率极低。
5.2.7 由Windows虚拟内存导致的长时间停顿
场景:一个带心跳检测的GUI桌面程序,每15秒发送一次心跳信号。程序上线后心跳检测有误报,日志显示程序偶尔会间隔约一分钟无日志输出,处于停顿状态
原因:GC停顿导致。GC日志显示真正执行GC的时间不长,但从准备开始GC到真正开始GC之间消耗了绝大部分时间。程序最小化时,工作内存被操作系统交换到磁盘页面文件中,GC时需要恢复页面文件导致异常停顿
解决方案 :加入-Dsun.awt.keepWorkingSetOnMinimize=true参数,保证程序恢复最小化时工作内存不被交换到磁盘。VisualVM等AWT程序也使用此参数
要点:桌面GUI程序最小化时内存可能被交换到磁盘,导致恢复时GC停顿异常增加。
5.2.8 安全点导致长时间停顿
场景 :一个离线HBase集群,JDK 8,使用G1收集器,设置了-XX:MaxGCPauseMillis=500(最大暂停时间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:MaxDirectMemorySize=512m |
| OOM时生成dump | -XX:+HeapDumpOnOutOfMemoryError |
| 打印GC停顿时间 | -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCDateStamps -Xloggc:gc.log |
| 打印安全点统计 | -XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 |
| 安全点超时检测 | -XX:+SafepointTimeout -XX:SafepointTimeoutDelay=2000 |
| Windows GUI内存保持 | -Dsun.awt.keepWorkingSetOnMinimize=true |
| 去掉Survivor区(治标) | -XX:SurvivorRatio=65536 -XX:MaxTenuringThreshold=0 |
| 最大GC停顿目标(G1) | -XX:MaxGCPauseMillis=200 |
5.5 本章小结
- 调优前先确认"需要调优":以量化指标(RT、TP99、QPS、GC时间占比)为准
- 先排查业务代码,再调整JVM参数:大多数性能问题源于低效代码(如频繁调用外部命令、使用低效数据结构、不合理的集群同步)
- 善用监控工具:GC日志分析工具(GCViewer、GCEasy)、VisualVM + VisualGC、Arthas等
- 安全点问题容易被忽视:注意可数循环中可能导致的长时间停顿
- 32位系统的内存限制:进程最大内存通常为2GB,需综合考虑堆、直接内存、线程栈、Socket缓存等开销