垃圾收集器在对堆对象进行回收前,第一件事就是要确定这些对象之中那些还存活着,那些对象已经死去。
引用计数算法在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
可达性分析算法通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
回收方法区方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。回收废弃常量与回收Java堆中的对象非常类似,已经没有任何字符串对象引用常量池中的某个常量,且虚拟机中也没有其他地方引用这个字面量,此时发生内存回收,这个常量会被系统清理出常量池。
判定一个类不在被使用的三个条件:
- 该类所有的实例都已经被回收。加载该类的类加载器已经被回收。该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
从如何判定对象消亡的角度出发,垃圾收集算法可以划分为“引用计数式垃圾收集”和追踪式垃圾收集
两大类,这两类也被称为“直接垃圾收集”和“间接垃圾收集”。
分代收集理论商业虚拟机的垃圾收集器,大多数遵循“分代收集”的理论,分代收集建立在两个分代假说上。
弱分代假说:绝大多数对象都是朝生夕灭的。
强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。
这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将Java堆划分出不同的领域,然后将回收对象依据其年龄分配到不同的区域之中存储。
运用分代收集理论后,至少把Java堆划分为新生代和老年代两个区域。
在新生代中,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。
如果要进行一次只局限于新生代或老年代区域内的收集,但新生代中的对象是完全有可能被老年代所引用,为了找到该区域中的存活对象,不得不在固定的GC Roots之外,在遍历整个老年代中所有的对象来确保可达性分析的正确性,反正也如此。这样为内存回收带来很大的性能负担,为解决这个问题,添加第三条经验法则
跨代引用假说:跨代引用相对于同代引用来说仅占极少数。
标记-清除算法算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来运用。标记过程就是对象是否属于垃圾的判定过程。
两个主要缺点:
- 执行效率不稳定:当Java堆中包含大量对象,同时大部分需要回收时,必须进行大量标记和清除动作,使执行效率随对象数量增加而降低。内存空间的碎片化问题:标记、清除产生大量不连续的内存碎片,当后续需要分配较大内存时,无法找到足够内存空间而触发另一次垃圾回收。
为解决标记-清除算法面对大量可回收对象时执行效率低的问题,提出了“半区复制”的垃圾收集算法:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完后,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,但对于大多数对象都是可回收的情况,算法需要复制的就是少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不要考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。
标记-整理算法标记过程与“标记-清除”算法一样,但是后续步骤是让所有存活的对象向内存空间一端移动,然后直接清理掉边界以外的内存。但是移动后需要更新所有引用移动对象的地方。
经典垃圾收集器 Serial收集器最基础、历史最悠久的收集器。单线程收集器:它在进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。
ParNew收集器Serial收集器的多线程并行版本。
Parallel Scavenge收集器新生代收集器,基于标记-复制算法实现的收集器,能够并行收集的多线程收集器。
目标是达到一个可控制的吞吐量。吞吐量:处理器运行用户代码的时间 / 处理器总消耗的时间
Serial Old收集器Serial收集器的老年代版本,单线程收集器,使用标记-整理算法。主要供客户端模式下的HotSpot虚拟机使用。
Parallel Old收集器Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法。
CMS收集器以获得最短回收停顿时间为目标的收集器。
整个收集过程分为四步:
- 初始并发并发标记重新标记并发清除
缺点:
对处理器资源非常敏感无法处理收集过程中新产生的垃圾对象会产生大量空间碎片 Garbage First收集器(G1)
面向服务端应用的垃圾收集器。
G1抛弃了之前的分代收集的方式,面向整个堆内存进行回收,把内存划分为多个大小相等的独立区域Region。
一共有4种Region:
- 自由分区Free Region年轻代分区Young Region,年轻代还是会存在Eden和Survivor的区分老年代分区Old Region大对象分区Humongous Region
每个Region的大小通过-XX:G1HeapRegionSize来设置,大小为1~32MB,默认最多可以有2048个Region,那么按照默认值计算G1能管理的最大内存就是32MB*2048=64G。
对于大对象的存储,存在Humongous概念,对G1来说,超过一个Region一半大小的对象都被认为大对象,将会被放入Humongous Region,而对于超过整个Region的大对象,则用几个连续的Humongous来存储
G1的回收过程分为以下四个步骤:
- 初始标记:标记GC ROOT能关联到的对象,需要STW并发标记:从GCRoots的直接关联对象开始遍历整个对象图的过程,扫描完成后还会重新处理并发标记过程中产生变动的对象最终标记:短暂暂停用户线程,再处理一次,需要STW筛选回收:更新Region的统计数据,对每个Region的回收价值和成本排序,根据用户设置的停顿时间制定回收计划。再把需要回收的Region中存活对象复制到空的Region,同时清理旧的Region。需要STW。
除了并发标记外,其余阶段也要完全暂停用户线程。
低延迟垃圾收集器 Shenandoah收集器非官方出品,并被官方排挤,被OracleJDK拒绝加入
ZGC收集器ZGC的目标是希望在尽可能对吞吐量影响不太大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟。
ZGC收集器是一款基于Region内存布局的,(暂时) 不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。
工作过程
- 并发标记(Concurrent Mark):并发标记是遍历对象图做可达性分析的阶段,前后也要经过初始标记、最终标记的短暂停顿,而且这些停顿阶段所做的事情在目标上也是相类似的。ZGC 的标记是在指针上而不是在对象上进行的,标记阶段会更新染色指针中的Marked 0、Marked 1标志位。并发预备重分配(Concurrent Prepare for Relocate):这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。ZGC划分Region的目的并非为了像G1那样做收益优先的增量回收。ZGC每次回收都会扫描所有的Region,用范围更大的扫描成本换取省去G1中记忆集的维护成本。因此,ZGC的重分配集只是决定了里面的存活对象会被重新复制到其他的Region中,里面的Region会被释放,而并不能说回收行为就只是针对这个集合里面的Region进行,因为标记过程是针对全堆的。此外,在JDK 12的ZGC中开始支持的类卸载以及弱引用的处理,也是在这个阶段中完成的。并发重分配(Concurrent Relocate):重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。得益于染色指针的支持,ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”(SelfHealing)能力。这样做的好处是只有第一次访问旧对象会陷入转发,也就是只慢一次,对比 Shenandoah的Brooks转发指针,那是每次对象访问都必须付出的固定开销,简单地说就是每次都慢, 因此ZGC对用户程序的运行时负载要比Shenandoah来得更低一些。还有另外一个直接的好处是由于染色指针的存在,一旦重分配集中某个Region的存活对象都复制完毕后,这个Region就可以立即释放用于新对象的分配(但是转发表还得留着不能释放掉),哪怕堆中还有很多指向这个对象的未更新指针也没有关系,这些旧指针一旦被使用,它们都是可以自愈的。并发重映射(Concurrent Remap):重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,这一点从目标角度看是与Shenandoah并发引用更新阶段一样的,但是ZGC的并发重映射并不是一个必须要“迫切”去完成的任务,因为前面说过,即使是旧引用,它也是可以自愈的,最多只是第一次使用时多一次转发和修正操作。重映射清理这些旧引用的主要目的是为了不变慢(还有清理结束后可以释放转发表这样的附带收益),所以说这并不是很“迫切”。因此,ZGC很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销。一旦所有指针都被修正之后,原来记录新旧对象关系的转发表就可以释放掉了。



