学习java一定要知道的垃圾收集器

2022-11-13 12:11:13 学习 垃圾 收集器

垃圾收集器如何演化的?

垃圾收集器的发展路线,简单来说是随着内存越来越大而发生变化。

从分代算法逐渐演化为不分代算法。

从serial的几十兆,逐渐演化到parallel的几个G,再到CMS的几十个G,也从此开始了并发回收。

年轻代收集器

Serial

特点:年轻代、串行回收、STW、简单高效

Serial(串行)收集器是最基本、发展历史最悠久的收集器,它是采用复制算法的新生代收集器,曾经(jdk 1.3.1之前)是虚拟机新生代收集的唯一选择。

在垃圾回收时,必须暂停其他所有线程的工作线程,即所谓的“Stop The World”。

Serial收集器是HotSpot虚拟机运行在Client模式下的默认新生代收集器。

Serial收集器具有简单而高效,由于没有线程交互的开销,可以获得最高的单线程收集效率(在单个CPU环境中)。

添加该参数来显式的使用Serial垃圾收集器:

-XX:+UseSerialGC

ParNew

特点:年轻代、多线程回收、STW、配合CMS、核心越多效率越高

ParNew收集器是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The Word、对象分配规则、回收策略等都与Serial收集器一样。

目前只有它能和CMS收集器(Concurrent Mark Sweep)配合工作。

在单CPU的环境下,其性能绝对不会有比Serial收集器有更好的效果,甚至由于多线程的切换开销,在双核情况下也有可能无法做到超越Serial。 在多核的情况下,GC线程数默认与核心数相同,随着核心的增多,能获得更好的效果。

指定使用CMS后,会默认使用ParNew作为新生代收集器:

-XX:+UseConcMarkSweepGC

强制指定使用ParNew

-XX:+UseParNewGC

指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同:

-XX:ParallelGCThreads

Parallel Scavenge

特点:年轻代、并行回收、吞吐量、GC自适应的调节策略(GC ErGonomics)

Parallel Scavenge收集器是一个新生代收集器,使用复制算法,且是并行的多线程收集器。

Parallel Scavenge收集器关注点是达到一个可控制的吞吐量:

(吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间))

而其他收集器关注点在尽可能的缩短垃圾收集时用户线程的停顿时间。

控制最大垃圾收集停顿时间:参数允许的值是一个大于0的毫秒数,收集器将尽可能的保证内存垃圾回收花费的时间不超过设定的值(但是,并不是越小越好,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的,如果设置的值太小,将会导致频繁GC,这样虽然GC停顿时间下来了,但是吞吐量也下来了)

-XX:MaxGCPauseMillis

控制吞吐量大小:参数的值是一个大于0且小于100的整数,也就是垃圾收集时间占总时间的比率,默认值是99,就是允许最大1%(即1/(1+99))的垃圾收集时间。

-XX:GCTimeRatio

GC自适应的调节策略(GC Ergonomics) :当这个参数打开之后,就不需要手工指定新生代的大小、Eden与Survivor区的比例、晋升老年代对象年龄等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)。只需要把基本的内存数据设置好(如-Xmx设置最大堆),然后使用MaxGVPauseMillis参数或GCTimeRation参数给虚拟机设立一个优化目标,JVM会自动调节其他优化参数

-XX:+UseAdaptiveSizePolicy

老年代收集器

SerialOld

特点:老年代、串行回收、STW

Serial Old收集器是Seria收集器的老年代版本,他同样是一个单线程收集器,使用" 标记-整理" 算法。

Serial Old收集器主要用于Client模式下的虚拟机使用。

Server模式下的两大用途:

  • 在JDK1.5及之前的版本与Parallel Scavenge收集器搭配使用;
  • 作为CMS收集器的后备方案,在并发收集发生Conturrent Mode Failure时使用。

结合Serial的回收流程如下图所示:

ParallelOld

特点:老年代、多线程、STW、吞吐量

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。此收集器的出现代表着“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。

使用po收集参数:

-XX:+UseParallelOldGC

与Parallele Scavenge结合使用的回收流程如下:

使用吞吐量收集器的吞吐量计算方式如下图所示:

CMS(ConcurrentMarkSweep)

