4JVM调优
JVM常用参数
hotSpot参数分类
- 标准的:- 开头,所有的hotSpot都支持
- 非标准的:- X 开头,特定版本hotSpot支持特定命令
- 不稳定:- XX开头,下个版本可能取消
常用参数
-Xmn -Xms -Xmx -Xss
年轻代 最小堆 最大堆 栈空间-XX:+UseTLAB
使用TLAB,默认打开-XX:+PrintTLAB
打印TLAB的使用情况-XX:TLABSize
设置TLAB大小-XX:+DisableExplictGC
System.gc()不管用 ,FGC-XX:+PrintGC
打印GC信息-XX:+PrintGCDetails
打印详细GC信息-XX:+PrintHeapAtGC
GC时,打印堆栈情况-XX:+PrintGCTimeStamps
打印发生GC时的系统时间-XX:+PrintGCApplicationConcurrentTime (低)
打印应用程序时间-XX:+PrintGCApplicationStoppedTime (低)
打印暂停时长-XX:+PrintReferenceGC (重要性低)
记录回收了多少种不同引用类型的引用-verbose:class
类加载详细过程-XX:+PrintVMOptions
-Xloggc:opt/log/gc.log
-XX:MaxTenuringThreshold
升代年龄,最大值15
锁自旋次数 -XX:PreBlockSpin 热点代码检测参数-XX:CompileThreshold 逃逸分析 标量替换 …
这些不建议设置-XX:+PrintCommandLineFlags:查看程序使用的默认JVM参数
-Xmn10M(年轻代大小) -Xms40M(最小堆) -Xmx60M(最大堆) -XX:+PrintCommandLineFlags -XX:+PrintGC(打印GC回收信息)
- GC信息常用参数:PrintGCDetails PrintGCTimeStamps PrintGCCauses
java -XX:+PrintFlagsInitial 默认参数值
java -XX:+PrintFlagsFinal 最终参数值
java -XX:+PrintFlagsFinal | grep xxx 找到对应的参数
java -XX:+PrintFlagsFinal -version |grep GC
Parallel常用参数
- -XX:SurvivorRatio
Eden和s1 s2 的比例 - -XX:PreTenureSizeThreshold
对于直接分配到old区的大对象的大小定义 - -XX:MaxTenuringThreshold
- -XX:+ParallelGCThreads
并行收集器的线程数,同样适用于CMS,一般设为和CPU核数相同 - -XX:+UseAdaptiveSizePolicy
自动选择各区大小比例
CMS常用参数
-XX:+UseConcMarkSweepGC
使用CMS垃圾收集器-XX:ParallelCMSThreads
CMS要使用的线程数量-XX:CMSInitiatingOccupancyFraction
使用多少比例的老年代后开始CMS收集,默认是68%(近似值),如果频繁发生SerialOld卡顿,应该调小,(频繁CMS回收)-XX:+UseCMSCompactAtFullCollection
在FGC时进行压缩-XX:CMSFullGCsBeforeCompaction
多少次FGC之后进行压缩-XX:+CMSClassUnloadingEnabled
-XX:CMSInitiatingPermOccupancyFraction
达到什么比例时进行Perm回收-XX:GCTimeRatio
设置GC时间占用程序运行时间的百分比-XX:MaxGCPauseMillis
停顿时间,是一个建议时间,GC会尝试用各种手段达到这个时间,比如减小年轻代
G1常用参数
- -XX:+UseG1GC
- -XX:MaxGCPauseMillis
建议值,G1会尝试调整Young区的块数来达到这个值 - -XX:GCPauseIntervalMillis
?GC的间隔时间 TODO - -XX:+G1HeapRegionSize
Region分区大小,建议逐渐增大该值,1 2 4 8 16 32M。
随着size增加,垃圾的存活时间更长,GC间隔更长,但每次GC的时间也会更长
ZGC做了改进(动态区块大小) - -XX:+G1NewSizePercent
新生代最小比例,默认为5% - -XX:+G1MaxNewSizePercent
新生代最大比例,默认为60% - -XX:+GCTimeRatio
GC时间建议比例,G1会根据这个值调整堆空间 - -XX:+ConcGCThreads
线程数量 - -XX:+InitiatingHeapOccupancyPercent
启动G1的堆空间占用比例
GC日志
常用日志参数
- -Xloggc:/opt/xxx/logs/xxx-xxx-gc-%t.log 文件位置
- -XX:+UseGCLogFileRotation 日志文件循环使用
- -XX:NumberOfGCLogFiles=5 日志文件的总数量
- -XX:GCLogFileSize=20M 日志文件的大小
- -XX:+PrintGCDetails 打印GC详细信息
- -XX:+PrintGCDateStamps 打印GC的时间
- -XX:+PrintGCCause 记录GC原因

