垃圾收集算法
GC算法网上已经有一大堆了,相信很多人已经看了至少2~3遍,在这里简单的过一遍。
1.GC算法有哪些?各有什么优缺点?
1.1 标记-清除 算法
算法分为两个阶段,先标记,后清除。先将不可回收对象进行标记,剩下的那些需要回收的统一清除。
算法简单,缺点是会产生内存碎片。
1.2 复制算法
将内存分为两块,每次使用其中的一块,当内存不足时,将存活的对象复制到另一块内存中去,剩下的直接清除。
回收后内存时连续的,缺点是会浪费一些内存,因为每次只使用其中的一块。
1.3 标记-整理 算法
类似标记清除算法,只是在回收的时候将存活的对象移动到一端,剩下的清除掉。
充分使用了内存,也避免了内存碎片。因为要移动对象,效率较低。
1.4 分代算法
严格上讲,它并不是一个算法,只是按照对象存活的特性将内存分成不同的区域,在不同的区域采用不同的算法,而这些算法是上面三种的组合。
JVM中,堆内存被分为新生代和老年代。
新生代中,又细划分为,Eden、Survivor1、Survivor2,这个区域一般是复制算法
老年代,对象在新生代中经过多次复制后仍然存活(默认是对象年龄达到15),会被复制到老年代,或者大对象等直接在老年代分配,而老年代根据垃圾收集器的不同,会采用不同的算法,如标记清除,标记整理等
算法名称 | 优点 | 缺点 |
---|---|---|
标记清除 | 实现简单 | 回收后内存会变得不连续,即内存碎片,新创建的大对象可能会不够分配 |
复制算法 | GC后的内存空间是连续的 | 一部分的内存浪费 |
标记整理 | GC后内存是连续的,也没有浪费内存 | 效率低 |
分代算法 | 将内存划分成不同的区域,采用不同的收集算法进行回收 | – |
2. 如何判断对象已死亡?(如何标记哪些对象需要被回收)
回收算法就是为了回收不再存活的对象,那么如何判断对象死亡呢?
2.1 引用计数法
每个对象中都有一个计数器,每当对象被引用一次就加1,失去引用就减1,这样当计数器为0时,代表着可回收。这种方法足够简单高效,但是解决不了循环引用问题,例如:
public class A {
public B b;
}
public class B {
public A a;
}
public static void main(String[] args) {
A a = new A();
B b = new B();
a.b = b;
b.a = a;
a = null;
b = null;
}
a 对象中引用b对象,b对象中引用a对象,导致a、b的计数器都不为0,本该被回收的对象无法回收,该方法基本不被虚拟机使用。
2.2可达性分析
通过一些列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时(就是从GC Roots 到这个对象是不可达),则证明此对象是不可用的,它们会被判定为可回收对象,当然要真正宣告一个对象死亡,至少要经历两次标记过程。
可以作为GC Roots中的对象:
- JAVA虚拟机栈中引用的对象(线程私有的空间,说明线程正在执行中,栈帧中引用的对象多少都是存活的)
- 本地方法栈(Native方法)中引用的对象(与虚拟机栈类似)
- 方法区中类静态变量和常量引用的对象
3.垃圾收集器CMS,G1,ZGC
CMS
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。这是因为CMS收集器工作时,GC工作线程与用户线程可以并发执行,以此来达到降低收集停顿时间的目的.
CMS收集器仅作用于老年代的收集,是基于标记-清除算法的,过程分为4个步骤:
初始标记
并发标记
重新标记
并发清除
初始标记、重新标记这两个步骤仍然需要停顿(Stop-the-world)。
初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。
并发标记阶段就是进行GC Roots 跟踪的过程。
而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始阶段稍长一些,但远比并发标记的时间短。
优点:并发收集、低停顿
缺点:对CPU资源非常敏感,无法处理浮动垃圾,标记-清除算法会产生内存碎片。
CMS收集器之所以能够做到并发,根本原因在于采用基于“标记-清除”的算法并对算法过程进行了细粒度的分解。而标记-清除算法将产生大量的内存碎片这对新生代来说是难以接受的(新生代占用的比例小,内存更加宝贵),因此新生代的收集器并未提供CMS版本。
JVM在暂停的时候,需要选准一个时机。由于JVM系统运行期间的复杂性,不可能做到随时暂停,因此引入了安全点的概念
安全点(Safepoint)
安全点,即程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。Safepoint的选定既不能太少以至于让GC等待时间太长,也不能过于频繁以致于过分增大运行时的负荷。
安全点的初始目的并不是让其他线程停下,而是找到一个稳定的执行状态。在这个执行状态下,Java虚拟机的堆栈不会发生变化。这么一来,垃圾回收器便能够“安全”地执行可达性分析。只要不离开这个安全点,Java虚拟机便能够在垃圾回收的同时,继续运行这段本地代码。程序运行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的。“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safepoint。对于安全点,另一个需要考虑的问题就是如何在GC发生时让所有线程(这里不包括执行JNI调用的线程)都“跑”到最近的安全点上再停顿下来。
两种解决方案:
抢先式中断(Preemptive Suspension)
抢先式中断不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机采用这种方式来暂停线程从而响应GC事件。
主动式中断(Voluntary Suspension)
主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。
G1
它虽然还是保留的有新生代和老年代的概念,但是新生代和老年代之前再也不是区域上的隔离了。它将整个 Java 堆划分为多个大小相等的独立区域,叫做 Region 。而新生代和老年代就是由一个个 Region 动态组成的区域,它们可以是不连续的区间。每一个 Region 都可以根据需要,扮演新生代的 Eden 空间,Survivor 空间,或者老年代空间。除此之外它还有一类特殊的区域叫做 Humongous,专门用来存储大对象。G1的堆内存被划分为多个 Region , Region 的总个数在 2048 个左右,默认是 2048 。对于一个 Region 来说,是逻辑连续的一段空间,其大小的取值范围是 1MB 到 32MB 之间。
G1 工作过程:
- 初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。
- 并发标记:是从GC Roots开始堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行
- 最终标记:是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行
- 筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。这个阶段也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率
4.CMS和G1都是有一个并发标记的过程,并发标记要解决什么问题?带来了什么问题?怎么解决这些问题呢?
解决的问题:消减标记过程中的停顿时间。那就是让垃圾回收器和用户线程同时运行,并发工作。也就是我们说的并发标记的阶段。
带来的问题:在该阶段,并发标记的同时用户线程也在执行,也就是说用户线程也在修改对象的引用关系,那么可能会带来如下问题:
一种是把原本消亡的对象错误的标记为存活,这不是好事,但是其实是可以容忍的,只不过产生了一点逃过本次回收的浮动垃圾而已(如果面试官问,浮动垃圾如何产生的,在那一阶段产生的,就可以用这个问题回答),下次再清理就可以
一种是把原本存活的对象错误的标记为已消亡,这就是非常严重的后果了,一个程序还需要使用的对象被回收了,那程序肯定会因此发生错误
相比于第一个问题,显然第二个要严重的多,那么如何解决这个问题呢?
Wilson于1994年在理论上证明了,当且仅当以下两个条件同时满足时,会产生“对象消失”的问题,即原本应该是黑色的对象被误标为白色:
赋值器插入了一条或多条从黑色对象到白色对象的新引用;
赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。
由于两个条件之间是当且仅当的关系。所以,我们要解决并发标记时对象消失的问题,只需要破坏两个条件中的任意一个就行。 于是产生了两种解决方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning,SATB)
在HotSpot虚拟机中,CMS是基于增量更新来做并发标记的,G1则采用的是原始快照的方式。
增量更新,解决的是第一个问题:
在三色标记中,当黑色标记的对象引用白色标记的对象时,就将这个新的引用记录下来,等并发扫描结束之后,再以这些记录过引用关系中的黑色对象为根,重新向下扫描一次。这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了,这个过程其实就是重新标记。
原始快照,解决的是第二个问题:
当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。 这个可以简化理解为:无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照开进行搜索
zgc
zgc 是一个以实现低停顿(停顿时间小于10ms)为目标的垃圾收集器,JDK11 开始支持。
与G1 一样,ZGC也采用基于Region的堆内存布局
很多低延迟高可用Java服务的系统可用性经常受GC停顿的困扰。GC停顿指垃圾回收期间STW(Stop The World),当STW时,所有应用线程停止活动,等待GC停顿结束。