本文介绍GC基础原理和理论,GC调优方法思路和方法,基于Hotspot jdk1.8,学习以后将了解如何对生产系统出现的GC问题进行排查解决html
阅读时长约30分钟,内容主要以下:git
大多数状况下对 Java 程序进行GC调优, 主要关注两个目标:响应速度、吞吐量github
响应速度指程序或系统对一个请求的响应有多迅速。好比,用户订单查询响应时间,对响应速度要求很高的系统,较大的停顿时间是不可接受的。调优的重点是在短的时间内快速响应web
吞吐量关注在一个特定时间段内应用系统的最大工做量,例如每小时批处理系统能完成的任务数量,在吞吐量方面优化的系统,较长的GC停顿时间也是能够接受的,由于高吞吐量应用更关心的是如何尽量快地完成整个任务,不考虑快速响应用户请求算法
GC调优中,GC致使的应用暂停时间影响系统响应速度,GC处理线程的CPU使用率影响系统吞吐量segmentfault
现代的垃圾收集器基本都是采用分代收集算法,其主要思想:
将Java的堆内存逻辑上分红两块:新生代、老年代,针对不一样存活周期、不一样大小的对象采起不一样的垃圾回收策略安全
新生代又叫年轻代,大多数对象在新生代中被建立,不少对象的生命周期很短。每次新生代的垃圾回收(又称Young GC、Minor GC、YGC)后只有少许对象存活,因此使用复制算法,只需少许的复制操做成本就能够完成回收服务器
新生代内又分三个区:一个Eden区,两个Survivor区(S0、S1,又称From Survivor、To Survivor),大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到两个Survivor区(中的一个)。当这个Survivor区满时,此区的存活且不知足晋升到老年代条件的对象将被复制到另一个Survivor区。对象每经历一次复制,年龄加1,达到晋升年龄阈值后,转移到老年代多线程
在新生代中经历了N次垃圾回收后仍然存活的对象,就会被放到老年代,该区域中对象存活率高。老年代的垃圾回收一般使用“标记-整理”算法并发
根据垃圾收集回收的区域不一样,垃圾收集主要一般分为Young GC、Old GC、Full GC、Mixed GC
新生代内存的垃圾收集事件称为Young GC(又称Minor GC),当JVM没法为新对象分配在新生代内存空间时总会触发 Young GC,好比 Eden 区占满时。新对象分配频率越高, Young GC 的频率就越高
Young GC 每次都会引发全线停顿(Stop-The-World),暂停全部的应用线程,停顿时间相对老年代GC的形成的停顿,几乎能够忽略不计
Old GC,只清理老年代空间的GC事件,只有CMS的并发收集是这个模式
Full GC,清理整个堆的GC事件,包括新生代、老年代、元空间等
GC日志是一个很重要的工具,它准确记录了每一次的GC的执行时间和执行结果,经过分析GC日志能够调优堆设置和GC设置,或者改进应用程序的对象分配模式,开启的JVM启动参数以下:
-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps
常见的Young GC、Full GC日志含义以下:
免费的GC日志图形分析工具推荐下面2个:
),下载jar包直接运行
Java提供的自动内存管理,能够归结为解决了对象的内存分配和回收的问题,前面已经介绍了内存回收,下面介绍几条最广泛的内存分配策略
大多数状况下,对象在先新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Young GC
JVM提供了一个对象大小阈值参数(-XX:PretenureSizeThreshold,默认值为0,表明无论多大都是先在Eden中分配内存),大于参数设置的阈值值的对象直接在老年代分配,这样能够避免对象在Eden及两个Survivor直接发生大内存复制
对象每经历一次垃圾回收,且没被回收掉,它的年龄就增长1,大于年龄阈值参数(-XX:MaxTenuringThreshold,默认15)的对象,将晋升到老年代中
当进行Young GC以前,JVM须要预估:老年代是否可以容纳Young GC后新生代晋升到老年代的存活对象,以肯定是否须要提早触发GC回收老年代空间,基于空间分配担保策略来计算:
continueSize:老年代最大可用连续空间
Young GC以后若是成功(Young GC后晋升对象能放入老年代),则表明担保成功,不用再进行Full GC,提升性能;若是失败,则会出现“promotion failed”错误,表明担保失败,须要进行Full GC
新生代对象的年龄可能没达到阈值(MaxTenuringThreshold参数指定)就晋升老年代,若是Young GC以后,新生代存活对象达到相同年龄全部对象大小的总和大于任一Survivor空间(S0 或 S1总空间)的一半,此时S0或者S1区即将容纳不了存活的新生代对象,年龄大于或等于该年龄的对象就能够直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄
另外,若是Young GC后S0或S1区不足以容纳:未达到晋升老年代条件的新生代存活对象,会致使这些存活对象直接进入老年代,须要尽可能避免
可达性分析算法:用于判断对象是否存活,基本思想是经过一系列称为“GC Root”的对象做为起点(常见的GC Root有系统类加载器、栈中的对象、处于激活状态的线程等),基于对象引用关系,从GC Roots开始向下搜索,所走过的路径称为引用链,当一个对象到GC Root没有任何引用链相连,证实对象再也不存活
Stop The World:GC过程当中分析对象引用关系,为了保证分析结果的准确性,须要经过停顿全部Java执行线程,保证引用关系再也不动态变化,该停顿事件称为Stop The World(STW)
Safepoint:代码执行过程当中的一些特殊位置,当线程执行到这些位置的时候,说明虚拟机当前的状态是安全的,若是有须要GC,线程能够在这个位置暂停。HotSpot采用主动中断的方式,让执行线程在运行期轮询是否须要暂停的标志,若须要则中断挂起
CMS(Concurrent Mark and Swee 并发-标记-清除),是一款基于并发、使用标记清除算法的垃圾回收算法,只针对老年代进行垃圾回收。CMS收集器工做时,尽量让GC线程和用户线程并发执行,以达到下降STW时间的目的
经过如下命令行参数,启用CMS垃圾收集器:
-XX:+UseConcMarkSweepGC
值得补充的是,下面介绍到的CMS GC是指老年代的GC,而Full GC指的是整个堆的GC事件,包括新生代、老年代、元空间等,二者有所区分
能与CMS搭配使用的新生代垃圾收集器有Serial收集器和ParNew收集器。这2个收集器都采用标记复制算法,都会触发STW事件,中止全部的应用线程。不一样之处在于,Serial是单线程执行,ParNew是多线程执行
CMS GC以获取最小停顿时间为目的,尽量减小STW时间,能够分为7个阶段
此阶段的目标是标记老年代中全部存活的对象, 包括 GC Root 的直接引用, 以及由新生代中存活对象所引用的对象,触发第一次STW事件
这个过程是支持多线程的(JDK7以前单线程,JDK8以后并行,可经过参数CMSParallelInitialMarkEnabled调整)
此阶段GC线程和应用线程并发执行,遍历阶段1初始标记出来的存活对象,而后继续递归标记这些对象可达的对象
此阶段GC线程和应用线程也是并发执行,由于阶段2是与应用线程并发执行,可能有些引用关系已经发生改变。
经过卡片标记(Card Marking),提早把老年代空间逻辑划分为相等大小的区域(Card),若是引用关系发生改变,JVM会将发生改变的区域标记位“脏区”(Dirty Card),而后在本阶段,这些脏区会被找出来,刷新引用关系,清除“脏区”标记
此阶段也不中止应用线程. 本阶段尝试在 STW 的 最终标记阶段(Final Remark)以前尽量地多作一些工做,以减小应用暂停时间
在该阶段不断循环处理:标记老年代的可达对象、扫描处理Dirty Card区域中的对象,循环的终止条件有:
1 达到循环次数
2 达到循环执行时间阈值
3 新生代内存使用率达到阈值
这是GC事件中第二次(也是最后一次)STW阶段,目标是完成老年代中全部存活对象的标记。在此阶段执行:
1 遍历新生代对象,从新标记
2 根据GC Roots,从新标记
3 遍历老年代的Dirty Card,从新标记
此阶段与应用程序并发执行,不须要STW停顿,根据标记结果清除垃圾对象
此阶段与应用程序并发执行,重置CMS算法相关的内部数据, 为下一次GC循环作准备
CMS的GC停顿时间约80%都在最终标记阶段(Final Remark),若该阶段停顿时间过长,常见缘由是新生代对老年代的无效引用,在上一阶段的并发可取消预清理阶段中,执行阈值时间内未完成循环,来不及触发Young GC,清理这些无效引用
经过添加参数:-XX:+CMSScavengeBeforeRemark。在执行最终操做以前先触发Young GC,从而减小新生代对老年代的无效引用,下降最终标记阶段的停顿,但若是在上个阶段(并发可取消的预清理)已触发Young GC,也会重复触发Young GC
并发模式失败:当CMS在执行回收时,新生代发生垃圾回收,同时老年代又没有足够的空间容纳晋升的对象时,CMS 垃圾回收就会退化成单线程的Full GC。全部的应用线程都会被暂停,老年代中全部的无效对象都被回收
晋升失败:当新生代发生垃圾回收,老年代有足够的空间能够容纳晋升的对象,可是因为空闲空间的碎片化,致使晋升失败,此时会触发单线程且带压缩动做的Full GC
并发模式失败和晋升失败都会致使长时间的停顿,常看法决思路以下:
一般CMS的GC过程基于标记清除算法,不带压缩动做,致使愈来愈多的内存碎片须要压缩,常见如下场景会触发内存碎片压缩:
可经过参数CMSFullGCsBeforeCompaction的值,设置多少次Full GC触发一次压缩,默认值为0,表明每次进入Full GC都会触发压缩,带压缩动做的算法为上面提到的单线程Serial Old算法,暂停时间(STW)时间很是长,须要尽量减小压缩时间
G1(Garbage-First)是一款面向服务器的垃圾收集器,支持新生代和老年代空间的垃圾收集,主要针对配备多核处理器及大容量内存的机器,G1最主要的设计目标是: 实现可预期及可配置的STW停顿时间
为实现大内存空间的低停顿时间的回收,将划分为多个大小相等的Region。每一个小堆区均可能是 Eden区,Survivor区或者Old区,可是在同一时刻只能属于某个代
在逻辑上, 全部的Eden区和Survivor区合起来就是新生代,全部的Old区合起来就是老年代,且新生代和老年代各自的内存Region区域由G1自动控制,不断变更
当对象大小超过Region的一半,则认为是巨型对象(Humongous Object),直接被分配到老年代的巨型对象区(Humongous regions),这些巨型区域是一个连续的区域集,每个Region中最多有一个巨型对象,巨型对象能够占多个Region
G1把堆内存划分红一个个Region的意义在于:
针对新生代和老年代,G1提供2种GC模式,Young GC和Mixed GC,两种会致使Stop The World
当新生代的空间不足时,G1触发Young GC回收新生代空间
Young GC主要是对Eden区进行GC,它在Eden空间耗尽时触发,基于分代回收思想和复制算法,每次Young GC都会选定全部新生代的Region,同时计算下次Young GC所需的Eden区和Survivor区的空间,动态调整新生代所占Region个数来控制Young GC开销
当老年代空间达到阈值会触发Mixed GC,选定全部新生代里的Region,根据全局并发标记阶段(下面介绍到)统计得出收集收益高的若干老年代 Region。在用户指定的开销目标范围内,尽量选择收益高的老年代Region进行GC,经过选择哪些老年代Region和选择多少Region来控制Mixed GC开销
全局并发标记主要是为Mixed GC计算找出回收收益较高的Region区域,具体分为5个阶段
暂停全部应用线程(STW),并发地进行标记从 GC Root 开始直接可达的对象(原生栈对象、全局对象、JNI 对象),当达到触发条件时,G1 并不会当即发起并发标记周期,而是等待下一次新生代收集,利用新生代收集的 STW 时间段,完成初始标记,这种方式称为借道(Piggybacking)
在初始标记暂停结束后,新生代收集也完成的对象复制到 Survivor 的工做,应用线程开始活跃起来;
此时为了保证标记算法的正确性,全部新复制到 Survivor 分区的对象,须要找出哪些对象存在对老年代对象的引用,把这些对象标记成根(Root);
这个过程称为根分区扫描(Root Region Scanning),同时扫描的 Suvivor 分区也被称为根分区(Root Region);
根分区扫描必须在下一次新生代垃圾收集启动前完成(接下来并发标记的过程当中,可能会被若干次新生代垃圾收集打断),由于每次 GC 会产生新的存活对象集合
标记线程与应用程序线程并行执行,标记各个堆中Region的存活对象信息,这个步骤可能被新的 Young GC 打断
全部的标记任务必须在堆满前就完成扫描,若是并发标记耗时很长,那么有可能在并发标记过程当中,又经历了几回新生代收集
和CMS相似暂停全部应用线程(STW),以完成标记过程短暂地中止应用线程, 标记在并发标记阶段发生变化的对象,和全部未被标记的存活对象,同时完成存活数据计算
为即将到来的转移阶段作准备, 此阶段也为下一次标记执行全部必需的整理计算工做:
G1的正常处理流程中没有Full GC,只有在垃圾回收处理不过来(或者主动触发)时才会出现, G1的Full GC就是单线程执行的Serial old gc,会致使很是长的STW,是调优的重点,须要尽可能避免Full GC,常见缘由以下:
相似CMS,常见的解决是:
巨型对象区中的每一个Region中包含一个巨型对象,剩余空间再也不利用,致使空间碎片化,当G1没有合适空间分配巨型对象时,G1会启动串行Full GC来释放空间。能够经过增长 -XX:G1HeapRegionSize来增大Region大小,这样一来,至关一部分的巨型对象就再也不是巨型对象了,而是采用普通的分配方式
缘由是为了尽可能知足目标停顿时间,逻辑上的Young区会进行动态调整。若是设置了大小,则会覆盖掉而且会禁用掉对停顿时间的控制
使用应用的平均响应时间做为参考来设置MaxGCPauseMillis,JVM会尽可能去知足该条件,多是90%的请求或者更多的响应时间在这以内, 可是并不表明是全部的请求都能知足,平均响应时间设置太小会致使频繁GC
如何分析系统JVM GC运行情况及合理优化?
GC优化的核心思路在于:尽量让对象在新生代中分配和回收,尽可能避免过多对象进入老年代,致使对老年代频繁进行垃圾回收,同时给系统足够的内存减小新生代垃圾回收次数,进行系统分析和优化也是围绕着这个思路展开
经常使用工具以下:
jvm自带命令行工具,可用于统计内存分配速率、GC次数,GC耗时,经常使用命令格式
jstat -gc <pid> <统计间隔时间> <统计次数>
输出返回值表明含义以下:
例如: jstat -gc 32683 1000 10 ,统计pid=32683的进程,每秒统计1次,统计10次
jvm自带命令行工具,可用于了解系统运行时的对象分布,经常使用命令格式以下
// 命令行输出类名、类数量数量,类占用内存大小, // 按照类占用内存大小降序排列 jmap -histo <pid> // 生成堆内存转储快照,在当前目录下导出dump.hrpof的二进制文件, // 能够用eclipse的MAT图形化工具分析 jmap -dump:live,format=b,file=dump.hprof <pid>
命令格式
jinfo <pid>
用来查看正在运行的 Java 应用程序的扩展参数,包括Java System属性和JVM命令行参数
其余GC工具
平台主要对用户在APP中行为进行定时分析统计,并支持报表导出,使用CMS GC算法。数据分析师在使用中发现系统页面打开常常卡顿,经过jstat命令发现系统每次Young GC后大约有10%的存活对象进入老年代。
原来是由于Survivor区空间设置太小,每次Young GC后存活对象在Survivor区域放不下,提早进入老年代,经过调大Survivor区,使得Survivor区能够容纳Young GC后存活对象,对象在Survivor区经历屡次Young GC达到年龄阈值才进入老年代,调整以后每次Young GC后进入老年代的存活对象稳定运行时仅几百Kb,Full GC频率大大下降
网关主要消费Kafka数据,进行数据处理计算而后转发到另外的Kafka队列,系统运行几个小时候出现OOM,重启系统几个小时以后又OOM,经过jmap导出堆内存,在eclipse MAT工具分析才找出缘由:代码中将某个业务Kafka的topic数据进行日志异步打印,该业务数据量较大,大量对象堆积在内存中等待被打印,致使OOM
系统对外提供各类帐号鉴权服务,使用时发现系统常常服务不可用,经过Zabbix的监控平台监控发现系统频繁发生长时间Full GC,且触发时老年代的堆内存一般并无占满,发现原来是业务代码中调用了System.gc()
GC问题能够说没有捷径,排查线上的性能问题自己就并不简单,除了将本文介绍到的原理和工具融会贯通,还须要咱们不断去积累经验,真正作到性能最优
篇幅所限,再也不展开介绍常见GC参数的使用,我发布在github:https://github.com/caison/cai...
《Java Performance: The Definitive Guide》 Scott Oaks
《深刻理解 Java 虚拟机:JVM 高级特性与最佳实践(第二版》 周志华
Getting Started with the G1 Garbage Collector
Java Hotspot G1 GC的一些关键技术——美团技术团队