前言
前面几篇博客下来,对JVM整体应该算了解了,细节的部分,字节码和类文件结构后续会详细总结(其实这两个可以深入仔细研究,但是非常花时间,后续空了会继续总结)。但是针对所谓的JVM调优我们似乎还是没有太好的理解,如何调优怎么调优,现在对我们来说似乎还有些陌生,本篇博客就会小打小闹总结一下JVM调优使用。
GC日志
关于垃圾收集器的种类,种类就不再赘述了,这里只是简单总结一下GC日志的含义,大部分依旧参照的是《深入理解JVM虚拟机》一书。
在让应用程序打印GC日志的时候需要给应用程序增加如下配置参数。
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:F:\gcLogs\gc.log
这里就是告知应用程序打印GC日志,并将日志保存到F盘下的gcLogs目录下。
日志示例
0.938: [GC (Allocation Failure) [PSYoungGen: 12288K->1512K(13824K)] 12288K->2219K(14848K), 0.0255781 secs] [Times: user=0.06 sys=0.00, real=0.03 secs]
如上面一段是直接在应用程序中获取的gc日志,直接看确实有些难以理解,这里一一解构吧,就以上面一行日志为实例。
0.938表示这个GC操作发生在JVM启动后的0.938秒。
下面分析第一个中括号中的内容,GC(Allocation Failure)表明了这次垃圾收集的停顿类型,括号中是引发这次GC的原因。
[PSYoungGen:12288K->1512K(13824K)] PSYoungGen指示了垃圾收集器的类型,后面方括号内部的 12288K->1512K(13824K)含义是“GC前该内存区域已使用容量->GC后该内存区域已用容量(该内存区域总容量)”。即12288K[Young区回收前的大小]->1512K[Young区回收后的大小] (13824K[Young区总容量大小])。
12288K->2219K(14848K) 这个标示的是堆的总大小回收前和回收后的大小变化。
后面的时间 0.0255781标示的是本次垃圾回收所花费的时间。
[Times: user=0.06 sys=0.00, real=0.03 secs] 这端日志给出的是更具体的时间数据。user=0.06标识系统用户状态消耗CPU的耗时,sys=0.00表示内核态CPU耗时,real=0.03表示总耗时。
Parallel日志
其实如果有了上述的日志示例,其实具体的日志也没啥可总结的了,但是不同的垃圾收集器的所有流程步骤都会在日志中体现。
日志示例中的一行日志就是来自Parallel垃圾收集器的日志,这里也不再赘述,只是需要说明的是,真正的GC日志中会打印出所有的JVM参数,如下所示:
CommandLine flags: -XX:-BytecodeVerificationLocal -XX:-BytecodeVerificationRemote -XX:InitialHeapSize=15728640 -XX:+ManagementServer -XX:MaxHeapSize=15728640 -XX:MaxNewSize=15728640 -XX:NewSize=15728640 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:TieredStopAtLevel=1 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
CMS日志
之前我们总结的时候,CMS垃圾收集器分为四个阶段:1、初始标记,2、并发标记,3、重新标记,4、并发清除四个阶段。这些都在日志中会有所体现
`0.975: [GC (Allocation Failure) 2020-01-20T19:33:34.626+0800: 0.975: [ParNew (promotion failed): 12288K->13824K(13824K), 0.0056167 secs]2020-01-20T19:33:34.632+0800: 0.981: [CMS: 457K->1023K(1024K), 0.0112219 secs] 12288K->2189K(14848K), [Metaspace: 8327K->8327K(1056768K)], 0.0171089 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
0.997: [GC (CMS Initial Mark) [1 CMS-initial-mark: 1023K(1024K)] 2428K(14848K), 0.0014480 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.999: [CMS-concurrent-mark-start]
1.000: [CMS-concurrent-mark: 0.002/0.002 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
1.001: [CMS-concurrent-preclean-start]
1.002: [CMS-concurrent-preclean: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
1.003: [GC (CMS Final Remark) [YG occupancy: 1645 K (13824 K)]2020-01-20T19:33:34.653+0800: 1.003: [Rescan (parallel) , 0.0016054 secs]2020-01-20T19:33:34.655+0800: 1.004: [weak refs processing, 0.0000257 secs]2020-01-20T19:33:34.655+0800: 1.004: [class unloading, 0.0008643 secs]2020-01-20T19:33:34.656+0800: 1.005: [scrub symbol table, 0.0015139 secs]2020-01-20T19:33:34.658+0800: 1.007: [scrub string table, 0.0002249 secs][1 CMS-remark: 1023K(1024K)] 2669K(14848K), 0.0044642 secs] [Times: user=0.05 sys=0.00, real=0.00 secs]
1.007: [CMS-concurrent-sweep-start]
1.008: [CMS-concurrent-sweep: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
1.008: [CMS-concurrent-reset-start]
1.008: [CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] `
上述贴出来的日志只有两个阶段(由于程序过于简单,没有并发标记和并发清除)但在线上分析该日志的时候会有该阶段。
G1日志
G1日志相比其他的垃圾收集器日志就复杂的多了。如果想要详细理解G1日志,可以通过这个链接:G1 日志 Oracle官方博客
同样的针对G1垃圾回收的各个阶段,在日志中都有所表现。之前总结过G1有一个比较牛逼的地方就是,可以动态设置GC停顿时间(参数:-XX:MaxGCPauseMillis=n),这是一个软指标,JVM会尽量达到这个标准。这是一把双刃剑,如果堆内存过小,这个参数设置时间过长则用户线程响应时间过长,如果这个参数设置过短,G1为了达到这个时间阈值只能回收部分内存空间,GC回收并不彻底。
查看日志的工具
其实有很多但常用的有两个,GC easy 和GCViewer
gc easy
gc easy 直接将生成的GC日志上传到这个地址即可完成解析。
GCViewer
这个也是直接上传gc日志即可。之后会显示吞吐量,内存空间等。
GC调优实例(G1)
先仅仅只是使用G1
设置JVM参数:-XX:+UseG1GC,之后启动应用程序。应用程序设置参数如下:-Xmx15M -Xms15M -Xmn15M -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+UseG1GC -Xloggc:F:\G1-gc.log
堆得大小设置为15M,之后利用GCViewer分析GC日志,之后会看出如下数据:
| Throughput | Min Pause | Max Pause | Avg Pause | GC times | comment |
|---|---|---|---|---|---|
| 90.71% | 0.00004 | 0.06916 | 0.01925 | 51 | 仅仅只是使用G1 |
吞吐量90.71%,最小的停顿时间为0.04ms,最大停顿时间为69ms,平均停顿时间为19.25ms,GC次数为51次。可以看到GC确实过于频繁,为了减少GC的次数,我们首先的反应是增大堆的容量。
增大堆的容量
应用程序设置参数如下:-Xmx25M -Xms25M -Xmn25M -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+UseG1GC -Xloggc:F:\G1-bigger-gc.log
堆大小增加了10M,总共25M,之后启动应用程序,可以得到如下数据:
| Throughput | Min Pause | Max Pause | Avg Pause | GC times | comment |
|---|---|---|---|---|---|
| 91.61% | 0.00141 | 0.06844 | 0.02688 | 22 | 堆的大小为25M |
可以看到通过增加堆的容量之后,吞吐量增大,各项指标均减少,但是平均GC的耗时增加,GC次数减少。
设置GC停顿时间
之前提到过G1能设置GC响应的时长。应用程序设置如下:-Xmx25M -Xms25M -Xmn25M -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:MaxGCPauseMillis=15 -XX:+UseG1GC -Xloggc:F:\G1-stoptime-gc.log
之后会有如下数据:
| Throughput | Min Pause | Max Pause | Avg Pause | GC times | comment |
|---|---|---|---|---|---|
| 96.25% | 0.00003 | 0.06807 | 0.00954 | 26 | 设置STW时间为20MS |
最小的停顿时间为3毫秒,最大的停顿时间为68毫秒,但是平均停顿时间为26毫秒,我们通过-XX:MaxGCPauseMillils设置为15,这个时候可以明显的看到,吞吐量增加(毕竟GC时间有所限制了)平均停顿时间降到9毫秒,GC次数相比之前有所增加(毕竟为了达到最小时间,不得不多GC几次)。
设置触发GC的内存使用占比
通过InitiatingHeapOccupancyPercent参数进行设置这个占比,默认情况下这个数据是45%,可以通过jps和jinfo进行查看,如下所示。
InitiatingHeapOccupancyPercent这个参数是设置GC操作基于整个堆的使用率,而不只是某一代内存的使用比例,值为0则表示一直执行G循环操作,默认值为45%,如上图所示。
GC调优官方指导
关于GC调优,官方早就有些指导了,连接地址为:Oracle G1调优官方指导 可以看到官方文档中给出了一些建议。
`When you evaluate and fine-tune G1 GC, keep the following recommendations in mind:
-
Young Generation Size: Avoid explicitly setting young generation size with the
-Xmnoption or any or other related option such as-XX:NewRatio. Fixing the size of the young generation overrides the target pause-time goal. -
Pause Time Goals: When you evaluate or tune any garbage collection, there is always a latency versus throughput trade-off. The G1 GC is an incremental garbage collector with uniform pauses, but also more overhead on the application threads. The throughput goal for the G1 GC is 90 percent application time and 10 percent garbage collection time. Compare this to the Java HotSpot VM parallel collector. The throughput goal of the parallel collector is 99 percent application time and 1 percent garbage collection time. Therefore, when you evaluate the G1 GC for throughput, relax your pause time target. Setting too aggressive a goal indicates that you are willing to bear an increase in garbage collection overhead, which has a direct effect on throughput. When you evaluate the G1 GC for latency, you set your desired (soft) real-time goal, and the G1 GC will try to meet it. As a side effect, throughput may suffer. See the section Pause Time Goal in Garbage-First Garbage Collector for additional information.
-
Taming Mixed Garbage Collections: Experiment with the following options when you tune mixed garbage collections. See the section Important Defaults for information about these options:
-
-XX:InitiatingHeapOccupancyPercent: Use to change the marking threshold. -
-XX:G1MixedGCLiveThresholdPercentand-XX:G1HeapWastePercent: Use to change the mixed garbage collection decisions. -
-XX:G1MixedGCCountTargetand-XX:G1OldCSetRegionThresholdPercent: Use to adjust the CSet for old regions.
-
1、新生代的大小:避免通过-Xmn参严格设置新生代的初始大小,也尽量避免用-XX:NewRatio参数设置新生代大小和from/to区的比例(因为G1垃圾收集器在运行过程中会自动调整新生代的大小,用以达到用于为垃圾收集器设置的暂定时间,如果手动设置了大小,就意味着放弃了G1的自动调优)。
2、暂停时间目标:我们使用任何垃圾收集器都会有吞吐量和响应时间的一个权衡,但是G1GC是一个有着统一暂停的增量式垃圾收集器,但是在应用线程上也有更多的系统开销。G1 GC的吞吐量目标是90%的应用程序和10%的垃圾收集程序耗时,将这些与Java HotSpot的垃圾收集器比较,并行收集器的吞吐量目标是99%的应用程序时间和1%的垃圾收集时间。因此,在评估G1 GC的吞吐量的时候,需要放宽暂停时间的目标。如果将目标时间设置的过短则应用程序就需要承担更多的垃圾收集的时间开销,这样吞吐量就会降低。这些也说过很多次了。
3、调整G1垃圾收集器还可以有如下参数:
-XX:InitiatingHeapOccupancyPercent:用于设置触发垃圾收集器的堆使用占比
-XX:G1MixedGCLiveThresholdPercent/-XX:G1HeapWastePercent:用于设置运行浪费的堆内存占比
-XX:G1MixedGCCountTarget/-XX:G1OldCSetRegionThresholdPercent:用于调整region的CSet大小(CSet:用于记录引用的集合)。
高并发场景下JVM调优
在高并发条件下,进行JVM调优,我们需要做的是明确问题,并通过相关的操作进行问题的排查。
1、如果出现GC频繁,我们需要打印出GC日志,然后查看Minor GC/Major GC,并结合工具gc viewer和gceasy.io等方式进行日志的查看。分析出GC过长的原因,如果GC时间过长,可以通过尝试适当增加堆内存大小或者更换垃圾收集器等操作。
2、如果出现死锁,需要通过jstack查看线程堆栈信息,查看各个线程状态,如果不影响业务则直接kill掉相关线程。如果可以通过分布式锁完成尽量用分布式锁完成相关锁的操作。
3、如果出现OOM,则需要导出dump文件,并通过MAT等工具进行dump文件分析,分析出占用堆较大的对象。
4、线程池不够,如果出现线程池分配不够,则需要使用jconsole,jvsualvm等操作分析线程池的操作。如果线程池不够,则需要增大线程池容量。
5、CPU负载过高,CPU负载过高,很大的一个原因是线程切换频繁,GC操作频繁,或者有些代码没有正确释放锁,因此,需要通过分析日志文件,查看线程状态。后续要优化多线程代码,通过集群部署减少单点系统压力,利用一些消息中间件实现消息的异步处理。
常见的面试问题
内存泄漏与内存溢出的区别
内存溢出(OOM)
JVM没有足够的内存空间进行对象的分配,会直接导致OOM
内存泄漏
对象无法得到及时的回收,持续占用内存空间,从而造成内存空间浪费。内存泄漏到一定的程度就会导致内存溢出,但是内存溢出也有可能是大对象导致的。
Young GC是否会有STW?
必须有,一定有,因为任何垃圾收集器都有STW,不然怎么回收垃圾?
Major GC和Full GC的区别
Major GC专指针对老年代的GC操作,Full GC等于Young+old+metaspace的GC操作。
G1与CMS的区别是什么
CMS是适用于老年代的垃圾收集器,G1老年代和新生代都适用。
G1使用了Region的思想对堆进行划分,且基于垃圾优先的算法进行垃圾收集整理。
什么是直接内存
直接内存——Java堆外内存,直接向系统申请的内存空间,通常访问直接内存的速度会优于Java堆,因此出于性能考虑,读写频繁的场合可能会考虑使用直接内存。
不可达对象一定会回收么?
看是否处于正式垃圾回收的阶段,如果处于初始标记阶段,标记为不可达对象,则不一定会被回收,要正式宣告一个对象为不可达对象,则至少需要经过两次标记过程——可达性分析中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize方法,如果在finalize方法中,对该对象进行了重新的引用指向,则这个对象也不能被回收。
方法区中会回收什么对象
方法区主要回收的是无用的类的元数据,那么如何判断一个类是无用的类?
判定一个常量是否是“废弃的常量”比较简单。而判定一个类是否是”无用的类“的条件则相对苛刻许多,类需要同时满足下面三个条件才能算是”无用的类“:
1、该类所需要的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
2、加载该类的ClassLoader已经被回收
3、该类对应的Class对象已经没有被引用,无法在任何地方通过反射访问该类的方法。
满足上面三个条件之后才会被标记,才可能被回收。
总结
本篇博客简单总结了JVM的相关调优操作,并总结了一些简单的面试问题,至此JVM的相关总结到此结束,针对类文件,类字节码后续还需要进一步总结。