首先,要分类,从JVM的理念来分,有两大类,串行垃圾回收器和并行垃圾回收器
串行垃圾回收器
在JDK3之前,单线程垃圾回收器是唯一的选择。在进行垃圾回收时会暂停用户的线程,直到他收集完毕。
串行的垃圾回收器有两种,Serial和Serial Old,一般搭配使用(分代回收(老年代标记-压缩)),
并行垃圾回收器
随着JDK版本的更新迭代,串行垃圾回收器的停顿时间较长,所以衍生出了并行垃圾回收器,多个线程同时工作,但也会STW(stop-the-world),暂停用户线程。在JDK5之后,搭配CMS一起使用。
并行垃圾回收器是ParNew、 Parallel Scavenge、Parllel Old。
他们的区别是:
ParNew
和Parallel
是新生代收集。
Parallel Scavenge
不同的是,该垃圾回收器关注吞吐量,吞吐量优先。(代码运行时间/(代码运行时间+垃圾收集时间))所以它的目标与其他垃圾回收器不一样,它的目标通过参数设置,达到一个可控的吞吐量,而不是向CMS等收集器一样尽可能的缩短垃圾收集的时候用户线程的停止时间。所以在JDK1.6之后,搭配Parallel Old的是Parallel New 而不是Parallel Scavenge。
Parallel Old
是老年代收集。
JDK6之后,Paraller NEW和Parallel New搭配使用。
第一款并发垃圾回收器:CMS
CMS垃圾回收器作为第一款并发垃圾回收器,它设立的初衷是为了提升用户程序响应的时间,也就是降低STW时间。所以CMS采用了并发标记和并发清除,让垃圾收集线程和用户线程并发运行,从而实现提升程序响应时间,减少STW。但是CMS 是基于标记-清除算法来实现的,会产生内存碎片。CMS 在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
它标记、清理垃圾主要有四个阶段
- 初始标记 独占
这个阶段会标记所有与GCRoots相关联的对象,速度很快,但是需要暂停用户线程,独占进行。 - 并发标记 并发 三色标记算法标记垃圾算法
这个阶段垃圾回收线程与用户线程并发执行(用户线程不用暂停),使用可达性分析算法标记处所有对象。 - 重新标记 独占
也叫修正标记,因用户线程继续运作导致标记产生变动的那一部分对象的标记记录,需要暂停用户线程,但是时间相对并发标记阶段还是很短的;STW,多线程独占,标记出新的垃圾对象 - 并发清除 并发
清理掉标记阶段已经死亡的对象,由于使用清除算法,不需要移动对象,因此不需要暂停用户线程,并发清除;使用标记-清除算法)
CMS垃圾回收器主要有几个缺点:
- CMS的默认回收线程数是(处理器核心数+3)/4,CPU的核心数越少,CMS回收对用户线程的影响就越大。
- CMS是基于标记清除算法的垃圾回收器,那么就会产生内存碎片,这样分配大对象就会遇到麻烦,不得不提前触发一次Full GC
- 最后一个是大问题,也就是CMS锁退化问题。首先因为CMS 无法清理浮动垃圾 (在与用户线程并发回收垃圾时有产生了新的垃圾,称为浮动垃圾)。因为CMS不会等到老年代满才回收,而是有个阈值,
在JDK6时设置为92%,当然也可以通过参数来修改。
所以这8%的空间来存放浮动垃圾。但是如果这时候新生代又产生了垃圾对象,吧这8%存放浮动垃圾的空间占满,这时候CMS就是退化为SerialOld回收器 单线程对老年代进行回收。所以这样也会降低垃圾收集的效率,STW时间延长。
另一个问题是CMS对内存碎片的解决。因为它采用的是标记-清除算法。
CMS提供了一个参数(-XX:UseCMS-CompactAtFullCollection),该参数控制在full Gc的时候压缩一下空间,整理内存碎片(需要stw)。
难道每次发送 Full GC 的时候都STW一下压缩一下内存?这样岂不是也会降低效率?
其实CMS还提供了一个参数(-XX:CMSFullGCsBeforeCompaction=n),可以指定几次full gc后开始对内存碎片进行压缩,避免频繁整理内存碎片而造成stw过长。默认是0,也就是在默认配置下每次CMS GC顶不住了而要转入full GC的时候都会做压缩,可自己指定。
被寄予厚望的Garbage First垃圾回收器
G1回收器是一款主要面向服务端应用的垃圾回收器,JDK9之后,它替代了Parallel回收器。G1回收器可以面向堆内存中的任意地方来回收内存(Mix GC),没有被局限在新生代、老年代、和整堆回收中。
G1回收器中的基于Region的堆内存布局是实现上述目标的关键。G1将Java堆分为多个大小相等的独立区域(Region)
,每个Region都可以根据需要扮演新生代的Eden空间、Survivior空间以及老年代空间。Region中还有一个特殊的Humongos
区域,用于存储大对象,当大对象的大小超过单个Region的时候,这个对象会被放在多个连续的Humongos Region
中,G1会把这个区域当作老年代来处理。
G1收集器的新生代和老年代并不是固定的,他们都是一系列区域(不需要连续)的动态集合,G1进行垃圾回收的时候Region是垃圾回收的最小单元,每次收集的内存空间是G1的整数倍,有限回收收益最大的区域,能够让G1收集器在有限的时间内获取尽可能高的手机效率。
G1收集的过程大致分为以下四个步骤:
- 初始标记 标记GC Roots能直接关联到的对象, 独占式
- 并发标记 从GC Roots开始对堆中的对象进行可达性分析。 并发式
- 最终标记 重新标记 独占式
- 最终标记 对各个Region的回收价值和成本进行排序,根据用户期望的停顿时间来制定停顿计划,把回收的那部分Region中存活的对象复制到空的Region中,再清理掉旧的Region中的全部空间,涉及对象的复制移动,需要暂停用户线程
G1在整体上是基于标记整理算法实现的,但是从俩个Region来看,是基于标记复制算法,这俩种方法确保G1在垃圾回收的时候不会产生内存碎片。