JVM调优
JVM调优经历
系统优化:
一般系统优化思路是这样的:数据库->应用->JVM->网络和操作系统
1. 首先排查是否为数据库的问题,这个过程中就需要评估自己建的索引是否合理、是否需要引入分布式缓存、是否需要分库分表等等。
2. 然后考虑应用是否需要扩容(横向和纵向都会考虑,有可能是系统的压力过大或者是系统的硬件能力不足导致系统频繁出现问题)。应用代码层面上排查并优化,需要审视自己写的代码是否存在资源浪费的问题,又或者是在逻辑上可存在优化的地方,比如说通过并行的方式处理某些请求。
3. 再接着,JVM层面上排查并优化,这个过程我们观察JVM是否存在多次GC问题等等。
4. 最后,网络和操作系统层面排查,这个过程查看内存/CPU/网络/硬盘读写指标是否正常等等。
JVM调优可分为以下步骤:
一、分析系统系统运行情况
实时监控:通过Linux命令、JDK相关命令、实时监控平台查看CPU性能、内存占用、GC回收情况。
事后分析:分析GC日志及堆转储快照文件,判断是否需要优化,确定问题点;
一般我们是「遇到问题」之后才进行调优的,而遇到问题后需要利用各种的「工具」进行排查
1. jps:查看JVM进程基础信息(进程号、主类)
2. jstat:查看Java进程统计类相关的信息,查看各个区内存和GC的情况
3. jinfo:查看JVM的配置信息。
4. jmap:JVM堆内存分析工具,常用于把JVM内存使用情况dump到文件,然后再用MAT( Memory Analyzer tool)、VisualVM等工具对文件进行分析
5. jstack:查看JVM线程信息, jstack可以定位到线程堆栈,根据堆栈信息我们可以定位到具体代码。这个命令用常用于排查死锁相关的问题
6. jconsole、jvisualvm:带图形界面的JVM监控工具,查看JVM基本情况、做栈和堆转储等。
7. Arthas: 还有近期比较热门的Arthas(阿里开源的诊断工具),涵盖了上面很多命令的功能且自带图形化界面。
二、确定JVM调优量化目标
调优的最终目的都是为了让应用程序使用最小的硬件消耗来承载更大的吞吐。
- 低延迟:GC低停顿和GC低频率;
- 高吞吐量;
- 低内存使用率;
下面展示了一些JVM调优的量化目标参考实例:
- Heap 内存使用率 <= 70%;
- Old generation内存使用率<= 70%;
- avgpause <= 1秒;
- Full gc 次数0 或 avg pause interval >= 24小时 ;
三、选择合适的垃圾回收器
CPU单核,那么毫无疑问Serial 垃圾收集器是你唯一的选择。
CPU多核,对系统吞吐量有较高要求,那么选择PS+PO组合。(
CPU多核,对响应时间要求比较高,JDK版本1.6或者1.7,那么选择CMS。
CPU多核,对响应时间要求比较高,JDK1.8及以上,JVM可用内存6G以上,那么选择G1。
四、调整JVM参数配置
对比观察调优前后的差异,不断的分析和调整,直到找到合适的JVM参数配置,并进行后续跟踪。
内存空间的分配设置:JVM 内存分配不合理带来的性能表现并不会像内存溢出问题这么突出,最直接的表现就是频繁的 GC,这会导致上下文切换等性能问题,从而降低系统的吞吐量、增加系统的响应时间。具体的实现包括调整堆内存空间减少 Full GC、调整年轻代减少 MinorGC、设置合理的 Eden 和 Survivor 区的比例。
一般调优JVM我们认为会有几种指标可以参考:『吞吐量』、『停顿时间』和『垃圾回收频率』。基于这些指标,我们就有可能需要调整:
1. 内存区域大小以及相关策略(比如整块堆内存占多少、新生代占多少、老年代占多少、Survivor占多少、晋升老年代的条件等等)。按经验来说:IO密集型的可以稍微把「年轻代」空间加大些,因为大多数对象都是在年轻代就会灭亡。内存计算密集型的可以稍微把「老年代」空间加大些,对象存活时间会更长些)
-Xmx:设置堆的最大值、
-Xms:设置堆的初始值、
-Xmn:表示年轻代的大小、
-XX:SurvivorRatio:伊甸区和幸存区的比例
2. 垃圾回收器(选择合适的垃圾回收器,以及各个垃圾回收器的各种调优参数)
-XX:+UseG1GC:指定 JVM 使用的垃圾回收器为 G1、
-XX:MaxGCPauseMillis:设置目标停顿时间、
-XX:InitiatingHeapOccupancyPercent:当整个堆内存使用达到一定比例,全局并发标记阶段 就会被启动等
7.2、调整内存大小
现象:垃圾收集频率非常频繁。
原因:如果内存太小,就会导致频繁的需要进行垃圾收集才能释放出足够的空间来创建新的对象,所以增加堆内存大小的效果是非常显而易见的。
注意:如果垃圾收集次数非常频繁,但是每次能回收的对象非常少,那么这个时候并非内存太小,而可能是内存泄露导致对象无法回收,从而造成频繁GC。
参数配置:
//设置堆初始值
指令1:-Xms2g
指令2:-XX:InitialHeapSize=2048m
//设置堆区最大值
指令1:`-Xmx2g`
指令2: -XX:MaxHeapSize=2048m
//新生代内存配置
指令1:-Xmn512m
指令2:-XX:MaxNewSize=512m
7.3、设置符合预期的停顿时间
现象:程序间接性的卡顿
原因:如果没有确切的停顿时间设定,垃圾收集器以吞吐量为主,那么垃圾收集时间就会不稳定。
注意:不要设置不切实际的停顿时间,单次时间越短也意味着需要更多的GC次数才能回收完原有数量的垃圾.
参数配置:
//GC停顿时间,垃圾收集器会尝试用各种手段达到这个时间
-XX:MaxGCPauseMillis
7.4、调整内存区域大小比率
现象:某一个区域的GC频繁,其他都正常。
原因:如果对应区域空间不足,导致需要频繁GC来释放空间,在JVM堆内存无法增加的情况下,可以调整对应区域的大小比率。
注意:也许并非空间不足,而是因为内存泄造成内存无法回收。从而导致GC频繁。
参数配置:
//survivor区和Eden区大小比率
指令:-XX:SurvivorRatio=6 //S区和Eden区占新生代比率为1:6,两个S区2:6
//新生代和老年代的占比
-XX:NewRatio=4 //表示新生代:老年代 = 1:4 即老年代占整个堆的4/5;默认值=2
7.5、调整对象升老年代的年龄
现象:老年代频繁GC,每次回收的对象很多。
原因:如果升代年龄小,新生代的对象很快就进入老年代了,导致老年代对象变多,而这些对象其实在随后的很短时间内就可以回收,这时候可以调整对象的升级代年龄,让对象不那么容易进入老年代解决老年代空间不足频繁GC问题。
注意:增加了年龄之后,这些对象在新生代的时间会变长可能导致新生代的GC频率增加,并且频繁复制这些对象新生的GC时间也可能变长。
配置参数:
//进入老年代最小的GC年龄,年轻代对象转换为老年代对象最小年龄值,默认值7
-XX:InitialTenuringThreshol=7
7.6、调整大对象的标准
现象:老年代频繁GC,每次回收的对象很多,而且单个对象的体积都比较大。
原因:如果大量的大对象直接分配到老年代,导致老年代容易被填满而造成频繁GC,可设置对象直接进入老年代的标准。
注意:这些大对象进入新生代后可能会使新生代的GC频率和时间增加。
配置参数:
//新生代可容纳的最大对象,大于则直接会分配到老年代,0代表没有限制。
-XX:PretenureSizeThreshold=1000000
7.7、调整GC的触发时机
现象:CMS,G1 经常 Full GC,程序卡顿严重。
原因:G1和CMS 部分GC阶段是并发进行的,业务线程和垃圾收集线程一起工作,也就说明垃圾收集的过程中业务线程会生成新的对象,所以在GC的时候需要预留一部分内存空间来容纳新产生的对象,如果这个时候内存空间不足以容纳新产生的对象,那么JVM就会停止并发收集暂停所有业务线程(STW)来保证垃圾收集的正常运行。这个时候可以调整GC触发的时机(比如在老年代占用60%就触发GC),这样就可以预留足够的空间来让业务线程创建的对象有足够的空间分配。
注意:提早触发GC会增加老年代GC的频率。
配置参数:
//使用多少比例的老年代后开始CMS收集,默认是68%,如果频繁发生SerialOld卡顿,应该调小
-XX:CMSInitiatingOccupancyFraction
//G1混合垃圾回收周期中要包括的旧区域设置占用率阈值。默认占用率为 65%
-XX:G1MixedGCLiveThresholdPercent=65
7.8、调整 JVM本地内存大小
现象:GC的次数、时间和回收的对象都正常,堆内存空间充足,但是报OOM
原因: JVM除了堆内存之外还有一块堆外内存,这片内存也叫本地内存,可是这块内存区域不足了并不会主动触发GC,只有在堆内存区域触发的时候顺带会把本地内存回收了,而一旦本地内存分配不足就会直接报OOM异常。
注意: 本地内存异常的时候除了上面的现象之外,异常信息可能是OutOfMemoryError:Direct buffer memory。 解决方式除了调整本地内存大小之外,也可以在出现此异常时进行捕获,手动触发GC(System.gc())。
配置参数:
XX:MaxDirectMemorySize
作者:三分恶
链接:https://www.zhihu.com/question/362201242/answer/2290090371
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
链接:https://www.zhihu.com/question/362201242/answer/2290090371
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
8、JVM调优实例
以下是整理自网络的一些JVM调优实例:
8.1、网站流量浏览量暴增后,网站反应页面响很慢
1、问题推测:在测试环境测速度比较快,但是一到生产就变慢,所以推测可能是因为垃圾收集导致的业务线程停顿。
2、定位:为了确认推测的正确性,在线上通过jstat -gc 指令 看到JVM进行GC 次数频率非常高,GC所占用的时间非常长,所以基本推断就是因为GC频率非常高,所以导致业务线程经常停顿,从而造成网页反应很慢。
3、解决方案:因为网页访问量很高,所以对象创建速度非常快,导致堆内存容易填满从而频繁GC,所以这里问题在于新生代内存太小,所以这里可以增加JVM内存就行了,所以初步从原来的2G内存增加到16G内存。
4、第二个问题:增加内存后的确平常的请求比较快了,但是又出现了另外一个问题,就是不定期的会间断性的卡顿,而且单次卡顿的时间要比之前要长很多。
5、问题推测:练习到是之前的优化加大了内存,所以推测可能是因为内存加大了,从而导致单次GC的时间变长从而导致间接性的卡顿。
6、定位:还是通过jstat -gc 指令 查看到 的确FGC次数并不是很高,但是花费在FGC上的时间是非常高的,根据GC日志 查看到单次FGC的时间有达到几十秒的。
7、解决方案: 因为JVM默认使用的是PS+PO的组合,PS+PO垃圾标记和收集阶段都是STW,所以内存加大了之后,需要进行垃圾回收的时间就变长了,所以这里要想避免单次GC时间过长,所以需要更换并发类的收集器,因为当前的JDK版本为1.7,所以最后选择CMS垃圾收集器,根据之前垃圾收集情况设置了一个预期的停顿的时间,上线后网站再也没有了卡顿问题。
8.2、后台导出数据引发的OOM
**问题描述:**公司的后台系统,偶发性的引发OOM异常,堆内存溢出。
1、因为是偶发性的,所以第一次简单的认为就是堆内存不足导致,所以单方面的加大了堆内存从4G调整到8G。
2、但是问题依然没有解决,只能从堆内存信息下手,通过开启了-XX:+HeapDumpOnOutOfMemoryError参数 获得堆内存的dump文件。
3、VisualVM 对 堆dump文件进行分析,通过VisualVM查看到占用内存最大的对象是String对象,本来想跟踪着String对象找到其引用的地方,但dump文件太大,跟踪进去的时候总是卡死,而String对象占用比较多也比较正常,最开始也没有认定就是这里的问题,于是就从线程信息里面找突破点。
4、通过线程进行分析,先找到了几个正在运行的业务线程,然后逐一跟进业务线程看了下代码,发现有个引起我注意的方法,导出订单信息。
5、因为订单信息导出这个方法可能会有几万的数据量,首先要从数据库里面查询出来订单信息,然后把订单信息生成excel,这个过程会产生大量的String对象。
6、为了验证自己的猜想,于是准备登录后台去测试下,结果在测试的过程中发现到处订单的按钮前端居然没有做点击后按钮置灰交互事件,结果按钮可以一直点,因为导出订单数据本来就非常慢,使用的人员可能发现点击后很久后页面都没反应,结果就一直点,结果就大量的请求进入到后台,堆内存产生了大量的订单对象和EXCEL对象,而且方法执行非常慢,导致这一段时间内这些对象都无法被回收,所以最终导致内存溢出。
7、知道了问题就容易解决了,最终没有调整任何JVM参数,只是在前端的导出订单按钮上加上了置灰状态,等后端响应之后按钮才可以进行点击,然后减少了查询订单信息的非必要字段来减少生成对象的体积,然后问题就解决了。
8.3、单个缓存数据过大导致的系统CPU飚高
1、系统发布后发现CPU一直飚高到600%,发现这个问题后首先要做的是定位到是哪个应用占用CPU高,通过top 找到了对应的一个java应用占用CPU资源600%。
2、如果是应用的CPU飚高,那么基本上可以定位可能是锁资源竞争,或者是频繁GC造成的。
3、所以准备首先从GC的情况排查,如果GC正常的话再从线程的角度排查,首先使用jstat -gc PID 指令打印出GC的信息,结果得到得到的GC 统计信息有明显的异常,应用在运行了才几分钟的情况下GC的时间就占用了482秒,那么问这很明显就是频繁GC导致的CPU飚高。
4、定位到了是GC的问题,那么下一步就是找到频繁GC的原因了,所以可以从两方面定位了,可能是哪个地方频繁创建对象,或者就是有内存泄露导致内存回收不掉。
5、根据这个思路决定把堆内存信息dump下来看一下,使用jmap -dump 指令把堆内存信息dump下来(堆内存空间大的慎用这个指令否则容易导致会影响应用,因为我们的堆内存空间才2G所以也就没考虑这个问题了)。
6、把堆内存信息dump下来后,就使用visualVM进行离线分析了,首先从占用内存最多的对象中查找,结果排名第三看到一个业务VO占用堆内存约10%的空间,很明显这个对象是有问题的。
7、通过业务对象找到了对应的业务代码,通过代码的分析找到了一个可疑之处,这个业务对象是查看新闻资讯信息生成的对象,由于想提升查询的效率,所以把新闻资讯保存到了redis缓存里面,每次调用资讯接口都是从缓存里面获取。
8、把新闻保存到redis缓存里面这个方式是没有问题的,有问题的是新闻的50000多条数据都是保存在一个key里面,这样就导致每次调用查询新闻接口都会从redis里面把50000多条数据都拿出来,再做筛选分页拿出10条返回给前端。50000多条数据也就意味着会产生50000多个对象,每个对象280个字节左右,50000个对象就有13.3M,这就意味着只要查看一次新闻信息就会产生至少13.3M的对象,那么并发请求量只要到10,那么每秒钟都会产生133M的对象,而这种大对象会被直接分配到老年代,这样的话一个2G大小的老年代内存,只需要几秒就会塞满,从而触发GC。
9、知道了问题所在后那么就容易解决了,问题是因为单个缓存过大造成的,那么只需要把缓存减小就行了,这里只需要把缓存以页的粒度进行缓存就行了,每个key缓存10条作为返回给前端1页的数据,这样的话每次查询新闻信息只会从缓存拿出10条数据,就避免了此问题的 产生。
8.4、CPU经常100% 问题定位
问题分析:CPU高一定是某个程序长期占用了CPU资源。
1、所以先需要找出那个进行占用CPU高。
top 列出系统各个进程的资源占用情况。
2、然后根据找到对应进行里哪个线程占用CPU高。
top -Hp 进程ID 列出对应进程里面的线程占用资源情况
3、找到对应线程ID后,再打印出对应线程的堆栈信息
printf "%x\n" PID 把线程ID转换为16进制。
jstack PID 打印出进程的所有线程信息,从打印出来的线程信息中找到上一步转换为16进制的线程ID对应的线程信息。
4、最后根据线程的堆栈信息定位到具体业务方法,从代码逻辑中找到问题所在。
查看是否有线程长时间的watting 或blocked
如果线程长期处于watting状态下, 关注watting on xxxxxx,说明线程在等待这把锁,然后根据锁的地址找到持有锁的线程。
8.5、内存飚高问题定位
分析: 内存飚高如果是发生在java进程上,一般是因为创建了大量对象所导致,持续飚高说明垃圾回收跟不上对象创建的速度,或者内存泄露导致对象无法回收。
1、先观察垃圾回收的情况
jstat -gc PID 1000 查看GC次数,时间等信息,每隔一秒打印一次。
jmap -histo PID | head -20 查看堆内存占用空间最大的前20个对象类型,可初步查看是哪个对象占用了内存。
如果每次GC次数频繁,而且每次回收的内存空间也正常,那说明是因为对象创建速度快导致内存一直占用很高;如果每次回收的内存非常少,那么很可能是因为内存泄露导致内存一直无法被回收。
2、导出堆内存文件快照
jmap -dump:live,format=b,file=/home/myheapdump.hprof PID dump堆内存信息到文件。
3、使用visualVM对dump文件进行离线分析,找到占用内存高的对象,再找到创建该对象的业务代码位置,从代码和业务场景中定位具体问题。
8.6、数据分析平台系统频繁 Full GC
平台主要对用户在 App 中行为进行定时分析统计,并支持报表导出,使用 CMS GC 算法。
数据分析师在使用中发现系统页面打开经常卡顿,通过 jstat 命令发现系统每次 Young GC 后大约有 10% 的存活对象进入老年代。
原来是因为 Survivor 区空间设置过小,每次 Young GC 后存活对象在 Survivor 区域放不下,提前进入老年代。
通过调大 Survivor 区,使得 Survivor 区可以容纳 Young GC 后存活对象,对象在 Survivor 区经历多次 Young GC 达到年龄阈值才进入老年代。
调整之后每次 Young GC 后进入老年代的存活对象稳定运行时仅几百 Kb,Full GC 频率大大降低。
8.7、业务对接网关 OOM
网关主要消费 Kafka 数据,进行数据处理计算然后转发到另外的 Kafka 队列,系统运行几个小时候出现 OOM,重启系统几个小时之后又 OOM。
通过 jmap 导出堆内存,在 eclipse MAT 工具分析才找出原因:代码中将某个业务 Kafka 的 topic 数据进行日志异步打印,该业务数据量较大,大量对象堆积在内存中等待被打印,导致 OOM。
8.8、鉴权系统频繁长时间 Full GC
系统对外提供各种账号鉴权服务,使用时发现系统经常服务不可用,通过 Zabbix 的监控平台监控发现系统频繁发生长时间 Full GC,且触发时老年代的堆内存通常并没有占满,发现原来是业务代码中调用了 System.gc()。
作者:极客时间
链接:https://www.zhihu.com/question/362201242/answer/1115831177
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
链接:https://www.zhihu.com/question/362201242/answer/2081679563
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
1.1 Serial/Serial Old 收集器:
Serial 收集器作用于年轻代中, 采用 复制算法 , 属于串行回收方式
Serial Old 收集器 采用串行回收, STW机制, 采用 标记-压缩 算法 ,
"-XX:+UseSerialGC" 手动指定Serial收集器执行内存回收任务
"-XX:+PrintGCDetails" 年轻代串行收集器的工作日志开关
1.2 ParNew 收集器
ParnNew 可以说是Serial收集器的多线程版本, ParNew 收集器在 年轻代 中同样才用的也是复制算法 和 STW 机制 并行回收机制。
ParnNew 收集器的优势体现在多CPU,多核心的环境中, 在某些注重低延迟的应用场景下ParNew 和 CMS 收集器的组合模式, 在Server 模式下的内存回收效果很好。
使用 "-XX:+UserParNewGC" 手动指定使用ParNew收集器 "
-XX:+UserParallelGC" 表示 年轻代使用并行垃圾回收器, 老年代使用串行收集器
1.3 Parallel/Parallel Old 收集器
Parallel收集器是并行回收 采用复制算法 年轻代 STW , 和ParNew不同Parrllel收集器可以控制程序的吞吐量大小 , 被称为-吞吐量优先的垃圾收集器.
Parallel Old采用标记整理算法,用于老年代的垃圾回收。
常用参数:
"-XX:+GCTimeRatio:N" 设置执行内存回收的时间所占JVM运行总时间的比例, 1/(1+N) 默认N为99
"-XX:+MaxGCPauseMills" 设置执行内存回收STW 的暂停时间阈值, 若指定该值,则尽可能地在设定的时间内完成内存回收。
"-XX:+UseAdaptiveSizePolicy" 选项用于设置GC的自动分代大小调节策略。
"-XX:UseParallelOldGC"可在年轻代和老年代都是用并行回收收集器, 此收集器重点关注吞吐量
"-XX:ParallelGCThreads" 可用于设置垃圾回收时的线程数量
Parallel Old 收集器采用了 标记-压缩算法 , 用于老年代垃圾回收 , 并行回收 STW
Parallel 和 Parallel Old 收集器的组合 在Server 模式下的内存回收性能较好。
1.4 CMS(Concurrent-Mark-Sweep) 收集器
基于低延迟的考虑 , 是并行垃圾回收器, 而且是老年代垃圾收集, 低延迟, 采用标记清除算法。会有短暂的STW
基本步骤如下 :
1. 初始标记(Initial Mark) : STW 标记根对象直接关联、可达的对象
2. 并发标记(Concurrent Mark) :将不可达对象 标记为垃圾对象
3. 再次标记(Remark) : STW 确保垃圾对象被成功且正确得标记
4. 并发清除(Concurrent Sweep): 垃圾回收
常用参数:
"-XX:+UseCMS-CompactAtFullCollection" 用于指定在执行完FullGC 之后 是否对内存空间进行压缩整理,
"-XX:+CMSFullGCs-BeforeCompaction" 设定在执行多少次FullGC 之后对内存空间进行压缩整理
"-XX:+CMSInitiatingOccupanyFraction" 设置老年代中的内存使用率达到多少百分比的时候执行内存回收 JDK1.6之前默认值为68% JDK1.6 之后默认92% , CMS垃圾收集器在回收过程中程依然可能会产生垃圾,所以需要设定一个阈值来进行垃圾回收,如果CMS回收失败,JVM则会启动老年代串行收集器进行垃圾回收,程序的STW时间会较长, 所以可以在内存增长缓慢的程序里面设置较大阈值,在内存增长快速的程序里面设置较小的阈值, 避免触发老年代串行收集器。
"-XX:UseConMarkSweepGC" 表示年轻代使用并行收集器,老年代使用CMS 年轻代 并行收集器工作时的线程数量可以使用 "-XX:ParallelGCThreads" 选项指定, 一般最好与CPU的数量相当.
1.5 G1(Garbage-First) 收集器
G1将整个堆划分为若干个大小相等的区间(1-32MB),Region类型分为Unused Region、Eden Region 和 Survivor Region组成了年轻代空间,Old Region, Humongous Region(里面的对象超过每个Region的50%),
每个Region都会有一个Region Set (RS),RS的数据结构是Hash表,里面的数据是CardTable (堆中没512Byte 映射在 card table 1 byte ) , 简单说 RS 里面存的是Region种存活对象的指针。 当Region中数据发生变化的时候 ,首先反映到Card Table 中的一个或者对个card上,RS通过扫面内部的Card Table 得知Region中内存使用情况和存活对象,在使用Region过程中,如果一个Region被填满了,分配内存的线程会重新选择一个新的Region,空闲Region被组织到一个基于链表的数据结构(LinkedList )中,这样可以快速找到Region.
G1 GC 可以分为 Young GC 和Mixed GC
年轻代GC:当年轻代达到一定的阈值,就开始年轻代的并发收集。将Eden Region中存活对象copy转移到Survivor的Region中,同时释放Eden Region 。 存活的时间够久就移到Old Reagion。
Mixed GC: 当Old Region占比达到一定的比例(通过 -XX:InitantingHeapOccupancyPercent 设置,默认45%)之后,会触发并行标记,然后就会进行Mixed GC。 Mixed GC 对一个叫做CSet的Region集合进行垃圾回收,其中包含了所有的年轻代Region和选取的一部分回收效益最好的Old Region。
并发标记的过程类似于CMS中的标记过程。
G1 可选优化参数
- 年轻代优化
-XX:G1NewSizePercent Java 堆初始化大小 ,默认是整个Java堆大小的5%,
-XX:G1MaxNewPercent 最大占用对内存的百分比,默认是60%
-XX:MaxGCPauseMills 每次目标停顿时间, 默认200ms
可以根据上述三个参数的调整来优化年轻代的垃圾回收
- 并行标记阶段优化
并行标记阶段, -XX:InitantingHeapOccupancyPercent 决定了什么时候初始化并行标记循环 默认值45%
-XX:ConcGCThreads 并发GC数量, 是-XX:ParallelGCThreads 的1/4 ,可以通过修改两个值来改变并线程的数量。 - 混合回收阶段优化
-XX:+PrintAdaptiveSizePoliy 开启后会输出完成的GC生态日志,
-XX:G1HeapWastePercent 表示最大可忍受的垃圾总量,默认是堆空间的5%,如果Mixed GC占用的时间过多,可以将此值调整大一点。
-XX:G1MixedGCCountTarget 默认为8,表示Mixed GC过程中入选的Old Region的最小阈值,用于控制入选的Old Region的数量
-XX:G1OldCSetRegionThresholdPercent 默认值100% ,表示加入到CSet中的Old Region的最大数量。
-XX:G1MixedGCLiveThresholdPercent 默认值 85% ,表示设置的每CSet中每个区间最多存活对象的百分比。
2 JVM的一些常用参数
- -XX:+PrintGCDetails 用于记录GC运行时的详细信息并输出。
- -verbose:gc -Xloggc:gc.log 结合前一个选项可以在项目根目录下生成gc.log文件记录gc详细信息
- -XX:+UseSerialGC 使用串行gc收集器,使用与小于100MB的内存空间场景
- -XX:+UserParNewGC 独占式GC , 多线程GC (未来不可用)
- -XX:+UseParallelGC
- -XX:+UseParallelOldGC
- -XX:+UseConcMarkSweepGC
- -XX:+UseG1GC
- -XX:+PrintGCApplicationStoppedTime 输出GC造成应用程序暂停的时间
- -XX:+PrintGCApplicationConcurrentTime 与上面参数结合使用
- -XX:+ConcGCThreads=4 设置Java应用程序线程并行执行的GC线程数量 若设置的值超过JVM允许GC并行线程的数量则报错, 默认的并行标记线程数量计算如下 ConGCThreads = Max((ParallelGCThreads#+2)/4,1)
- -XX:G1HeapRegionSize G1 GC 独有,Region大小默认为堆的1/2000 也可以设置 1MB 2MB 4MB 8MB 16MB 32MB 等6个档次
- -XX:G1HeapWastePercent=5 控制G1GC 不会回收的空闲内存比例,默认是堆内存的5% G1 GC在回收过程中会回收所有Region的内存,并持续地进行回收工作,直到内存空闲比例达到次值
- -XX:G1MixedGCCountTarget=8 老年代Region 的回收时间通常来说比年轻带Region回收时间长,此选项设置并行循环之后启动多少个混合GC 默认值是 8个 设置一个比较大的值可以让G1 GC在老年代Region回收时多花一些时间,如果一个混合GC停顿的时间很长,说明它要做的使很多,所以可以增大这个值的设置,但这个值过大的话,会造成并行循环等待混合GC完成的时间也相应的增加。
- -XX:G1PrintRegionLivenessInfo 开启这个选项会在标记循环之后输出详细信息(诊断选项) 在使用之前需要开启-XX:UnlockDiagnosticVMOptions 选项, 此选项会打印内存内部每个Region里面存活的对象信息, 包括使用率 RSET大小、回收一个Region的价值(性价比) 15. -XX:G1ReservePercent=10 此选项默认保留对内存的10%,用于某个对象进入下一个阶段,预留内存空间不可用于年轻带
- -XX:+G1SummarizeRSetStats 打印每个Region的详细信息。 此选项和-XX:G1PrintRegionLivenessInfo选项一样,是一个诊断选项也需要开启 -XX:UnlockDiagnosticVMOptions 选项 。
- -XX:+G1TraceConcRefinement 诊断选项,启动这个选项,并行Refinement线程相关的信息会被打印,线程启动和结束时,信息都会被打印。
- -XX:G1UseAdaptiveConcRefinement 默认开启的选项, 它会动态地对每一次GC中-XX:G1ConcRefinementGreenZone、-XX:G1ConcRfinementYellowZone、-XX:-XX:G1ConcRfinementRedZone,的值进行重新计算 并行Refinement线程是持续运行的,并且会随着update log buffer 积累的数量而动态调节, -XX:G1ConcRefinementGreenZone、-XX:G1ConcRfinementYellowZone、-XX:-XX:G1ConcRfinementRedZone,三个选项是用来根据不同的buffer使用不同的Refinement线程,其作用就是保证Refinement线程尽可能更上update log buffer生产的的步伐
- -XX:GCTimeRatio=9 这个选项代表Java应用程序话费时间与GC线程花费时间的比率, 1/(1+GCTimeRatio) 默认值是9 表示花费在GC工作量上的时间占总时间的10%
- -XX:+HeapDumpBeforeFullGC/-XX:+HeapDumpAfterFullGC 启用此选项 , 在Full GC开始之前有一个hprof文件会被创建, 两个选项同时使用,可以对比Full GC前后的java堆内存,找出内存泄漏以及其他问题,
- -XX:InitiatingHeapOccypancyPercent=45 该选项默认值是45 表示G1 GC并行循环初始设置的堆大小值, 这个值决定了一个并行循环是不是要开始执行, 它的逻辑是在一次GC完成后,标胶老年代占用的空间和整个Java堆之间的比例,如果大于这个值,则预约下一次GC开始一个并行循环回收垃圾,从初始标记阶段开始。 这个值越小,GC 越频繁,反之 值越大,
- -XX:UseStringDeduplication 该选项启动String对象的去重工作,默认不启用。 如果启用该选项 String1.equals(String2) 如果两个对象包含相同的内容则返回true
- -XX:StringDeduplicationAgeThreshold=3 针对-XX:UseStringDeduplication选项,默认值为3 字符串对象的年龄超过设定的阈值,或者提升到G1 GC老年代Region之后,就会成为字符串去重的候选对象,去重操作只会有一次。
- -XX:PrintStringDeduplicationStatistics 可以通过读取输出的统计资料来了解是否字符串去重后节约了大量的堆内存空间,默认关闭
- -XX:+G1UseAdaptiveHOP JDK9的新选项 默认启用,通过动态调节标记阶段开始的时间,以达到提升应用程序吞吐量的目标, 主要通过尽可能迟地触发标记循环方式来避免消耗老年代空间,
- -XX:MaxGCPauseMills=200 【重要选项】设置G1的目标停顿时间 ,单位为ms 默认值为200ms
- -XX:MinHeapFreeRatio=40 设置对内可以空闲的最小的内存空间大小,默认为堆内存的40% 当空闲堆内存大小小于此值的时候,需要判断-Xms 和 -Xmx 两个初始化设置值,如果-Xms 和 -Xmx 不一样,那么就有机会扩展堆内存,否则就无法扩展。
- -X:MaxHeapFreeRatio =70 这只最大空闲空间大小,默认为堆内存的70%,当大于这个空闲比率的时候 G1 GC 会自动减少对内存的大小, 需要判断-Xms 和 -Xmx的大小 如果两者一样则有机会减小堆内存,否则无法减小堆内存
- -XX:+PrintAdaptiveSizePolicy 这个选项决定是否开启堆内存大小变化的相应记录信息打印, 是否打印这些星系到GC日志里面
- -XX:+ResizePLAB GC使用的本地线程分配缓存块采用动态值还是静态值 默认开启 31. -XX:+ResizeTLAB Java线程使用的本地线程分配缓存块采用 动态值还是静态值。 默认开启
- -XX:+ClassUnloadingWithCncurrent 开启G1 GC在并行循环卸载类,尤其是在老年代的并行回收阶段,默认是开启的。 这个选项开启后会在并行循环的重标记阶段卸载JVM没有用到的类, 这些工作也可以放到Full GC 里面去做。是否开启此选项要看性价比。 如果GC停顿时间比我们设置的最大GC停顿目标时间还长,并且需要卸载的类也不多,建议关闭此选项
- -XX:+ClassUnloading 默认是是True 决定JVM是否会卸载无用的类,如果关闭此选项,无论是并行回收循环还是Full GC 都不会再卸载这些类,所以需要谨慎关闭
- -XX:+UnloadingDiagnosticVMOptions 是否开启诊断选项,默认值是 False
- -XX:+UnlockExperimentalVMOptions 默认关闭
- -XX:+UnlockCommercialFeatures 是都使用Oracle特有特性, 默认关闭
注意:不同应用的JVM调优量化目标是不一样的。
在「解释」阶段,会有两种方式把字节码信息解释成机器指令码,一个是字节码解释器、一个是即时编译器(JIT),你了解JVM的JIT优化技术嘛?
JIT优化技术比较出名的有两种:方法内联和逃逸分析,不同的JVM版本对JIT的优化都不太相同, 这里也只能算是一个参考
方法内联:就是把「目标方法」的代码复制到「调用的方法」中,避免发生真实的方法调用。因为每次方法调用都会生成栈帧(压栈出栈记录方法调用位置等等)会带来一定的性能损耗,所以「方法内联」的优化可以提高一定的性能。在JVM中也有相关的参数给予我们指定(-XX:MaxFreqInlineSize、-XX:MaxInlineSize)
逃逸分析:则是判断一个对象是否被外部方法引用或外部线程访问的分析技术,如果「没有被引用」,就可以对其进行优化,比如说:
1. 锁消除(同步忽略):该对象只在方法内部被访问,不会被别的地方引用,那么就一定是线程安全的,可以把锁相关的代码给忽略掉
2. 栈上分配:该对象只会在方法内部被访问,直接将对象分配在「栈」中(Java默认是将对象分配在「堆」中,是需要通过JVM垃圾回收期进行回收,需要损耗一定的性能,而栈内分配则快很多)
3. 标量替换/分离对象:当程序真正执行的时候可以不创建这个对象,而直接创建它的成员变量来代替。将对象拆分后,可以分配对象的成员变量在栈或寄存器上,原本的对象就无需分配内存空间了