前台(4核8G内存) JDK1.8
-server -Xms4g -Xmx4g -Xmn2g -Xss768k -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSClassUnloadingEnabled -XX:+DisableExplicitGC -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=80 -verbose:gc -XX:+PrintGCDetails -Xloggc:${CATALINA_BASE}/logs/gc.log -XX:+PrintGCDateStamps -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${CATALINA_BASE}/logs
后台(4核8G内存) JDK1.8
-server -Xms4g -Xmx4g -Xmn2g -Xss768k -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -verbose:gc -XX:+PrintGCDetails -Xloggc:${CATALINA_BASE}/logs/gc.log -XX:+PrintGCDateStamps -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${CATALINA_BASE}/logs
各参数介绍:
-server: JVM的server模式,区别于client模式
-Xms4g -Xmx4g: JVM堆内存的初始值、最大值
-Xmn2g: 年轻代大小
-Xss768k: 线程的堆栈大小,JDK1.5+每个线程堆栈大小为1M,减少每个线程的堆栈大小有利于增加应用可开启的线程数
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m: 设置Metaspace的初始大小和最大值,JDK1.8+使用Metaspace,设置这个值有利于减缓应用启动时fullGC次数,因为JVM在申请扩容Metaspace时如果超过了MetaspaceSize就会触发fullGC
相对的,JDK1.7以下使用-XX:PermSize=256m -XX:MaxPermSize=256m来设置Perm区大小;
-XX:+UseParNewGC: 使用多线程并行收集对年轻代进行GC
-XX:+UseConcMarkSweepGC: 使用CMS收集器对年老代进行GC,CMS收集器优势在于应用停顿时间少,适用于不能忍受长时间停顿的前台应用;
-XX:+CMSClassUnloadingEnabled: 在使用CMS收集器进行垃圾回收时同时清理持久代不再使用的class
-XX:+UseCMSInitiatingOccupancyOnly: CMS默认基于运行时收集的数据来启动CMS垃圾收集周期,开启这个参数来手动指定JVM通过CMSInitiatingOccupancyFraction的值进行垃圾收集策略;
-XX:CMSInitiatingOccupancyFraction=80 : 默认CMS是在年老代占满68%的时候开始进行CMS收集,如果你的年老代增长不是那么快,并且希望降低CMS次数的话,可以适当调高此值;
-XX:+DisableExplicitGC: 禁止程序显示调用gc方法:System.gc()来触发系统GC,一个最常见的例子是使用jxl关闭excel表格读写时会显示触发gc
-verbose:gc: 输出JVM gc日志
-XX:+PrintGCDetails: 输出JVM gc详情
-Xloggc:${CATALINA_BASE}/logs/gc.log: 指定gc日志目录
-XX:+PrintGCDateStamps: gc日志中输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+HeapDumpOnOutOfMemoryError: 在发生OOM时dump堆内存便于排查问题
-XX:HeapDumpPath=${CATALINA_BASE}/logs: 指定堆内存dump文件导出路径
JVM调优原则:
一定要结合应用本身的情况来选择,没有最优的参数,只有最适合的参数; 可以通过应用监控或者jstat、jmap等工具命令观察应用启动、运行时情况来配置,重点关注以下几点:
年轻代、年老代、MetaSpace占用和增长情况;
youngGC的频率和时间
fullGC的频率和时间
GC前后垃圾释放的程度
频繁fullGC问题一直是java开发面临的比较头疼的JVM运行时问题之一,头疼的点在于:
故障影响严重: 不同于其他业务异常,频繁fullGC时发生的stop-the-world会导致应用完全无法访问,并且频繁fullGC最终也会导致引用出现Java heap space的OOM;
而如果是由于业务方法产生的内存泄露导致的fullGC,很容易在failOver的高可用策略下将故障转移到剩余可用实例上,从而引发大量的实例出现频繁fullGC;
问题排查较复杂: 大部分由于内存泄露导致的fullGC很难快速定位到业务方法;
真是fullgGC案例:
发现问题
某天收到钉钉短信报警,订单服务有一个实例出现长时间多次的fullGC情况,需要赶紧做出响应。
(因为根据经验,如果是内存泄露发生fullGC,那么导致内存泄漏的业务方法再次被访问到时会引发更多的实例故障,最终引发服务整体不可用)
由于是生产环境,需要优先保障可用性,因此在大面积出现频繁fullGC而不能确定问题原因时,首先通过重启保证一定数量的实例提供线上高负载的访问;
不过在重启之前一定要记得保留现场以排查问题,留下一台故障实例不重启而是dump堆内存:
(以下是从生产环境的dump脚本中截取的堆内存dump命令)
$JAVA_HOME/bin/jmap -dump:format=b,file=$DUMP_DIR/jmap-bin-$PID.hprof -F $PID 2>&1
(导出的文件格式是hprof, -F参数是强制dump,否则应用因为正在fullGC不会响应dump命令)
堆内存的dump时间视应用情况不同,一般需要5~20分钟的时间dump完一个2G堆内存的应用,而且dump文件大小与堆内存相同,磁盘上留有足够的dump空间;
dump完成的hprof文件由于比较大,需要先压缩然后下载到本地进行分析
dump文件使用MAT工具(Eclipse Memory Analyzer)打开进行分析,界面看起来是这样:
可以使用Leak Suspects帮助分析:
直接使用Dominator Tree做详细分析:
发现有一个线程持有的对象的堆内存占用达到了500+M!基本可以确定是由于某个业务方法发生了内存泄露导致dubbo provider线程在虚拟机栈中持有了一个超大的对象,继续点开详情:
确定问题原因是某个业务方法使得线程持有了一个110k个BizOrder对象的数组以及一个349M的字符串(实际是BizOrder数组在分布式追踪框架里面生成的详细信息),继续排查到底是哪个业务方法发生的内存泄露:
根据线程虚拟机栈中持有的sql条件字符串以及接口和返回dto类型,排查到问题代码为某个后台订单查询场景下需要查询符合条件的订单条数(count)时写成了查询符合条件的所有订单记录;最终临时上线hotfix修复了该问题。