特点:老年代、并发的,短停顿(降低STW时间)

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用,这些应用都非常重视服务的响应速度。从名字上(“Mark Sweep”)就可以看出它是基于“标记-清除”算法实现的。

CMS问题比较多,所以现在没有一个版本默认是CMS,只能手工指定。

CMS既然是MarkSweep,就一定会有碎片化的问题,碎片到达一定程度,CMS的老年代分配对象分配不下的时候,使用SerialOld 进行老年代回收。

CMS的回收过程主要分为4个步骤:

  • 初始标记 标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
  • 并发标记 进行GC Roots Tracing(根可达算法)的过程,在整个过程中耗时最长,标记所有根节点能照的对象。与用户线程一起工作。
  • 重新标记 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要“Stop The World”。
  • 并发清理 与用户线程一起运行。

从以上过程中可以总结出其优点:并发收集,并发清理,地停顿。

三个缺点:

  • CMS收集器对CPU资源非常敏感。 CMS默认启动的回收线程数是(CPU数量+3)/4,回收线程会占用部分线程资源,导致系统吞吐量降低。在选用CMS收集器的时候,需要考虑,当前的应用系统,是否对CPU资源敏感。
  • 垃圾收集过程中,无法处理浮动垃圾,可能会出现Concurrent Mode Failure问题,导致触发Full GC。
  • 浮动垃圾:是由于CMS收集器的并发清理阶段,清理线程是和用户线程一起运行,如果在清理过程中,用户线程产生了垃圾对象,由于过了标记阶段,所以这些垃圾对象就成为了浮动垃圾。
  • CMS无法在当前垃圾收集过程中集中处理这些浮动垃圾,需要预留内存空间去保存这些垃圾,可以通过参数 -XX:CMSInitiatingOccupancyFraction 控制触发占用老年代的百分比。
  • 如果预留空间无法装下浮动垃圾,会导致Concurrent Mode Failure失败,这个时候,虚拟机将启动后备预案,临时启动Serial Old收集器来对老年代重新进行垃圾收集,这样会导致垃圾收集的时间边长,特别是当老年代内存很大的时候。所以对参数-XX:CMSInitiatingOccupancyFraction的设置,过高,会导致发生Concurrent Mode Failure,过低,则浪费内存空间。
  • 使用的"标记-清除"算法会出现很多内存碎片。 过多的内存碎片导致缺少连续的内存空间,会影响大对象的分配,最终触发Full GC,为了解决这个问题,CMS收集器提供了一个"-XX:+UseCMSCompactAtFullCollection"参数(默认是开启的),用于CMS收集器在必要的时候对内存碎片进行压缩整理。由于内存碎片整理过程不是并发的,所以会导致停顿时间变长。虚拟机还提供了一个-XX:CMSFullGCsBeforeCompaction"参数,来控制进行过多少次不压缩的Full GC以后,进行一次带压缩的Full GC,默认值是0,表示每次在进行Full GC前都进行碎片整理。

主要参数: 使用CMS收集器:

-XX:+UseConcMarkSweepGC

Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长:

-XX:+UseCMSCompactAtFullCollection

设置进行几次Full GC后,进行一次碎片整理:

-XX:+CMSFullGCsBeforeCompaction

设定CMS的线程数量(一般情况约等于可用CPU数量):

-XX:ParallelCMSThreads

新型收集器

G1

算法:三色标记 + SATB

G1(Garbage-First)收集器是当今收集器技术发展最前沿的成果之一,它是一款面向服务端应用的垃圾收集器,HotSpot开发团队赋予它的使命是(在比较长期的)未来可以替换掉JDK 1.5中发布的CMS收集器。G1从JDK9开始已经作为默认的垃圾回收器。如果对于应用程序来说停顿时间比吞吐量更重要,G1是非常合适的选择。

G1与上面介绍的GC有很大的不同

  • G1的设计原则是首先收集尽可能多的垃圾(Garbage First) 。 G1并不会等内存耗尽(串行、并行)或者快耗尽(CMS)的时候开始垃圾收集,而是在内部采用了启发式算法,在老年代找出具有高收集收益的分区进行收集
  • G1采用内存分区(Region)的思路
  • 逻辑上的分代概念,不是物理分代
  • 所有收集都是STW,混合(mixed)收集(年轻代和老年代同时进行收集)

