GC分类与性能指标
吞吐量:运行用户代码时间栈总运行时间的比例。(程序运行时间 / 程序运行时间 + 内存回收时间)
暂停时间:执行垃圾收集时,程序的工作线程所暂停的时间。
内存占用:java堆区所占用的内存大小
现在标准:在延迟可控的情况下,尽量提高吞吐量
常见垃圾回收器
串行回收器:Serial、Serial Old
并行回收器:ParNew、Parallel Scavenge、 Parallel Old
并发收集器:CMS、G1
jdk9取消红色虚线组合。
jdk14弃用绿色虚线组合,删除CMS
为什么有很多垃圾收集器?
因为java的使用场景很多,针对不同的场景提供不同的垃圾收集器。
Serial:串行回收
Serial作为hotspot中client模式下的默认新生代收集器。
Serial收集器采用:复制算法、串行回收和“Stop the world”机制执行内存回收。
Serial Old 收集器同样采用串行回收,“stop the world”,只不过内存回收算法使用的是标记压缩算法。
、串行回收:只会使用一个cpu或者一条收集线程去完成垃圾收集工作,且必须暂停其它所有的工作线程。
限定于单个cpu是简单高效的。现在基本不使用。
ParNew:并行回收
只能处理新生代
,采用并行回收方式,复制算法,stop the world。
对于新生代:回收次数频繁,使用并行方式高效
对于老年代:回收次数少,使用串行方式节省资源。(cpu并行需要切换线程,串行可以省去切换线程的资源)
Parallel:吞吐量优先
自适应策略是Parallel与ParNew的重要区别。
高吞吐量可以高效地利用cpu时间,尽快的完成任务,主要适合在后台运算而不需要太多交互的任务。因此,常见服务器环境中使用。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序。
新生代:Paralell,复制算法,stop the world,并行回收
老年代:Parallel采用标记压缩算法,并行回收、stop the world
在程序吞吐量优先的应用场景中,Parallel收集器和Parallel Old收集器的组合,在server模式下的内存回收性能很不错。
jdk8中默认采用Parallel、Parallel Old。
参数配置:
-XX:+printParallelGC:手动年轻代使用Parallel执行并行收集器
-XX:+UserParallelOldGC:手动老年代使用Parallel Old使用并行回收器
-XX:ParallelGCThreads:设置年轻代并行收集器的线程数。一般的最好与cpu数量(核心数)相等。
-XX:MaxGCPauseMillis:设置垃圾收集器最大停顿时间(STW),单位ms。
-XX:GCTimeRatio:垃圾收集时间占总时间的比例。用于平衡吞吐量大小。
-XX:+UserAdaptiveSizePolicy:设置Parallel Scavenge的自适应调节策略。
在这种模式下年轻代的大小、Eden、Survivor的比例
晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡。
CMS:低延迟(Concurrent-Mark-Sweep)
这款收集器是Hotspot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作
采用标记清楚算法,存在stop the word。
CMS适合与用户交互的程序,良好的响应速度能提升用户体验。着重点在缩短垃圾收集时用户线程停顿的时间。
目前很大一部分java应用集中在互联网站或 B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短。
CMS四阶段:
- 初始标记:程序中所有线程(stop the world)出现短暂暂停,这个阶段仅仅只是标记处GC Roots能直接关联的对象,所以速度非常快。
- 并发标记:从GC Roots直接关联的对象开始遍历整个对象图的过程,耗时较长但是不需要停止用户线程。
- 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象标记记录,这个阶段停顿时间比初始标记时间稍长一点。
- 并发清楚:清理删除标记阶段判断已经死亡的对象,释放内存空间。
由于最耗时的并发标记与并发清除都不需要暂停工作,所以整体的回收是低停顿的。
由于垃圾收集线程与用户线程没有中断,所以在CMS回收过程中,还应确保应用程序有足够的内存空间。因此CMS都堆内存达到一定阈值时就开始清理,不会等到被填满才开始
。要是内存不够就会出现“Concurrent Mode Failure”,然后启动Serial Old重新收集老年代。
标记清楚会产生内存碎片,在为新对象分配空间是无法采用指针碰撞。但也不会CMS不采用标记压缩,原因是,当清理垃圾时是并发操作,如果改变对象地址,那么正在运行的用户线程就拿不到对象。
CMS缺点:
- 会产生内存碎片。在无法分配大对象时会提前触发Full GC
- CMS收集器对cpu资源非常敏感。并发阶段不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量降低。
- CMS收集器无法收集浮动垃圾:浮动垃圾产生于并发标记阶段用户线程断掉联系的对象,重新标记不会对这部分对象进行标记处理,只能等到下一次GC时清理这些浮动垃圾。
参数设置
-XX:+UserConcMarkSweepGC:手动采用CMS收集器执行回收任务。
-XX:+CMSlnitiatingOccupanyFraction:设置对内存使用率的阈值,一旦达到阈值,便开始回收。jdk5默认68%,jdk6默认92%
-XX:+UserCMSCompactAtFullCollection:用于指定在执行完Full GC后对内存空间进行压缩整理
-XX:CMSFullGCCsBeforeCompaction:设置执行多少次Full GC对内存空间进行压缩。
-XX:ParallelCMSThreads:设置CMS的线程数量,默认:(并行线程 + 3)/ 4
小结
最小使用内存和并行开销:Serial GC
最大应用程序的吞吐量:Parallel GC
最小化GC中断或停顿时间:CMS GC
G1:区域分代式(Garbage first)
主要针对多核cpu及大容量内存的机器。在jdk9中默认使用。
把堆内存划分为很多不相关的区域,使用不同的Region来表示Eden,s0,s1,老年代。
G1 GC避免了整个java堆进行全域的垃圾收集。G1根据各个Region里面的垃圾堆积价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收最大的Region。
Region只能是Eden、Survivor、Humongous中的一种,但是它的身份不是固定的,谁来占用那么这个Region就是谁的
并行与并发
并行性:G1再回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW。
并发性:G1拥有与应用程序交替执行的能力,部分工作,部分工作和应用程序同时执行。
分代收集
将堆空间分为若干个区域(region),这些区域包含逻辑上的年轻代与老年代。
和之前的回收器不同,它同时兼顾年轻代和老年代。
空间整合
CMS:标记清楚算法,内存碎片。若干次后进行一次碎片整理。
G1将内存划分为一个个的region。内存的回收是region作为单位的。
Region之间是复制算法,但整体上实际是可以看作标记压缩算法。
可预测的停顿时间模型
由于分区的原因,G1可以选取部分地区进行內村回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好控制。
G1 GC避免了整个java堆进行全域的垃圾收集。G1根据各个Region里面的垃圾堆积价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收最大的Region。
小内存CMS优势很明显,6~8GB时CMS与G1差不多,大于8GB时G1优势明显。
参数
-XX:+UserG1GC:jdk9时才默认开启,此参数在jdk8时可开启G1
-XX:G1HeapRegionSize:设置每个Region的大小。值是2的幂
-XX:MaxGCPauseMillis:设置期望到达最大GC停顿时间。默认是200ms
-XX:ParallelGCThreads:设置STW工作时GC线程数的值。最多设置为8.
-XX:ConGCThreads:设置并发标记的线程数。ParallelGCThreads的 1/4左右。
-XX:InitiatingHeapOccupancyPercent:设置触发并发GC周期的java堆占用阈值率。超过此值,就触发GC,默认45%。
如果设置了Region数量,那么Region大小就不是固定的,但是大小肯定是2的幂次方,并且在1~32M之间;如果设置了Region大小,那么Region数量就不是固定的,但是肯定是2048附近;
G1常见操作步骤:
- 开启G1垃圾收集器
- 设置堆的最大内存
- 设置最大停顿时间
适用场景:
面向服务端应用,针对具有大内存、多处理器的机器。
最主要的应用是需要低GC延迟,并且有大堆的应用程序提供解决方案。(G1每次只清理一部分Region)
用来替换JDK1.5中CMS收集器,
- 超过50%的java堆被活动数据占用
- 对象分配频率或年代提升频率变化很大
- GC停顿停顿时间过长(长于0.5至1秒)
设置H的原因(Humongous):
对于堆中的大对象,默认直接会被分配到老年代,但是如果短期存在的大对象,就会对垃圾收集器造成负面影响。Humongous就是用来专门储存大对象,如果一个H区存放不下就会找连续的H区来储存。
Remember Set(记忆集)
一个对象被不同区域引用的问题。
一个Region不可能是孤立的,一个Region中的对象可能被其它任意Region中对象引用,在判断对象是否存活时,是否需要扫描整个java堆才能保证准确?
解决方法:
- 无论是G1还是其它分代回收器,JVM都是使用 Remembered Set来避免全局扫描。
- 每个Region都有一个对应的Remember Set
- 每次Reference类型数据写操作时,都会产生一个 Write Barrier暂时中断操作。
- 然后检查将要写入的引用指向对象是否和该Reference类型数据在不同的Region。
- 如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remember Set中。
- 当进行垃圾收集时,在GC根节点的枚举范围加入Remember Set,就可以保证不进行全局扫描,也不会有遗漏。
上页提到的Remebered Set就是上述Reset,上页提到的Reference类型就是引用类型,其中Reset的作用是记录当前Region中哪些对象被外部引用指向,比如Old区中的对象会指向Eden区的对象,然后当我们要回收某个Region的时候,直接遍历遍历当前Region中的所有对象就可以了,然后针对性的去找到那些指向当前对象的其他对象,最终发现当前对象是否是根可达的,如果不是,那就应该被删除,其实之前的垃圾回收器都涉及到这个问题,当进行Minor GC的时候,通过GC Roots查找的时候还需要遍历Old区的对象,毕竟Old区对象也可能会指向Eden区对象,但是G1通过Rset避免了全堆的扫描,当引用类型数据写操作时,先暂时中断,然后判断当前引用类型数据是否被其他对象所指向,如果不被指向,那就直接放在Region中就可以了;如果被其他对象指向,那么还要判断这个对象是在当前要插入的Region中,还是在其他Region中;如果在其他Region中,那就需要使用CardTable把当前引用类型数据的指向信息放在Rset中,也就是形成上面的虚线连线,如果在当前Region中,那就不需要指向了,毕竟到时候我们会进行遍历查找根可达对象,那肯定会找到的,所以这种情况也是直接放在Region中就可以了;
G1回收器垃圾回收过程
- 一、年轻代GC(Young GC)
- 二、老年代并发标记过程(Concurrent Marking)
- 三、混合回收(Mixed GC)
- 强力回收(如果需要单线程、独占式、高强度的Full GC还是继续存在的。他针对GC的评估失败提供了一种失败保护机制,即清理回收)
当年轻代的Eden区用尽时开始年轻代的回收过程:G1的年轻代收集阶段是一个并行
的独占式
收集器。年轻代回收期间,G1 gc暂停所有应用程序线程,启动多线程执行年轻代的回收。然后从年轻代区间移动存活对象到Survivor区间或者老年代区间。
当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程。
标记完成马上开始混合回收过程。对于一个混合回收器,G1 gc从老年代移动存活对象到其他区域。
这些空闲区域也成为老年代一部分。和年轻代不同,老年代G1回收器不需要整个老年代被回收,一次只需要扫描或者回收一小部分老年代的Region即可。
G1优化建议:
一、年轻代大小
避免使用-Xmn或者-XX:NewRatio等相关选项设置年轻代大小
固定年轻代大小会覆盖暂停时间目标
二、暂停时间目标不要太过苛刻
G1 gc吞吐量的目标是90%时间应用程序线程,10%时间gc立即回收
评估G1 gc的吞吐量时,暂停时间目标不要太苛刻。目标太过苛刻表示你愿意承担更多的垃圾回收开销,会直接影响吞吐量。
怎么选择垃圾收集器
- 优先调整堆的大小让JVM自适应完成
- 如果内存小于100M,使用串行收集器(Serial,Serial Old)
- 如果单核、单机程序、并且没有停顿时间的要求,串行收集器
- 如果多核cpu、需要高吞吐量、允许停顿时间超过一秒,选择并行或JVM自适应(Parallel)
- 如果多cpu、追求停顿时间,需要快速响应,使用并发收集器(CMS,G1),官方推荐G1,性能高。现在的互联网项目基本都是用G1。
参数与日志查看
ZGC
未来将在服务端、大内存、低延迟应用的首选垃圾收集器。
-XX:+UnlockExperimentalVMOptions -XX:+UseZGCg :在mac或windows使用ZGC。