- 33.125 :[GC [DefNew : 3324K -> 152K(3712K), 0.0025925 secs] 3324K -> 152K(11904K) , 0.0031680 secs]
- 33.125: GC的发生时间,Java虚拟机启动以来经过的秒数,GC为YGC,FGC则显示FGC
- GC: 停顿类型,Full GC 是发生了STW的,Full GC(System) 调用系统方法System.gc().
- DefNew:GC发生的区域,名称由所使用的收集器来决定
- 3324K -> ***K(***k) : GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量)
- 方括号之外的是Java的容量,GC时间
- heap dump部分:
1
2
3eden space 5632K, 94% used [0x00000000ff980000,0x00000000ffeb3e28,0x00000000fff00000)
后面的内存地址指的是,起始地址,使用空间结束地址,整体空间结束地址
常见问题排查方案
- 吞吐量:用户代码执行时间 /(用户代码执行时间 + 垃圾回收时间) 一般用[PS]
- 响应时间: STW越短,响应时间越好 [CMS,G1,ZGC]
CPU100%排查
- top -c :查看所有的进程 看cpu使用率 P排序
- top -Hp [pid] 查看使用cpi最高的进程下的线程
- 使用jstack -l [pid] > **.stack 生成并查看堆栈日志文件,然后根据线程的十六进制grep查看栈日志文件中的信息
- 转换为十六进制:printf “0x%x\n”
- cat **.stack |grep ‘b26’ -C 8
- 重点关注:wating,blocked,
- waiting on <0x0000000088ca3310> (a java.lang.Object)
假如有一个进程中100个线程,很多线程都在waiting on,一定要找到是哪个线程持有这把锁
怎么找?搜索jstack dump的信息,找,看哪个线程持有这把锁RUNNABLE
- 工作线程占比高|垃圾回收线程占比高
某个线程进入wait状态
- 具体案例:
- 具体的场景是,在使用CountDownLatch时,由于需要每一个并行的任务都执行完成之后才会唤醒主线程往下执行。而当时我们是通过CountDownLatch控制多个线程连接并导出用户的gmail邮箱数据,这其中有一个线程连接上了用户邮箱,但是连接被服务器挂起了,导致该线程一直在等待服务器的响应。最终导致我们的主线程和其余几个线程都处于WAITING状态。
- 思路
- 通过grep在jstack日志中找出所有的处于TIMED_WAITING状态的线程,将其导出到某个文件中,如a1.log
- 等待一段时间之后,比如10s,再次对jstack日志进行grep,将其导出到另一个文件
- 重复步骤2,待导出34个文件之后,我们对导出的文件进行对比,找出其中在这几个文件中一直都存在的用户线程,这个线程基本上就可以确认是包含了处于等待状态有问题的线程。因为正常的请求线程是不会在2030s之后还是处于等待状态的。
- 经过排查得到这些线程之后,我们可以继续对其堆栈信息进行排查,如果该线程本身就应该处于等待状态,比如用户创建的线程池中处于空闲状态的线程,那么这种线程的堆栈信息中是不会包含用户自定义的类的。这些都可以排除掉,而剩下的线程基本上就可以确认是我们要找的有问题的线程。通过其堆栈信息,我们就可以得出具体是在哪个位置的代码导致该线程处于等待状态了。
- 我们在判断是否为用户线程时,可以通过线程最前面的线程名来判断,因为一般的框架的线程命名都是非常规范的,我们通过线程名就可以直接判断得出该线程是某些框架中的线程,这种线程基本上可以排除掉。而剩余的,比如上面的Thread-0,以及我们可以辨别的自定义线程名,这些都是我们需要排查的对象。
死锁
- 对于死锁,这种情况基本上很容易发现,因为jstack可以帮助我们检查死锁,并且在日志中打印具体的死锁线程信息。如下是一个产生死锁的一个jstack日志示例
Full GC次数过多
特征
- 线上多个线程的CPU都超过了100%,通过jstack命令可以看到这些线程主要是垃圾回收线程
- 通过jstat命令监控GC情况,可以看到Full GC次数非常多,并且次数在不断增加。
- 与cpu排查类似
- jstat -gcutil 查看cpu占用高的线程的垃圾回收情况
- 生成dump日志
主要原因
- 内存泄漏,代码中频繁创建了大量的对象,导致内存溢出,此时可以通过eclipse的mat工具查看内存中有哪些对象比较多;
- 内存占用不高,但是Full GC次数还是比较多,此时可能是显示的
System.gc()
调用导致GC次数过多,这可以通过添加-XX:+DisableExplicitGC
来禁用JVM对显示GC的响应。 - 大对象:系统一次性加载了过多数据到内存中(比如SQL查询未做分页),导致大对象进入了老年代。
- 程序频繁生成一些长生命周期的对象,当这些对象的存活年龄超过分代年龄时便会进入老年代,最后引发FGC。
排查指南
- 查看监控,以了解出现问题的时间点以及当前FGC的频率(可对比正常情况看频率是否正常)
- 了解该时间点之前有没有程序上线、基础组件升级等情况。
- 了解JVM的参数设置,包括:堆空间各个区域的大小设置,新生代和老年代分别采用了哪些垃圾收集器,然后分析JVM参数设置是否合理。
- 再对步骤1中列出的可能原因做排除法,其中元空间被打满、内存泄漏、代码显式调用gc方法比较容易排查。
- 针对大对象或者长生命周期对象导致的FGC,可通过 jmap -histo 命令并结合dump堆内存文件作进一步分析,需要先定位到可疑对象。
- 通过可疑对象定位到具体代码再次分析,这时候要结合GC原理和JVM参数设置,弄清楚可疑对象是否满足了进入到老年代的条件才能下结论。
系统内存飙高排查方案
- 导出堆内存 (jmap)
- 分析 (jhat jvisualvm mat jprofiler … )
如何监控jvm
- jstat jvisualvm jprofiler arthas top…
JVM堆外内存泄漏故障排查
- 问题描述:物理内存不断上升,很大的概率是JVM的内存泄漏造成的,生成dump
调优案例
12306遭遇春节大规模抢票应该如何支撑?
- 12306应该是中国并发量最大的秒杀网站:号称并发量100W最高
- 架构方面:CDN -> LVS -> NGINX -> 业务系统 -> 每台机器1W并发(10K问题) 100台机器
- 普通电商订单 -> 下单 ->订单系统(IO)减库存 ->等待用户付款
- 12306的一种可能的模型: 下单 -> 减库存 和 订单(redis kafka) 同时异步进行 ->等付款
- 减库存最后还会把压力压到一台服务器
- 可以做分布式本地库存 + 单独服务器做库存均衡
- 大流量的处理方法:分而治之
升级内存变得更卡顿
- 有一个50万PV的资料类网站(从磁盘提取文档到内存)原服务器32位,1.5G
的堆,用户反馈网站比较缓慢,因此公司决定升级,新的服务器为64位,16G
的堆内存,结果用户反馈卡顿十分严重,反而比以前效率更低了 - 为什么原网站慢?
- 很多用户浏览数据,很多数据load到内存,内存不足,频繁GC,STW长,响应时间变慢
- 为什么会更卡顿?
- 内存越大,FGC时间越长
- 咋办?
- PS -> PN + CMS 或者 G1
线程池的OOM问题
1 |
|
TOMCAT http-header-size过大问题
+
Lambda表达式导致方法区溢出问题
+
直接内存溢出
- 使用Unsafe 分配直接内存,或者使用NIO的问题
栈溢出问题
- -Xss设定太小
常用命令
jinfo
- 打印虚拟机详细信息
- 但如果想更加清晰的看具体的一个参数或者想知道未被显式指定的参数时,就可以通过 jinfo -flag 来查询了。
jinfo -flag MetaspaceSize [pid]
1
2
3
4
5
6
7
8
9### jconsole
+ jvm分析图形化工具
1. 程序启动加入参数:
> ```shell
> java -Djava.rmi.server.hostname=192.168.17.11 -Dcom.sun.management.jmxremote
> -Dcom.sun.management.jmxremote.port=11111
> -Dcom.sun.management.jmxremote.authenticate=false
> -Dcom.sun.management.jmxremote.ssl=false
> -XX:+printGC
- 如果遭遇 Local host name unknown:XXX的错误,修改/etc/hosts文件,把XXX加入进去
1
2192.168.17.11 basic localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6 - 关闭linux防火墙(实战中应该打开对应端口)
1
2service iptables stop
chkconfig iptables off #永久关闭 - windows上打开 jconsole远程连接 192.168.17.11:11111
jvisualvm
- TODO
jps
- 查看所有的java进程
- -q 不输出类名、Jar名和传入main方法的参数
- -m 输出传入main方法的参数
- -l 输出main类或Jar的全限名
- -v 输出JVM启动时传入的参数
jstat
- jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。大小如未特殊说明的均为kb
- jstat -class pid :类加载统计
- jstat -compiler pid : 编译统计 失败类型/失败方法
- jstat -gc pid : 垃圾统计回收
- S0C:第一个幸存区的大小 S1C:第二个幸存区的大小
S0U:第一个幸存区的使用大小 S1U:第二个幸存区的使用大小
EC:伊甸园区的大小 EU:伊甸园区的使用大小
OC:老年代大小 OU:老年代使用大小
MC:方法区大小 MU:方法区使用大小
CCSC:压缩类空间大小 CCSU:压缩类空间使用大小
YGC:年轻代垃圾回收次数 YGCT:年轻代垃圾回收消耗时间
FGC:FGC垃圾回收次数 FGCT:FGC垃圾回收消耗时间
GCT:垃圾回收消耗总时间
- S0C:第一个幸存区的大小 S1C:第二个幸存区的大小
- jstat -gccapacity pid :堆内存统计
- NGCMN:新生代最小容量 NGCMX:新生代最大容量 NGC:当前新生代容量
S0C:第一个幸存区大小 S1C:第二个幸存区的大小
EC:伊甸园区的大小
OGCMN:老年代最小容量 OGCMX:老年代最大容量 OGC:当前老年代大小[^1] OC:当前老年代大小
MCMN:最小元数据容量 MCMX:最大元数据容量 MC:当前元数据空间大小
CCSMN:最小压缩类空间大小 CCSMX:最大压缩类空间大小 CCSC:当前压缩类空间大小
YGC:年轻代gc次数 FGC:FGC次数- 所有的OC大小,因为老年代只有一个所以OGC=OC
- NGCMN:新生代最小容量 NGCMX:新生代最大容量 NGC:当前新生代容量
- jstat -gcnew pid : 新生带垃圾回收统计
- S0C:第一个幸存区大小 S1C:第二个幸存区的大小
S0U:第一个幸存区的使用大小 S1U:第二个幸存区的使用大小
TT:对象在新生代存活的次数 MTT:对象在新生代存活的最大次数
DSS:期望的幸存区大小
EC:伊甸园区的大小 EU:伊甸园区的使用大小
YGC:年轻代垃圾回收次数 YGCT:年轻代垃圾回收消耗时间
- S0C:第一个幸存区大小 S1C:第二个幸存区的大小
- jstat -gcnewcapacity pid : 新生代内存统计
- NGCMN:新生代最小容量 NGCMX:新生代最大容量 NGC:当前新生代容量
S0CMX:幸存1区最大值 S0C:当前幸存1区大小
S1CMX:幸存2区最大值 S1C:当前幸存2区大小
ECMX:最大伊甸园区大小 EC:当前伊甸园区大小
YGC:年轻代垃圾回收次数 FGC:FGC回收次数
- NGCMN:新生代最小容量 NGCMX:新生代最大容量 NGC:当前新生代容量
- jstat -gcold pid : 老年代垃圾回收统计
- MC:方法区大小 MU:方法区使用大小
CCSC:压缩类空间大小 CCSU:压缩类空间使用大小
OC:老年代大小 OU:老年代使用大小
YGC:年轻代垃圾回收次数
FGC:FGC垃圾回收次数
FGCT:FGC垃圾回收消耗时间 GCT:垃圾回收消耗总时间
- MC:方法区大小 MU:方法区使用大小
- jstat -gcoldcapacity pid : 老年代内存统计
- OGCMN:老年代最小容量 OGCMX:老年代最大容量 OGC:当前老年代大小 OC:老年代大小
YGC:年轻代垃圾回收次数 FGC:FGC圾回收次数 FGCT:FGC回收消耗时间
GCT:垃圾回收消耗总时间
- OGCMN:老年代最小容量 OGCMX:老年代最大容量 OGC:当前老年代大小 OC:老年代大小
- jstat -gcmetacapacity pid : JDK8下元数据空间统计
- MCMN:最小元数据容量 MCMX:最大元数据容量 MC:当前元数据空间大小
CCSMN:最小压缩类空间大小 CCSMX:最大压缩类空间大小
CCSC:当前压缩类空间大小
YGC:年轻代垃圾回收次数 FGC:FGC垃圾回收次数
FGCT:FGC垃圾回收消耗时间 GCT:垃圾回收消耗总时间
- MCMN:最小元数据容量 MCMX:最大元数据容量 MC:当前元数据空间大小
- jstat -gcutil pid : 垃圾回收信息统计总结
- S0:幸存1区当前使用比例 S1:幸存2区当前使用比例
E:伊甸园区使用比例
O:老年代使用比例
M:元数据区使用比例
CCS:压缩使用比例
YGC:年轻代垃圾回收次数
FGC:FGC次数
FGCT:FGC消耗时间
GCT:垃圾回收消耗总时间
- S0:幸存1区当前使用比例 S1:幸存2区当前使用比例
- jstat -printcompilation pid : JVM编译方法统计
- Compiled:最近编译方法的数量
- Size:最近编译方法的字节码数量
- Type:最近编译方法的编译类型。
- Method:方法名标识。
jstack
- -l:会打印出额外的锁信息,在发生死锁时可以用jstack -l pid来观察锁持有情况
- -m:不仅会输出Java堆栈信息,还会输出C/C++堆栈信息(比如Native方法)
- jstack -l pid > ./1.stack :将堆栈信息保存到本地
jmap
- -dump要注意的点
- 线上系统,内存特别大,jmap执行期间会对进程产生很大影响,甚至卡顿(电商不适合)
- 设定了参数HeapDump -XX:+HeapDumpOnOutOfMemoryError,OOM的时候会自动产生堆转储文件
- 很多服务器备份(高可用),停掉这台服务器对其他服务器不影响
- 在线定位(一般小点儿公司用不到)
- jmap pid
- 查询进程的内存映像信息
- jmap -heap pid
- 显示堆的详细信息
- jmap -histo:live pid
- 显示堆中对象的统计信息,其中包括每个Java类、对象数量、内存大小(单位:字节)、完全限定的类名。打印的虚拟机内部的类名称将会带有一个’*’前缀。如果指定了live子选项,则只计算活动的对象。
- jmap -clstats pid
- 打印类加载器的信息。-clstats是-permstat的替代方案,在JDK8之前,-permstat用来打印类加载器的数据
打印Java堆内存的永久保存区域的类加载器的智能统计信息。对于每个类加载器而言,它的名称、活跃度、地址、父类加载器、它所加载的类的数量和大小都会被打印。此外,包含的字符串数量和大小也会被打印。
- 打印类加载器的信息。-clstats是-permstat的替代方案,在JDK8之前,-permstat用来打印类加载器的数据
- jmap -finalizerinfo pid
- 打印等待终结的对象信息
- jmap -dump:format=b,file=heapdump.phrof pid
- 生成堆转储快照dump文件。
- 以hprof二进制格式转储Java堆到指定filename的文件中。live子选项是可选的。如果指定了live子选项,堆中只有活动的对象会被转储。想要浏览heap dump,你可以使用jhat(Java堆分析工具)读取生成的文件。
- 这个命令执行,JVM会将整个heap的信息dump写入到一个文件,heap如果比较大的话,就会导致这个过程比较耗时,并且执行的过程中为了保证dump的信息是可靠的,所以会暂停应用, 线上系统慎用。
- Eclipse的MAT(Memory Analyzer)或者JDK自带的JVisualVM可以用来分析dump
jhat
- jhat -port 7070 heapdump.phrof :使用html的形式展示dump文件