三色标记法 说起并发回收,就不得不了解三色标记法。先学习三色标记法,更好的理解G1的分区模型。

我们将对象分成三种类型:

  • 黑色:跟对象或者该对象与它的子对象都被扫描(标记完成)。
  • 灰色:对象本身被标记完成,但是还没有扫描完该对象中的子对象
  • 白色:未被扫描的对象。或者是扫描完成后,最终白色对象为不可达对象即为垃圾对象。

其实现过程如下图所示:

  • 第一步:根对象被置为黑色,子节点置为灰色。
  • 第二步:继续遍历灰色对象,将遍历过的子节点置为灰色,当前灰节点置为黑色。
  • 第三部:遍历所有可达对象后,所有可达对象置为黑色,不可达的为白色,将要被回收。

三色标记存在的问题 如下图所示,如果gc扫描到左侧位置,这时候程序执行C.d = D,B.d = null,则会导致B对D的引用消失,而C则引用了D,可是此时C是黑色的,不会对其子节点进行扫描,则D节点会被当做垃圾进行回收。

三色标记法问题解决方案

通常有两种解决方案:

  • 在插入的时候记录对象(CMS增量更新)
  • 在删除的时候记录对象(G1 STAB)

增量更新(Incremental update) : 在CMS采用的是增量更新(Incremental update),只要在写屏障(write barrier)里发现要有一个白对象的引用被赋值到一个黑对象 的字段里,那就把这个白对象变成灰色的。即插入的时候记录下来。

STAB(snapshot-at-the-beginning) : STAB 的做法在 GC 开始时,在初始标记时对内存进行一个对象图的逻辑快照 (snapshot),只要被快照到对象是活的,那在整个 GC 的过程中对象就被认定的是活的,即使该对象的引用稍后被修改或者删除。

同时新分配的对象也会被认为是活的,除此之外其它不可达的对象就被认为是死掉了。这样 STAB 就保证了真正存活的对象不会被 GC 误回收,但同时也造成了某些可以被回收的对象逃过了 GC,导致了内存里面存在浮动的垃圾 (float garbage),这些浮动垃圾将在下次回收时被收集。

G1的分区

G1是逻辑分代,将堆分为若干个区域(region),新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这样就解决了CMS中的内存碎片问题。

如上图分代模型所示,G1仍然有Eden,Survivor,Old等区域,同时多了一个Humongous Region(巨型对象)。

巨型对象:一个大小达到甚至超过分区大小一半的对象称为巨型对象(Humongous Object)。

巨型对象:”会独占一个、或多个连续分区,其中第一个分区被标记为开始巨型(StartsHumongous),相邻连续分区被标记为连续巨型(ContinuesHumongous)。巨型对象会直接在老年代分配,所占用的连续空间称为巨型分区(Humongous Region)。G1内部做了一个优化,一旦发现没有引用指向巨型对象,则可直接在年轻代收集周期中被回收。

G1中GC的分类:

  • Young GC:主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。 在这种情况下,Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到年老代空间。Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。最终Eden空间的数据为空,GC停止工作,应用线程继续执行
  • Mix GC:不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。

G1是否有Full GC? 答案是肯定的,G1触发了Full GC,这时G1会退化使用Serial Old收集器来完成垃圾的清理工作。

Full GC产生的原因? G1在对象复制/转移失败或者没法分配足够内存(比如巨型对象没有足够的连续分区分配)时,会触发Full GC。Full GC使用的是stop the world的单线程的Serial Old模式,所以一旦触发Full GC则会STW应用线程,并且执行效率很慢。JDK 8版本的G1是不提供Full GC的处理的。对于G1 GC的优化,很大的目标就是没有Full GC。

剩下三种GC本文会在后面主键补充:

  • ZGC (10ms - 1ms) PK C++:算法:ColoredPointers + LoadBarrier
  • Shenandoah:算法:ColoredPointers + WriteBarrier
  • Eplison:Epsilon不回收内存,只负责堆的管理与布局,对象的分配,与解释器、编译器、监控子系统的协作; epsilon适合运行时间短、在内存耗尽前就可退出的应用程序

到此这篇关于学习java一定要知道的垃圾收集器的文章就介绍到这了,更多相关java垃圾收集器内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!

相关文章