当前位置: 首页 > 后端技术 > Java

常见垃圾收集算法及具体实现

时间:2023-04-01 22:30:16 Java

分代收集理论目前主流虚拟机的垃圾收集器大多遵循“分代收集理论”进行设计。其实是基于两个假设:WeakGenerationalHypothesis:大部分对象生死攸关(可以扔掉~)StrongGenerationalHypothesis):经过多次垃圾回收还没有被回收的对象,被回收的可能性较小。基于以上两个假设,收集器应该将Java堆划分为不同的区域,然后回收的对象根据年龄居住在不同的区域。也就是把前几次垃圾回收后生死存亡难存活的对象放到一个区域。该区域只需要关注那些存活的对象,不需要关心大量即将被回收的对象;没有被垃圾回收回收的对象放在一个区域中。由于该区域的物品难以回收,因此该区域可以以较低的频率回收。这样既兼顾了垃圾回收的时间开销,又兼顾了内存空间的有效利用。后面讲解的“mark-clear”、“mark-copy”、“mark-sort”算法,都是基于这个分代理论设计的。但是分代收集有一个很明显的问题,就是“跨代引用”的问题。解决“跨代引用”可以通过在收集新生代对象时遍历所有老年代对象来保证对象可回收,但这无疑会给内存回收带来较大的性能负担。因此,得出第三条经验法则:跨代引用假说:跨代引用是指与同代引用相比,具有相互引用的对象数量非常少,应该同时生或死。例如,如果新生代中的对象被老年代中的对象引用,那么在新生代垃圾回收时,由于跨代引用,新生代中的对象无法被回收,新生代中的对象将多次回收后也提升到老年代,跨代引用也消失了。这个假设告诉我们,不需要扫描老年代的所有对象来解决跨代引用,也不需要浪费空间去记录新生代中哪些对象有跨代引用。我们只需要在新生代中建立一个全局的数据结构(“内存集”),这个结构将老年代划分为若干区域,并标识出老年代中哪一块内存可能存在跨代引用。之后,当发生MinorGC时,只有包含跨代引用的小块内存对象才会被扫描,如果GCRoots,该方法需要在对象改变引用关系时保持记录数据的正确性。MinorGC/YongGC:新生代是否收集,只收集新生代MajorGC/OldGC:老年代收集,指的是老年代的垃圾收集。目前只有CMS垃圾收集器有单独收集老年代的行为。MixedGC:收集整个新生代和部分老年代。目前,只有G1垃圾收集器具有此行为。FullGC:Wholeheapcollection,收集整个J??ava堆和方法区垃圾回收。常见的垃圾收集算法mark-and-sweep算法分为两个阶段:“标记”和“清除”。首先对所有需要回收的对象进行标记,标记完成后统一回收所有标记的对象,反之亦然。缺点:执行效率不稳定。如果Java堆中包含大量的对象,而且大部分都需要被回收,此时就必须进行大量的标记和清除动作,导致标记和清除这两个过程的执行效率下降。增长和下降;内存空间碎片化,标记清除后会产生大量不连续的空间碎片,过多的空间碎片可能会导致大对象在分配内存时找不到足够的连续内存而不得不触发垃圾回收动作。标记复制算法为了解决标记-清除算法在面对大量可回收对象时执行效率低的问题,1969年有人提出了“半区复制”垃圾回收算法,将可用内存进行划分分成两块,每次只使用其中一块,当这块用完后,将存活的对象复制到另一块,然后一次性清理掉已使用的内存空间。缺点:如果内存中存活的对象较多,该算法会产生大量的内存复制开销,因此该算法不适用于老年代。一次只使用一半内存,复制过去时无需考虑空间碎片问题,只需要顺序分配内存即可。只能使用一半的内存,很浪费空间。需要暂停用户线程来标记和清理可回收对象。暂停时间比下面提到的“标记排序”算法的暂停时间短。mark-sort算法是因为“mark-copy”算法在alive的对象很多的时候,copy开销比较大。大,所以不适合用在对象不易回收的老年代。更重要的是,因为复制算法会浪费一半使用的空间,所以老年代的大部分对象都是不会被回收的区域。选择复制算法。根据老年代对象的特点,1974年提出了另一种有针对性的“MarkCompact”算法,其标记过程与“Mark-Clear”算法相同,但后面的步骤不是直接回收可回收对象,它将所有存活的对象移动到内存空间的另一端,然后直接清理边界外的内存。两种算法的本质区别在于前者是非移动恢复算法,后者是移动恢复算法。回收后是否移动存活对象是一个有风险的决定,有利也有弊:如果移动对象,在老年代,每次收集有大量存活对象的地方,移动存活对象,并更新所有引用这些对象的地方objects将是一个极其重载的操作,而这个对象移动操作必须是STW(最新的ZGC和Shenandoah收集器使用readbarrier技术来实现整理进程和用户线程的并发执行)。如果不移动对象,那么面对大量不连续的内存空间只能依靠更复杂的内存分配器和内存访问器来解决,但是对于内存访问操作来说,增加额外的负担势必会影响程序的吞吐量。基于以上两点,移动与不移动对象都有缺点。如果对象被移动,回收就比较复杂,如果不移动,内存的分配就比较复杂。从垃圾回收的停顿时间来看,不移动对象的停顿时间更短,甚至不停顿,但移动对象从程序整体吞吐量上来说更划算。立即不移动对象会使收集器效率更高,但由于内存分配频率远高于垃圾收集,这部分会花费更多时间,整体吞吐量仍然会下降。HotSpot虚拟机关注吞吐量ParallScavenge收集器基于mark-compact算法,而latency-focusedCMS收集器基于mark-sweep。另一种方法是不要在内存分配和访问上增加太多额外的负担。方法是让虚拟机大部分时间使用“mark-clear”算法,暂时容忍内存碎片的存在,直到影响到对象分配内存为止。使用“标记-排序”算法再次收集,得到有规律的内存空间。当CMS垃圾收集器面临过多的空间碎片时,就会使用这种方法。算法实现详解GCRoots根节点枚举问题枚举所有GCRoots耗时问题,为了保证枚举根节点时对象引用关系的一致性,需要STW。解决方法,不遍历所有GCRoots怎么办由于虚拟机垃圾收集是精准收集,虚拟机知道对象引用存放在哪里,HotSpot中使用了OOPMap的数据结构,即扫描的时候可以直接从OOPMap中获取哪些位置有引用,不需要遍历所有的GCRoots。安全点问题对象之间的引用关系有很多变化,导致很多指令改变了OOPMap的内容,不可能每条指令都生成一个OOPMap。解决方案,如果我们可以避免对象之间引用关系的频繁变化影响GC呢?HotSpot不会为每条指令生成OOPMap,而只是将这些信息记录在一个“特定位置”,这个位置被称为“安全点(SafePoint)”。充当“安全点”的目的是告诉用户程序不能暂停在任何位置进行垃圾回收。安全点的选择不能太小,因为会导致垃圾回收程序等待时间过长,因为程序中每条指令的执行时间都很短,程序不可能执行很长时间因为指令流太长了。“长时间执行”最明显的特点就是指令序列的复用,比如“方法调用”、“循环跳转”、“异常跳转”等,如何让所有线程运行到最近的安全点并且在垃圾回收的时候停止中断,如果发现用户线程中断的地方不在安全点,就让这个线程继续执行,直到运行到安全点(几乎没有虚拟机实现这个method)active(需要用户线程配合)notactive中断用户线程,设置一个flag,用户线程继续轮询这个flag,一旦发现flag为真,就会挂在最近的安全点,flag和safepoint是重合的,HotSpot也考虑在创建对象或者为对象分配内存的位置加一个flag位,以检测是否即将发生垃圾回收,避免内存不足用于新对象的内存分配。只有safearea中的safepoints似乎可以完美解决垃圾回收前暂停用户线程的问题。安全点机制保证程序执行的时候,短时间内是安全的,但是如果程序不执行怎么办,比如用户线程处于Sleep或者Blocked状态,线程无法响应中断此时虚拟机的请求,但是虚拟机不能一直等待用户线程执行,所以引入安全区。安全区可以保证在某个代码段内,References不发生变化。所以在这个区域的任何地方开始垃圾收集都是安全的。并发可达性分析??GCRoots对象遍历一致性快照可达性分析是目前主流垃圾收集器用来判断对象是否存活的算法,但是该算法理论上要求整个过程都基于一个能够保证一致性的快照进行分析,只能是在中心进行,这意味着需要STW。上面提到通过OopMap优化了根节点枚举,停顿时间减少了很多,但是对象从GCRoos遍历到底层的时间是随着堆内存的大小成比例增加的(堆内存越大,对象越多,对象之间的关系越复杂,处理时间越长),为什么一定要在能保证一致性的快照上呢?为了遍历,可以自行查看三色标记的相关信息。这里简单说明一下三色标记:假设黑色是已经遍历过且可达的对象,灰色是已经遍历过,白色是待遍历或者已经遍历但是还未到达的对象仍然无法访问,但至少有一个对该对象的引用尚未被遍历。扫描后,E和D指代下图中的C对象。遍历过程中,D和C的引用断开了,所以E和D还是白的,但是遍历完C的所有引用对象后,D对象的引用关系发生了变化,被用户线程修改为被B引用了,但是标记线程不会再标记了,所以D对象会被回收,这样就会出大问题,用过的对象突然被回收了,是不是很可怕,所以GC根的遍历过程需要在一个能保证一致性的快照上进行。解决方案目前大部分垃圾收集器都采用可达性分析算法。如果在遍历GCRoots对象的过程中需要STW,这种情况会影响到大部分的垃圾收集器。如果这部分停顿时间能够减少,收益将是巨大的。在上面的三色标记中,我们发现并发标记的原因是:用户线程将黑色对象的引用指向了白色对象,用户线程断开了灰色对象对白色对象的引用。因此,解决方案有两种:增量更新定量更新需要解决第一种情况。当黑色对象插入指向白色对象的新引用关系时,记录新插入的引用。并发扫描结束后,将记录的引用关系以黑色对象为根再次扫描,也就是说只要黑色对象指向白色对象,黑色对象就会变成灰色对象。原始快照原始快照需要解决第二种情况。当灰色对象要删除指向白色对象的引用关系时,它会记录要删除的引用,并发扫描结束后,再引用这些记录。中的灰色对象是根,需要重新扫描。这里可以理解为,无论是否删除引用关系,都会根据刚开始扫描时的对象快照进行查找。以上两个版本的虚拟机都是通过writebarriers实现的,两种方案都有应用。比如CMS是基于增量更新进行并发标记的,G1和Shenandoah使用的是原始快照。本文由博客多发平台OpenWrite发布!