前言
本文是针对有一定Java虚拟机基础知识后,在实际工作中解决JVM相关问题及如何进行优化,偏方法论。
Java内存区域及收集器介绍


1.8同1.7比,最大的差别就是:元数据区取代了永久代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存。

垃圾收集器:
收集算法、垃圾收集器详细介绍:略,待补充。
垃圾收集从2个方面不断的改进:提高垃圾回收效率;提高用户体验(低停顿);

一、何时需要做jvm调优?
1、heap 内存(老年代)持续上涨达到设置的最大内存值;
2、Full GC 次数频繁;
3、GC 停顿时间过长(超过1秒);
4、应用出现OutOfMemory 等内存异常;
5、应用中有使用本地缓存且占用大量内存空间;
6、系统吞吐量与响应性能不高或下降。
二、 JVM调优原则
1、多数的Java应用不需要在服务器上进行JVM优化;
2、多数导致GC问题的Java应用,都不是因为我们参数设置错误,而是代码问题;
3、在应用上线之前,先考虑将机器的JVM参数设置到最优(最适合);
4、减少创建对象的数量;
5、减少使用全局变量和大对象;
6、JVM优化是到最后不得已才采用的手段;
7、在实际使用中,分析GC情况优化代码比优化JVM参数更好;
三、 JVM调优目标
1、GC低停顿;
2、GC低频率;
3、低内存占用;
4、高吞吐量;
JVM调优参数简介
堆设置
| 参数 | 说明 |
|---|---|
| -Xmn1200m | 设置年轻代大小为1200MB。增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8 |
| -Xms4g | 初始化堆内存大小为4GB |
| -Xmx4g | 堆内存最大值为4GB |
| -Xss512k | 设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1MB,以前每个线程堆栈大小为256K。应根据应用线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右 |
| -XX:MaxDirectMemorySize | 堆外内存/直接内存的大小,默认为堆内存减去一个Survivor区的大小 |
| -XX:MaxMetaspaceSize=512m | 设置元数据区最大值512M(jdk1.8) |
| -XX:MaxPermSize=256m | 设置持久代大小为256MB。(jdk1.7及以下) |
| -XX:MaxTenuringThreshold=15 | 设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论 |
| -XX:MetaspaceSize=128m | 设置元数据区初始值128M(jdk1.8) |
| -XX:NewRatio=4 | 设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5 |
| -XX:PermSize=100m | 初始化永久代大小为100MB。(jdk1.7及以下) |
| -XX:ReservedCodeCacheSize | JIT编译后二进制代码的存放区,满了之后就不再编译。默认开多层编译240M,可以在JMX里看看CodeCache的大小 |
| -XX:SurvivorRatio=8 | 设置年轻代中Eden区与Survivor区的大小比值。设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10 |
GC设置
| 参数 | 说明 |
|---|---|
| -XX:+HeapDumpOnOutOfMemoryError | 发生内存溢出是进行heap-dump |
| -XX:HeapDumpPath=/path/to/java_pid.hprof | 这个参数与-XX:+HeapDumpOnOutOfMemoryError共同作用,设置heap-dump时内容输出文件 |
| -XX:ErrorFile=/path/to/hs_err_pid.log | 指定致命错误日志位置。一般在JVM发生致命错误时会输出类似hs_err_pid.log的文件,默认是在工作目录中(如果没有权限,会尝试在/tmp中创建),不过还是自己指定位置更好一些,便于收集和查找,避免丢失 |
| -XX:StringTableSize=1000003 | 指定字符串常量池大小,默认值是60013。对Java稍微有点常识的应该知道,字符串是常量,创建之后就不可修改了,这些常量所在的地方叫做字符串常量池。如果自己系统中有很多字符串的操作,且这些字符串值比较固定,在允许的情况下,可以适当调大一些池子大小 |
GC日志
GC过程可以通过GC日志来提供优化依据。
| 参数 | 说明 |
|---|---|
| -XX:+PrintGCDetails | 启用gc日志打印功能 |
| -Xloggc:/path/to/gc.log | 指定gc日志位置 |
| -XX:+PrintHeapAtGC | 打印GC前后的详细堆栈信息 |
| -XX:+PrintGCDateStamps | 打印可读的日期而不是时间戳 |
| -XX:+PrintGCApplicationStoppedTime | 打印所有引起JVM停顿时间,如果真的发现了一些不知什么的停顿,再临时加上-XX:+PrintSafepointStatistics -XX: PrintSafepointStatisticsCount=1找原因 |
| -XX:+PrintGCApplicationConcurrentTime | 打印JVM在两次停顿之间正常运行时间,与-XX:+PrintGCApplicationStoppedTime一起使用效果更佳 |
| -XX:+PrintTenuringDistribution | 查看每次minor GC后新的存活周期的阈值 |
| -XX:+UseGCLogFileRotation 与 -XX:NumberOfGCLogFiles=10 与 -XX:GCLogFileSize=10M | GC日志在重启之后会清空,但是如果一个应用长时间不重启,那GC日志会增加,所以添加这3个参数,是GC日志滚动写入文件,但是如果重启,可能名字会出现混乱 |
| -XX:PrintFLSStatistics=1 | 打印每次GC前后内存碎片的统计信息 |
收集器
| 参数 | 说明1 | 说明2 |
|---|---|---|
| -XX:+UseConcMarkSweepGC | 设置CMS收集器 (只针对老年代) | 并发收集、低停顿;对CPU资源敏感,CMS默认启动的回收线程数是(CPU数量+3)/4; |
| -XX:+UseParNewGC | 设置年轻代为多线程收集 | 可以不设置,jdk 8中设置-XX:+UseConcMarkSweepGC,自动启用-XX:+UseParNewGC |
| -XX:+CMSClassUnloadingEnabled | 配合-XX:+UseConcMarkSweepGC使用,垃圾回收会清理持久代,移除不再使用的classes | |
| -XX:+ UseG1GC | 允许使用垃圾优先(G1)垃圾收集器。它是一个服务器式垃圾收集器,针对具有大量RAM的多处理器计算机。它以高概率满足GC暂停时间目标,同时保持良好的吞吐量。 | G1收集器推荐用于需要大堆(大小约为6 GB或更大)且GC延迟要求有限的应用(稳定且可预测的暂停时间低于0.5秒)。 |
| G1其他参数详情请见参考一 |
其他参数设置
| 参数 | 说明 |
|---|---|
| -ea | 启用断言,这个没有什么好说的,可以选择启用,或这选择不启用,没有什么大的差异。完全根据自己的系统进行处理 |
| -XX:+UseThreadPriorities | 启用线程优先级,主要是因为我们可以给予周期性任务更低的优先级,以避免干扰客户端工作。在我当前的环境中,是默认启用的 |
| -XX:ThreadPriorityPolicy=42 | 允许降低线程优先级 |
| -XX:+HeapDumpOnOutOfMemoryError | 发生内存溢出是进行heap-dump |
| -XX:HeapDumpPath=/path/to/java_pid.hprof | 这个参数与-XX:+HeapDumpOnOutOfMemoryError共同作用,设置heap-dump时内容输出文件 |
| -XX:ErrorFile=/path/to/hs_err_pid.log | 指定致命错误日志位置。一般在JVM发生致命错误时会输出类似hs_err_pid.log的文件,默认是在工作目录中(如果没有权限,会尝试在/tmp中创建),不过还是自己指定位置更好一些,便于收集和查找,避免丢失 |
| -XX:StringTableSize=1000003 | 指定字符串常量池大小,默认值是60013。对Java稍微有点常识的应该知道,字符串是常量,创建之后就不可修改了,这些常量所在的地方叫做字符串常量池。如果自己系统中有很多字符串的操作,且这些字符串值比较固定,在允许的情况下,可以适当调大一些池子大小 |
| -XX:+AlwaysPreTouch | 在启动时把所有参数定义的内存全部捋一遍。使用这个参数可能会使启动变慢,但是在后面内存使用过程中会更快。可以保证内存页面连续分配,新生代晋升时不会因为申请内存页面使GC停顿加长。通常只有在内存大于32G的时候才会有感觉 |
| -XX:-UseBiasedLocking | 禁用偏向锁(在存在大量锁对象的创建且高度并发的环境下(即非多线程高并发应用)禁用偏向锁能够带来一定的性能优化) |
| -XX:AutoBoxCacheMax=20000 | 增加数字对象自动装箱的范围,JDK默认-128~127的int和long,超出范围就会即时创建对象,所以,增加范围可以提高性能,但是也是需要测试 |
| -XX:-OmitStackTraceInFastThrow | 不忽略重复异常的栈,这是JDK的优化,大量重复的JDK异常不再打印其StackTrace。但是如果系统是长时间不重启的系统,在同一个地方跑了N多次异常,结果就被JDK忽略了,那岂不是查看日志的时候就看不到具体的StackTrace,那还怎么调试,所以还是关了的好 |
| -XX:+PerfDisableSharedMem | 启用标准内存使用。JVM控制分为标准或共享内存,区别在于一个是在JVM内存中,一个是生成/tmp/hsperfdata_{userid}/{pid}文件,存储统计数据,通过mmap映射到内存中,别的进程可以通过文件访问内容。通过这个参数,可以禁止JVM写在文件中写统计数据,代价就是jps、jstat这些命令用不了了,只能通过jmx获取数据。但是在问题排查是,jps、jstat这些小工具是很好用的,比jmx这种很重的东西好用很多,所以需要自己取舍。这里有个GC停顿的例子 |
| -Djava.net.preferIPv4Stack=true | 这个参数是属于网络问题的一个参数,可以根据需要设置。在某些开启ipv6的机器中,通过InetAddress.getLocalHost().getHostName()可以获取完整的机器名,但是在ipv4的机器中,可能通过这个方法获取的机器名不完整,可以通过这个参数来获取完整机器名 |
JVM如何调优
步骤
第1步:分析GC日志(详见参考四、五)及dump文件,判断是否需要优化,确定瓶颈问题点;
第2步:确定JVM调优量化目标;
第3步:确定JVM调优参数(根据历史JVM参数来调整);
- 调优步骤:
1、确定老年代内存占用大小,使用以下命令, concurrent mark-sweep generation项对应的used就是老年代的内存占用2、设置堆内存大小,-Xms、-Xmx,建议-Xms=-Xmx,Xmx 和 Xms设置为老年代存活对象的3-4倍,即FullGC之后的老年代内存占用的3-4倍,参考Xms和Xmx参数设置为相同值好处1
jmap -heap 【pid】
3、设置年轻代内存大小,-Xmn,建议整个堆3/8
4、设置元空间大小,-XX:MetaspaceSize、-XX:MaxMetaspaceSize,建议MetaspaceSize和MaxMetaspaceSize设置一样大,具体设置多大,建议稳定运行一段时间后通过jstat -gc pid确认且这个值大一些,对于大部分项目256m即可,参考MetaspaceSize的误解,Metaspace解密
第4步:调优一台服务器,对比观察调优前后的差异;
第5步:不断的分析和调整,直到找到合适的JVM参数配置;
第6步:找到最合适的参数,将这些参数应用到所有服务器,并进行后续跟踪。
命令
jps:查询java进程
jstat:虚拟机运行状态信息
jmap:生成堆存储快照
jstack:生成虚拟机当前时刻的线程快照,帮助定位线程出现长时间停顿的原因
参考:https://www.jianshu.com/p/c6a04c88900a
实例
不同的环境有不同的方案,可以参考,不过还是建议在自己的环境中有针对的验证之后再使用,毕竟各自的环境都有差异。
gc查看
查看gc.log状态,可参考 参考四、参考五;
结论:没有Full GC;每次从年轻代移到了老年代的内存占老年代总内存比例并不大:

