java中的垃圾收集算法
分带收集理论:当下虚拟机的垃圾回收都采用的是分带收集算法,将对象分别存放在不同的内存块中,Java中主要分为年轻代和老年代。年轻代(eden区、s1区、s2区)在每次的minor gc后非存活的对象会被通过标记复制算法回收掉,老年代中没有多余的空间可以用来复制存储对象,老年代的垃圾对象回收一般选择“标记-清除”、“标记-整理”进行垃圾对象的回收。
- 标记复制算法
- 标记复制算法的效率是其中最好的。将内存分配俩个不同的内存块例:s1和s2,将s1标记出来的存活对象复制到s2内存内,再将s1内的对象全部回收,这样保证每次都是对内存空间的一半进行回收。
- 内存整理前
- 内存整理后
- 标记清除算法
- 有算法的名称我们就可以知道,算法的执行过程分为标记和清除俩个过程。在回收对象的过程的中可以先标记没有被在引用的垃圾对象,也可以选择标记可以存活的对象,在标记流程结束后,进行对标记的对象统一的回收或者保留。 使用标记清除算法会存在以下问题:
- 空间碎片化严重问题,标记清除后会产生大量不连续的空间碎片。
- 在标记的过程中如果对象过多,会标记时间过长。
- 内存整理前
- 内存整理后
- 有算法的名称我们就可以知道,算法的执行过程分为标记和清除俩个过程。在回收对象的过程的中可以先标记没有被在引用的垃圾对象,也可以选择标记可以存活的对象,在标记流程结束后,进行对标记的对象统一的回收或者保留。 使用标记清除算法会存在以下问题:
- 标记整理算法
- 此算法的与标记清除算法几乎一致,唯一的区别在于标记整理算法会在对象回收之后,对内存空间进行一个整理,消除了标记清除算法带来的内存空间碎片化的问题。当然标记整理算法也同样存在时间效率的问题。
内存整理前
内存整理后
- 此算法的与标记清除算法几乎一致,唯一的区别在于标记整理算法会在对象回收之后,对内存空间进行一个整理,消除了标记清除算法带来的内存空间碎片化的问题。当然标记整理算法也同样存在时间效率的问题。
内存整理前
垃圾收集器
垃圾收集器是对上述对象垃圾回收算法的具体实现。当前在使用的垃圾收集器有G1、Epsilon、ZGC、CMS、ParNew、Serial、Serial old等。不同的垃圾收集器作用的分代也不同,同过相互组合使用,在具体的情况下用不同的搭配组合处理特定的情况,便能达到十分好的效果。这片文章主要谈到的是以下六种垃圾收集器的搭配使用。
文章对不同的收集器进行比较,是为了辨别不同垃圾收集器的搭配使用的特点,不是为了寻找最优或者万能无忧的收集器!
-
Serial收集器(-XX:+UseSerialGc(年轻代) -XX:+UserSerialOldGc(老年代))
-
Serial(串行)收集器是一个单线程模型的收集器,在其工作的过程中只会使用一条线程去回收垃圾。所以当串行收集器在回收垃圾对象的过程中会出发STW(stop the word), 此时会暂停用户程序请求。新生代采用的是复制算法、老年代使用的是标记整理算法。
-
Serial收集器触发的STW会给用户带来程序卡顿的不好的体验的缺点,但是同时收集器也有存在优于其他收集器的优点:
-
收集效率高:因为是单线程操作,没有多线程的线程切换的cpu开销,可以获得较高的收集效率。
-
可以与Parallel Scavenge收集器、CMS收集器搭配使用。并且可以作为CMS收集器的一种备用方案来使用。
-
-
-
Parallel Scavenge收集器(-XX:+UserParallelGc(年轻代),-XX:+UserParallelOldGc(老年代))
- Parallel 简单来说明下就是:多线程版本的Serial收集器,在触发回收线程的时候是由多个线程去回收对象(同样会暂停应用程序请求,也即是触发STW),更好的提升吞吐量和cpu的高效使用。Parallel Scavenge收集器的年轻代和老年代回收算法和Serial收集器是相同的 新生代采用的是复制算法、老年代使用的是标记整理算法。
- 因为是Serial收集器的多线程版本 所以优缺点也是十分的相似,缺点也是同样会给用户带来不好的体验,但是因为是多线程处理情况也有所好转。Parallel Old收集器是Parallel Scavenge 收集器的老年代版本。
-
在注重吞吐量(吞吐量定义:cpu运行用户代码的时间和cpu总消耗时间的比值)和cpu资源的场合下,可以优先考虑Parallel Sccvenge + Parallel Old 这样的收集器组合来进行垃圾回收(JDK1.8以后默认是此种垃圾收集器搭配)。
-
- Parallel 简单来说明下就是:多线程版本的Serial收集器,在触发回收线程的时候是由多个线程去回收对象(同样会暂停应用程序请求,也即是触发STW),更好的提升吞吐量和cpu的高效使用。Parallel Scavenge收集器的年轻代和老年代回收算法和Serial收集器是相同的 新生代采用的是复制算法、老年代使用的是标记整理算法。
-
ParNew收集器(-XX:UserParNewGc(新生代))
-
和Parallel 收集器很像,唯一的不同是ParNew收集器可以和CMS收集器搭配使用。新生代采用的是复制算法、老年代使用的是标记整理算法
-
ParNew收集器是除了Serial 收集器外,唯一可以和CMS收集器搭配使用的收集器。
-
-
CMS收集器(-XX:UserConcMarkSweepGc(老年代))
- cms=concurrent mark sweep 并发标记清除。将原来的收集器流程更加细分化,分为了以下五个主要阶段来进行标记清除:
-
初始标记:此阶段会触发STW,但是消耗的时间及其短暂,只会根据GC ROOTS直接进行标记还在被使用的对象,对于用户是无感知的。
-
并发标记:在此阶段中用户程序线程和回收标记线程是同步进行的,主要对初始标记中的关联对象进行遍历标记。因为并发标记过程中,用户也同时进行操作会存在刚刚被标记存活的对象,在下一时刻就随着用户线程结束,而变为可回收对象。也就是并发标记的过程中,会因为用户操作而导致已经标记过的对象状态发生改变。此过程不会stw。
-
重新标记:修正在并发标记过程中,因为用户操作而导致的标记状态改变的对象。主要用到三色标记方法。 会stw 时间消耗极短。
在重新标记过程中,也会存在用户程序新产生的对象,可能会存在对象还在标记回收的过程中,用户程序产生过大过多的对象而导致OOM(Out Of Memory)。所以可以通过设置jvm参数 -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比) 在gc前通过预留内存来避免发生OOM(Out Of Memory)
-XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引 用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段。 通过减少年轻代转向老年代的对象大小,来减少标记的时间消耗和内存消耗
-
并发清理:GC线程会对未标记区域进行清理,在此过程中用户线程创建的对象会变标记为黑色,不做处理,在此阶段会产生浮动垃圾,在下次gc中进行处理。
-
并发重置:重置本次gc过程中的标记数据。
-
- cms=concurrent mark sweep 并发标记清除。将原来的收集器流程更加细分化,分为了以下五个主要阶段来进行标记清除:
垃圾回收底层算法--CMS底层三色标记
三色标记主要是为了解决并发标记过程中的多标和漏标 情况。
-
黑色: 表示这个对象的成员变量(引用对象) 都被扫描过,代表这安全存活的对象,当再次被其他对象指向时,不会在进行扫描过程。!!!黑色对象不可能直接越过灰色对象直接指向白色对象,因为黑色对象的所有引用都被扫描过了,不会存在未被扫描的对象。
-
灰色: 表示这个对象被扫描过,但是此对象内至少还有一个成员变量(引用对象)没有被扫描。和黑色标记对象相比也就是存在还未被标记的引用对象
-
白色: 表示对象没有对收集器访问过,在初始时所有的对象都是白色,在分析结束后若对象还是白色标记状态 那么代表对象不可达,即将被回收。
-
多标的情况--浮动垃圾
- 在并发标记过程中,用户程序结束后,会让并发标记过程中本该被回收的对象可能会被标记为黑色而不被回。但是随着用户程序结束,这部分黑色对象理应是要被回收的,这部分对象称之为浮动垃圾。在重新标记过程中用户线程新产生的对象会都被标记为黑色,在下一次gc过程中,进行回收。多标的情况,只是会单纯的产生浮动垃圾,不会有太大的程序影响,漏标对象 会导致程序运行中的所需对象被回收掉 导致bug。漏标对象才是需要处理的关键所在。
-
漏标的情况--误删存活对象
- 在图中的黄色引用线,就是在并发标记过程中漏标的一条引用线。漏标的对象会在这次gc中被销毁,造成严重bug。所以在重新标记阶段要保证D对象不会被回收掉。
解决漏标的俩种方式
-
增量更新(Incremental Update)
- 在并发标记的过程中,新增的对象引用保存起来,会存放在一个集合内。在重新标记的过程总,从保存的集合内重新拿出新增引用的对象来进行重新扫描(此时即便是黑色对象也会重新进行扫描,因为从集合中拿出来的对象如果是黑色对象 那么会将其变为灰色对象 进行重新的扫描),此时之前可能是白色标记的对象,在重新被引用后,会被标记为灰色或者黑色对象,在这次gc过程中不会被回收。
-
原始快照(Snapshot At The Beginning,SATB)
- 说的直接点就是将未被标记的白色对象变为浮动垃圾。 在并发标记过程中将白色对象保存到一个集合内,在重新标记阶段直接将集合内的对象直接标记为黑色对象,即使此时真的没有被任何对象引用也是标记为黑色。通过将对象强行变为黑色对象的方式来保证不会漏标对象。