垃圾收集算法垃圾收集算法的实现涉及到很多程序细节,不同平台的虚拟机操作内存的方法也不同。这里不过多讨论算法实现,只着重介绍分代收集理论和几种算法思想及其发展过程。从如何判断对象消亡的角度来看,垃圾回收算法可以分为:“引用计数GC”,也常被称为“直接垃圾回收”和“TracingGC”。它也常被称为“间接垃圾收集”。由于主流的Java虚拟机都没有涉及引用计数垃圾回收算法,所以下面介绍的所有算法都属于跟踪垃圾回收的范畴。代收理论强,代收假说弱。目前商用的虚拟机垃圾收集器大多是根据“分代收集”(GenerationalCollection)[1]的理论设计的。分代集合称为理论,其本质是一套符合大多数程序实际情况的经验法则。它基于两个分代假设:弱分代假设(WeakGenerationalHypothesis):大多数对象都是短暂的。强分代假设(StrongGenerationalHypothesis):在垃圾回收过程中幸存下来的对象越多,它就越难消亡。[1]值得注意的是,分代收集理论也有其缺陷。最新(或实验中)的几款垃圾收集器都展示了针对全区收集设计的思路,或者说可以支持全区无代收集的工作模式。这两个分代假设共同为许多常用的垃圾收集器建立了一个一致的设计原则:收集器应该将Java堆划分为不同的区域,然后根据对象的年龄(年龄意味着对象在垃圾收集过程中存活的数量)进行回收次)分配到不同的区域进行存储。(分代处理)如果一个区域的大部分对象都在消亡,那么每次回收只需要注意如何保留少量存活的对象,就可以以较低的成本回收大量空间;(handlingthenewgeneration)如果一个区域的大部分对象都难以消亡,那么虚拟机可以用较低的频率回收这个区域,这既兼顾了垃圾回收的时间开销,又兼顾了内存空间的有效利用。(处理老年代)Java堆被划分为不同的区域后:垃圾收集器一次只能回收一个或部分区域——于是就有了“MinorGC”、“MajorGC”和“FullGC”》对此类回收类型的划分;针对不同的区域使用匹配的垃圾收集算法——从而开发出有针对性的垃圾收集算法,如“标记-复制算法”、“标记-清除算法”和“标记-排序算法”。将分代收集在目前商业化的Java虚拟机理论中,设计者一般将Java堆分为两个区域:新生代和老年代。收集,每次收集存活下来的少量对象,会逐渐提升到老年代进行存储。新生代中的对象可能被老年代中的对象引用。因此,如果只需要对新生代进行一次垃圾回收,为了保证可达性分析的正确性,就需要遍历整个老年代中的所有对象,反之亦然[2]。但是遍历所有老年代对象的方案会带来很大的性能负担。为了解决这个问题,需要用到第三条经验法则:代际参照假说:代际参照是相对于同代参照而言极少数。[2]通常只能独立收集新生代,所以这里的“反向”情况只是理论上允许的。事实上,除了CMS收集器之外,并没有只针对老年代的收集。根据这个假设,我们不应该再为了少量的跨代引用而扫描整个老年代,也不需要浪费空间去记录每个对象是否存在,存在哪些跨代引用。我们只需要在新生代创建一个全局的数据结构(这个结构叫做“RememberedSet”),将老年代分成若干小块,并标识老年代中哪一块内存会有跨代引用.之后,当发生MinorGC(指新生代的垃圾回收)时,只有小块内存中包含跨代引用的对象才会被加入到GCRoots中进行扫描。虽然这种方法需要在对象改变引用关系时(比如赋值给自己或者某个属性)保持记录数据的正确性,会增加一些运行时的开销,但是还是比扫描整个旧的更划算生成时收集的。为了避免混淆,这里统一定义回收类型的划分:部分回收(PartialGC):是指以不完全回收整个Java堆为目标的垃圾回收,进一步分为:MinorGC/YoungGC:指的是目标只是年轻代的垃圾回收。老年代收集(MajorGC/OldGC):指的是目标仅为老年代的垃圾收集。目前只有CMS收集器有单独收集老年代的行为。另外请注意,“MajorGC”这个词在不同的资料中往往有不同的含义,需要根据上下文区分是指老年代的收集还是整堆的收集。混合收集(MixedGC):指的是垃圾收集,其目标是收集整个新生代和部分老年代。目前只有G1收集器表现出这种行为。全堆收集(FullGC):收集整个J??ava堆和方法区的垃圾收集。Mark-SweepAlgorithm最早也是最基本的垃圾收集算法是“Mark-Sweep”算法,它是由Lisp之父JohnMcCarthy于1960年提出的。该算法分为“标记”和“清除”两个阶段”。首先,标记所有需要回收的对象。标记的对象。所以无论标记什么对象,被清除的永远是垃圾对象。标记过程是判断对象是否属于垃圾的过程,即可达性分析算法的过程。后续的采集算法大多是在mark-sweep算法的基础上改进其不足之处。它有两个主要缺点:执行效率不稳定。如果Java堆中包含大量的对象,而且大部分对象都需要回收,则必须进行大量的标记和清除动作,导致标记和清除两个过程的执行效率降低,如对象数量增加;内存空间碎片变化。标记和清除后,会产生大量不连续的内存碎片。太多的空间碎片可能导致程序以后需要分配大对象时,找不到足够的连续内存,不得不提前触发另一次垃圾回收动作。标记-复制算法标记-复制算法通常简称为复制算法。半区复制为了解决mark-clear算法在面对大量可回收对象时执行效率低的问题,Fenichel在1969年提出了一种称为“半区复制”(SemispaceCopying)的垃圾回收算法:可用内存按容量分成大小相等的两块,一次只使用其中一块。当这块内存用完后,将存活的对象复制到另一块中,然后一次性清理掉已使用的内存空间。半区复制算法的优点:当大部分对象可回收时,该算法只需复制少量存活对象,执行效率高。每次都是回收整个半个区域的内存,所以分配内存的时候不需要考虑空间碎片的复杂情况,只需要移动堆顶指针,按顺序分配即可。目前商业化的Java虚拟机大多采用这种收集算法先回收新生代,但并不需要按照1:1的比例划分新生代的内存空间。Appel-stylerecycling1989年,AndrewAppel针对具有“生死永生”特点的对象,提出了一种更优化的半区副本生成策略,现称为“Appel-style回收”。HotSpot虚拟机的Serial、ParNew等新一代收集器就是采用这种策略来设计新一代的内存布局[1]。Appel式回收的具体做法是:将新生代分成一个较大的Eden空间和两个较小的Survivor空间,每次分配内存时只使用Eden和其中一个Survivor空间。当发生垃圾回收时,一次性将Eden和Survivor中存活的对象复制到另一个Survivor空间,然后直接清理Eden和使用过的Survivor空间。HotSpot虚拟机默认的Eden和Survivor的比例是8:1,即每个新生代中的可用内存空间是整个新生代容量的90%(Eden的80%加上一个Survivor的10%),并且SurvivorSpace只有一个,也就是新生代的10%会被“浪费”。但是谁也没有办法100%保证每次回收只有不超过10%的物品存活下来,所以Appel式回收还有一个安全设计,作为罕见情况下的“逃生门”,当Survivor空间不足以容纳一个MinorGC后存活的对象时,需要依赖其他内存区域(其实大部分都是老年代)进行分配保证(HandlePromotion)。Mark-SortingAlgorithm一般来说,mark-sweep算法不能直接用在老年代,因为:mark-copy算法在对象存活率高的时候需要进行更多的复制操作,效率会降低。如果不想浪费50%的空间,就需要有额外的空间分配保证来应对所有对象100%存活的极端情况。针对对象在老年代的生存特性,EdwardLueders在1974年提出了另一种靶向方法“Mark-Compact”算法,标记过程仍然与“Mark-Clear”算法相同,只是后面的步骤不再直接清理可回收对象,而是让所有存活的对象都去内存空间移动,然后直接清理边界外的内存。mark-sweep算法和mark-sort算法的本质区别是前者是非移动的回收算法,而后者是移动的。回收后是否移动存活对象是一个冒险的决定,有利也有弊:如果移动存活对象,移动存活对象并更新所有引用这些对象的地方将是一个极其繁重的操作,而这个对象移动操作must只能通过一直挂起用户应用程序来完成[1]。这样的停顿被最初的虚拟机设计者形象地描述为“StopTheWorld”[2]。但是,如果像mark-sweep算法一样完全忽略存活对象的移动和排序,存活对象散落在堆中造成的空间碎片问题只能通过更复杂的内存分配器和内存访问器来解决。例如,使用“无分区分配链表”解决内存分配问题(计算机硬盘上大文件的存储不需要物理上连续的磁盘空间,碎片硬盘上的存储和访问是通过硬盘分区表)。内存访问是用户程序最频繁的操作,甚至没有之一。如果在这个环节增加额外的负担,肯定会直接影响到应用的吞吐量。[1]最新的ZGC和Shenandoah收集器采用ReadBarrier(ReadBarrier)技术实现排序进程和用户线程并发执行[2]通常,mark-sweep算法也需要暂停用户线程来标记和清理可回收对象。只是停顿时间比较短。基于以上两点,移动与不移动对象都有缺点。如果对象被移动,内存回收会比较复杂,如果不移动,内存分配会更复杂。从垃圾回收的暂停时间来看,非移动对象的暂停时间会更短,甚至不需要暂停,但从整个程序的吞吐量来看,移动对象会更划算.HotSpot虚拟机中关注吞吐量的ParallelScavenge收集器基于mark-sort算法,而关注延迟的CMS收集器基于mark-clear算法,也从侧面印证了这一点。此外,还有一种“和谐泥”方案,不会给内存分配和访问增加太多额外负担。方法是让虚拟机大部分时间使用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间对象的碎片程度大到影响对象分配时,标记-sort算法用于再次收集它以获得规则的内存空间。上述基于mark-sweep算法的CMS收集器在面对空间碎片过多时采用了这种方式。个人总结分代收集理论的三个假设是什么?弱世代假设:大多数对象都是短暂的。强分代假设(StrongGenerationalHypothesis):在垃圾回收过程中幸存下来的对象越多,它就越难消亡。代际参照假说(IntergenerationalReferenceHypothesis):代际参照与同代参照相比只是极少数。三代假设的作用是什么?弱分代假说和强分代假说指导Java堆分为不同的区域。不同的区域存储不同年龄的对象,不同的区域使用不同的垃圾回收算法。跨代引用假设指导我们在新生代区域收集垃圾时,扫描整个老年代对象寻找少量跨代引用,而只需要扫描部分具有跨代的老年代对象参考。回收类型分类的一般定义是什么?部分回收(PartialGC):是指以不完全回收整个Java堆为目标的垃圾回收,又分为:MinorGC/YoungGC:是指以新生代为目标的垃圾回收。老年代收集(MajorGC/OldGC):指的是目标仅为老年代的垃圾收集。目前只有CMS收集器有单独收集老年代的行为。另外请注意,“MajorGC”这个词在不同的资料中往往有不同的含义,需要根据上下文区分是指老年代的收集还是整堆的收集。混合收集(MixedGC):指的是垃圾收集,其目标是收集整个新生代和部分老年代。目前只有G1收集器表现出这种行为。全堆收集(FullGC):收集整个J??ava堆和方法区的垃圾收集。标记清除算法的缺点?执行效率不稳定。如果Java堆中包含大量的对象,而且大部分对象都需要回收,则必须进行大量的标记和清除动作,导致标记和清除两个过程的执行效率降低,如对象数量增加;所以不适用新生代更适合老年代。标记和清除后,会产生大量不连续的内存碎片。mark-copy算法主要应用在哪部分空间?HotSpot中新生代内存默认是怎么划分的?mark-copy算法主要应用于堆的新生代。Appel-stylecollection将新生代分为一个Eden空间和两个Survivor空间。HotSpot默认Eden和Survivor的大小比例为8:1。标记组织算法的过程是怎样的?mark-collat??e和mark-sweep算法有什么区别?标记排序算法的标记过程与“标记-清除”算法相同,但后面的步骤并不直接清理可回收对象,而是让所有存活的对象移动到内存空间的一端,并且然后直接清理边界外的内存。mark-sweep算法和mark-sort算法的本质区别是前者是非移动的回收算法,而后者是移动的。移动内存回收会比较复杂,停顿时间会比较长,但是整个程序的吞吐量很高。不移动,内存分配会更复杂,程序整体吞吐量会更低,但停顿时间会更短。参考《深入理解Java虚拟机(第三版)》