查看star.hprof文件(使用JProfiler),如果有的话;
查询某一时刻虚拟机运行状态;
./jdk/jdk1.8.0_101/bin/jstat -gc 【pid】 3600s
| 参数 | 说明 | 设置的值 |
|---|---|---|
| S0C | Survivor0空间的大小。单位KB。 | 316992.0(0.3G) |
| S1C | Survivor1空间的大小。单位KB。 | 316992.0(0.3G) |
| S0U | Survivor0已用空间的大小。单位KB。 | 0 |
| S1U | Survivor1已用空间的大小。单位KB。 | 17521.7 |
| EC | Eden空间的大小。单位KB。 | 2536320.0(2.4G) |
| EU | Eden已用空间的大小。单位KB。 | |
| OC | 老年代空间的大小。单位KB。 | 3121152.0(3G) |
| OU | 老年代已用空间的大小。单位KB。 | 221391(0.21G) |
| MC | 方法区大小 | 125824.0(0.12G) |
| MU | 方法区使用大小 | 119041(0.11G) |
| CCSC | 压缩类空间大小 | 13824.0(0.01G) |
| CCSU | 压缩类空间使用大小 | 12536 |
| YGC | 年轻代垃圾回收次数 | 261 |
| YGCT | 年轻代垃圾回收消耗时间 | 10.931(0.04s/次) |
| FGC | 老年代垃圾回收次数 | 0 |
| FGCT | 老年代垃圾回收消耗时间 | 0 |
| GCT | 垃圾回收消耗总时间 | 10.931 |
查看系统jvm参数状态
1 | jinfo -flags 【pid】 查看jvm的所有设置参数 |
机器状况
| 内存 | CPU | 磁盘空间 |
|---|---|---|
| 8G | 4*4 | 60G |
当前jvm参数配置:
1 | -Xms6144m -Xmx6144m -XX:MaxPermSize=256m -verbose:gc -XX:+PrintGCDetails -Xloggc:/../tomcat/logs/gc.log -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -Xmn3096m -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSClassUnloadingEnabled -XX:+ExplicitGCInvokesConcurrent -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/../tomcat/logs/star.hprof |
| 参数 | 说明 | 建议 |
| -Xms6144m | 初始化堆内存大小为6GB | 低于系统内存 |
| -Xmx6144m | 堆内存最大值为6GB | 低于系统内存 |
| 设置永久代最大为256MB(无用) | jdk1.8不需要设置 | |
| -verbose:gc | 输出虚拟机中GC的详细情况 | |
| -XX:+PrintGCDetails | 输出虚拟机中GC的详细情况 | |
| -Xloggc:/../tomcat/logs/gc.log | 设置gc日志文件位子 | |
| -XX:+PrintGCTimeStamps | 打印gc时间时间戳,gc.log | |
| -XX:MetaspaceSize=256m | 设置元数据区初始值256M (永久代) | 该值越大触发Full GC的时机就越晚, |
| -XX:MaxMetaspaceSize=256m | 设置元数据区最大值256M (永久代) | |
| -Xmn3096m | 设置年轻代区域3G | Sun官方推荐配置为整个堆的3/8 |
| -XX:+UseConcMarkSweepGC | 设置CMS收集器 (只针对老年代) | 并发收集、低停顿;对CPU资源敏感,CMS默认启动的回收线程数是(CPU数量+3)/4; |
| 设置年轻代为多线程收集 | 可以不设置,jdk 8中设置-XX:+UseConcMarkSweepGC,自动启用-XX:+UseParNewGC | |
| -XX:+CMSClassUnloadingEnabled | 配合-XX:+UseConcMarkSweepGC使用,垃圾回收会清理持久代,移除不再使用的classes | |
| -XX:+ExplicitGCInvokesConcurrent | 允许使用System.gc()请求调用并发GC 。默认情况下禁用此选项,并且只能与该-XX:+UseConcMarkSweepGC选项一起启用 |
|
| -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses | 通过System.gc()在并发GC周期期间使用请求和卸载类,可以调用并发GC。默认情况下禁用此选项,并且只能与该-XX:+UseConcMarkSweepGC选项一起启用 |
|
| -XX:+HeapDumpOnOutOfMemoryError | 通过使用堆分析器(HPROF)将Java堆转储到当前目录中的文件 | |
| -XX:HeapDumpPath=/home/ewallet/loan-star/loan-star-pre/default/tomcat/logs/star.hprof | 设置用于写入堆分析器(HPROF)提供的堆转储的路径和文件名。默认情况下,该文件在当前工作目录中创建,并且名为java_pidpid.hprof,其中pid是导致错误的进程的标识符 |
总结
当前机器的配置足够,在目前情况下优化参数不会有太大的提升效果。
假如在降低机器配置前提下,那需要依赖数据,做相应的调整,具体要看机器成本而定。
参考
附一 找到占用cpu最高的一个线程
第一步,找到占用cpu最高的一个线程【n】
方法:直接top获得【pid】,然后shift+h或者top -Hp 【pid】
第二步,将其转化成16进制。假使我们得到的线程号为【n】,接下来将它转成16进制,记为spid
方法:printf 0x%x 【n】
第三步,执行以下命令,打印后面100行分析问题
1
jstack -l 【pid】| grep 【spid】 -A 100
如果需要导出完整的线程栈,使用
1
jstack pid > pid.tdump
附二 堆内存操作
查看jvm的所有设置参数
1
jinfo -flags 【pid】
查看当前堆内存使用情况
1
jmap -heap 【pid】
gc情况查看
1
2
3jstat -gcutil 【pid】
jstat -gc 【pid】 2s 3
导出堆文件
- 导出整个堆文件
1
jmap -dump:format=b,file=heap.hprof 【pid】
- 导出存活的堆文件(慎用,会进行一次Full GC)
1
jmap -dump:live,format=b,file=heap.hprof 【pid】
附三 参考资料
参考一:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html ( 参数)
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/index.html(收集器)
https://www.oracle.com/technetwork/java/tuning-139912.html(调优试例)
参考二:书籍:深入理解Java虚拟机(第二版)
书籍:Java Performance:The Definitive Guide
参考三:https://blog.csdn.net/linhu007/article/details/48897597(CMS与G1)
参考四:https://blog.csdn.net/zc19921215/article/details/83029952