当前位置: 首页 > 科技观察

深入理解JVM,7种垃圾收集器,看完跪了

时间:2023-03-17 18:02:46 科技观察

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。Java虚拟机规范没有指定垃圾收集器应该如何实现。因此,不同厂商、不同版本的虚拟机提供的垃圾收集器可能会有很大差异,一般都会根据自己的需求提供参数供用户使用。应用特点和要求结合起来形成了各个时代使用的采集器。下面讨论的收集器是基于JDK1.7Update14之后的HotSpot虚拟机(这个版本官方提供了商用的G1收集器,之前G1还处于实验状态),本虚拟机包含的所有收集器都是如下图提示:上图展示了7个作用于不同世代的收集器。如果两个收集器之间有连接,就意味着它们可以一起使用。虚拟机所在的区域表示它属于新生代收集器还是老年代收集器。Hotspot实现这么多的收集器正是因为目前没有完美的收集器,只是针对具体的应用选择最合适的收集器。相关概念并行与并发并行(Parallel):指多个垃圾回收线程并行工作,但此时用户线程仍处于等待状态。并发:指用户线程和垃圾回收线程同时执行(但不一定并行,有可能交替执行),用户程序继续运行。垃圾收集器运行在另一个CPU上。吞吐量(Throughput)吞吐量是CPU花在运行用户代码上的时间占CPU总消耗时间的比例,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾回收时间)。假设虚拟机总共运行100分钟,垃圾回收耗时1分钟,则吞吐量为99%。MinorGCandFullGC新生代GC(MinorGC):指发生在新生代的垃圾收集动作。因为大多数Java对象都具有永恒的特性,所以MinorGC非常频繁,回收速度一般也比较快。详见上一篇文章。老年代GC(MajorGC/FullGC):指发生在老年代的GC。MajorGC的出现,往往伴随着至少一次MinorGC(但不是绝对的,在ParallelScavenge收集器的收集策略中。有直接MajorGC的策略选择过程)。MajorGC的速度一般比MinorGC慢10倍以上。NewGenerationCollectorSerialCollector串行(串行)收集器是最基本也是最古老的收集器。它是使用复制算法的新一代收集器。它曾经是(在JDK1.3.1之前)新一代的虚拟机。收藏的唯一选择。它是一种单线程收集器,只使用一个CPU或一个收集线程来完成垃圾收集工作。更重要的是,它必须在垃圾收集期间暂停所有其他工作线程,直到串行收集器收集结束(“StopTheWorld”)。这项工作由虚拟机在后台自动发起和完成。许多应用程序在用户不可见的情况下停止用户所有正常工作的线程是不可接受的。下图是Serial收集器的运行过程(老年代采用SerialOld收集器):为了消除或减少工作线程内存回收带来的停顿,HotSpot虚拟机开发团队在课程中开发JDK1.3以后的Java开发还有其他各种不错的收集器,后面会介绍。但这些收藏家的诞生并不意味着Serial收藏家“老废”。事实上,直到现在,它仍然是运行在Client模式下的HotSpot虚拟机默认的新生代收集器。相对于其他收集器还有优势:简单高效(相对于其他收集器的单线程),对于仅限于单CPU的环境,Serial收集器没有线程交互开销,专注于垃圾收集自然更高可以获得单线程收集效率。在用户的桌面应用场景中,分配给虚拟机管理的内存一般都不会很大,收集新生代几十兆甚至一两百兆(仅新生代使用的内存,以及桌面应用基本不会变大。),停顿时间完全可以控制在几十毫秒,最多一百毫秒,只要不是经常出现,这个停顿时间是可以接受的。因此,Serial收集器对于运行在Client模式下的虚拟机来说是一个不错的选择。ParNew收集器ParNew收集器是Serial收集器的多线程版本,也是新一代收集器。除了使用多线程进行垃圾收集外,其余行为包括Serial收集器可用的所有控制参数、收集算法(复制算法)、StopTheWorld、对象分配规则、回收策略等与Serial收集器完全相同,并且两者共享相同的相当多的代码。ParNew收集器的工作过程如下(老年代采用SerialOld收集器):ParNew收集器除了使用多线程收集外,与Serial收集器相比并没有太多的创新点,但是它运行在一个多线程上Server模式下虚拟机首选新生代收集器的重要原因与性能无关的是,除了Serial收集器外,目前只有CMS收集器(ConcurrentMarkSweep)可以工作,CMScollector是JDK1.5推出的划时代的收集器,具体内容在后面介绍。ParNew收集器在单CPU环境下永远不会比Serial收集器有更好的效果。即使由于线程交互的开销,在超线程技术实现的双CPU环境下,收集器也不能100%。保证被超越。在多CP??U环境下,随着CPU数量的增加,对于GC时系统资源的有效利用是非常有利的。默认启用的收集线程数与CPU数相同,当CPU数过多时可以通过-XX:ParallerGCThreads参数设置。ParallelScavenge收集器ParallelScavenge收集器也是一种并行的多线程新生代收集器,同样使用复制算法。ParallelScavenge收集器的特点是它的侧重点不同于其他收集器。CMS等收集器的重点是尽量减少垃圾收集时用户线程的停顿时间,而ParallelScavenge收集器的目标是实现可控的Throughput。停顿时间越短,越适合需要与用户交互的程序,良好的响应速度可以提升用户体验。高吞吐量可以高效地利用CPU时间,尽快完成程序的计算任务。主要适用于不需要过多交互的后台运行的任务。ParallelScavenge收集器除了明显提供可以精确控制吞吐量的参数外,还提供了一个参数-XX:+UseAdaptiveSizePolicy,这是一个开关参数。开启参数后,无需手动指定新生代的大小(-Xmn)、Eden区和Survivor区的比例(-XX:SurvivorRatio)、晋升到老年代的对象年龄(-XX:预留大小阈值)。虚拟机根据当前系统运行状态收集性能监控信息,并动态调整这些参数以提供最合适的停顿时间或最大吞吐量,这种方法称为GC自适应调整策略(GCErgonomics)。自适应调整策略也是ParallelScavenge收集器和ParNew收集器的一个重要区别。还有一点值得注意的是ParallelScavenge收集器不能和CMS收集器结合使用,所以在JDK1.6推出ParallelOld之前,如果新生代选择Para??llelScavenge收集器,只能和SerialOld收集器结合使用在老一代。OldCollectorSerialOldCollectorSerialOld是Serial收集器的旧版本。它也是一个单线程收集器,使用了“标记紧凑”(Mark-Compact)算法。这个collector的主要意义也是在Client模式下用于虚拟机。如果是Server模式,它还有两个用途:在JDK1.5及之前的版本(ParallelOld诞生之前)与ParallelScavenge收集器配合使用。作为CMS收集器的备份计划,在并发收集出现ConcurrentModeFailure时使用。它的工作流程与Serial收集器的工作流程相同。下面是再次使用Serial/SerialOld的工作流程图:ParallelOld收集器ParallelOld收集器是ParallelScavenge收集器的旧版本,使用多线程和“mark-finishing”算法。如前所述,此收集器仅在JDK1.6中提供。在此之前,如果新生代选择了ParallelScavenge收集器,那么老年代就只能选择SerialOld,所以在ParallelOld诞生之后,“吞吐量优先”的收集器终于有了更名副其实的应用组合。在注重吞吐量和对CPU资源敏感的场合,可以优先考虑ParallelScavenge和ParallelOld收集器。ParallelOld收集器的工作流程与ParallelScavenge的工作流程相同。下面是ParallelScavenge/ParallelOld收集器的使用流程图:CMS收集器CMS(ConcurrentMarkSweep)收集器是一种旨在获得最短恢复停顿时间的收集器。非常适合那些集中在互联网站点或B/S系统的服务器端的Java应用。这些应用程序非常重视服务的响应速度。从名字(“MarkSweep”)可以看出它是基于“mark-clear”算法的。CMS收集器整个工作过程分为以下4个步骤:初始标记(CMSinitialmark):只标记GCRoots可以直接关联到的对象,速度非常快,需要“StopTheWorld”.并发标记(CMSconcurrentmark):整个过程中GCRootsTracing的过程耗时最长。重标记(CMSremark):为了更正并发标记期间由于用户程序继续运行而改变的部分对象的标记记录,该阶段的停顿时间一般稍长比初始标记阶段短,但远比并发标记阶段短。这个阶段也需要“StopTheWorld”。并发清除(CMSconcurrentsweep)由于整个进程中耗时最长的并发标记和并发清除进程收集器线程可以和用户线程一起工作,所以,一般来说,CMS收集器的内存回收过程和用户??线程一起工作同时执行。从下图可以清楚的看出CMS收集器运行步骤中的并发和停顿时间:优点CMS是一款优秀的收集器,它的主要优点已经在名字上体现出来了:并发收集,低停顿,所以CMS收集器也称为并发低暂停收集器。缺点对CPU资源非常敏感事实上,为并发设计的程序对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但会因为占用一部分线程(或CPU资源)而拖慢应用程序,降低总吞吐量。CMS默认启动的回收线程数为(CPU数+3)/4,即当CPU数大于4时,并发回收时垃圾回收线程占用CPU资源不少于25%,且垃圾收集线程的数量随着CPU数量的增加而增加。衰退。但是当CPU少于4个时(比如2个),CMS对用户程序的影响可能会变得非常大。如果CPU负载比较大,则必须分配一半的计算能力来执行收集器线程。结果,用户程序的执行速度一下子降低了50%,这其实是不能接受的。无法处理浮动垃圾(FloatingGarbage)可能存在“ConcurrentModeFailure”故障导致另一次FullGC。由于在CMS的并发清理阶段用户线程还在运行,所以随着程序的运行自然会不断产生新的垃圾。这部分垃圾出现在marking过程之后,CMS无法在本次collection中处理掉,只能在下一次GC中清理掉。这部分垃圾被称为“漂浮垃圾”。也是因为在垃圾回收阶段用户线程还需要运行,所以需要预留足够的内存空间给用户线程使用,所以CMS收集器不能像其他收集器一样等到老年代几乎完全被填满在继续之前。对于收集,并发收集时需要预留一部分空间供程序运行。mark-sweep算法引起的空间碎片CMS是基于“mark-sweep”算法的收集器,这意味着在收集结束时会产生大量的空间碎片。当空间碎片过多时,会给大对象的分配带来很大的麻烦。往往老年代还有剩余空间,却找不到足够大的连续空间来分配当前对象。G1收集器G1(Garbage-First)收集器是当今收集器技术发展中最前沿的成就之一。它是服务器端应用程序的垃圾收集器。HotSpot开发团队赋予它的使命是(在一个相对长期内)能够在未来替代JDK1.5发布的CMS收集器。G1与其他GC收集器相比,具有以下特点:并行并发G1可以充分利用多CPU和多核环境下的硬件优势,使用多个CPU来缩短“StopTheWorld”暂停时间,这其他一些收集器原本需要暂停Java线程执行的GC动作,G1收集器仍然可以让Java程序以并发的方式继续执行。分代收集与其他收集器一样,G1中仍然保留了分代概念。虽然G1可以独立管理整个GC堆,不需要其他收集器的配合,但是它可以通过不同的方式来处理新创建的对象和存活了一段时间并存活了多次GC的旧对象,以获得更好的收集效果。空间整合G1是一个整体基于“mark-sort”算法实现的收集器,从局部(两个Region之间)的角度是基于“copy”算法实现的。这意味着G1在运行过程中不会产生内存空间碎片,回收后可以提供规律的可用内存。这个特性有利于程序的长时间运行。在分配大对象时,不会因为找不到连续的内存空间而提前触发下一次GC。可预测的暂停这是G1相对于CMS的主要优势。减少暂停时间是G1和CMS共同关心的问题。不过,除了减少停顿之外,G1还可以建立一个可预测的停顿时间模型,让用户可以明确指定何时在长度为M毫秒的时间片内,这几乎是实时Java(RTSJ)垃圾收集器的一个特性,在GC上花费的时间不应超过N毫秒。G1之前的其他收集器在整个堆上的收集范围是整个新生代或者老年代,但是G1已经不是这样了。在使用G1时,Java堆的内存布局与其他收集器有很大不同。它将整个Java堆划分为多个大小相等的独立区域(Regions)。虽然仍然保留了新生代和老年代的概念,但是新生代和老年代不再是物理上孤立的,而是Region的一部分的集合(不需要连续)。为可预测时间建模G1收集器为可预测的暂停时间建模,因为它可以编程方式避免在整个Java堆中进行区域范围的垃圾收集。G1跟踪每个Region的垃圾堆积值(回收获得的空间大小和回收所需时间的经验值),并在后台维护一个优先级列表。每次根据允许的回收时间,优先回收价值最高的Region(Garbage-First的名字由此而来)。这种使用Region划分内存空间和优先回收region的方法保证了G1收集器在有限的时间内获得尽可能高的收集效率。避免全堆扫描——记得SetG1将Java堆划分为多个Region,也就是“分割成部分”。但是,Region不能是孤立的。一个Region中分配的对象可以和整个Java堆中的任意一个对象有引用关系。在做可达性分析判断对象是否存活时,需要扫描整个Java堆来保证准确性,这显然对GC效率有很大的伤害。为了避免全堆扫描的发生,虚拟机在G1中为每个Region维护了一个对应的RememberedSet。当虚拟机发现程序正在写入Reference类型的数据时,会生成一个WriteBarrier暂时中断写入操作,并检查Reference引用的对象是否在不同的Region中(在生成的例子中,它就是检查老年代中的对象是否引用了新生代中的对象),如果是,则通过CardTable在被引用对象所属Region的RememberedSet中记录相关的引用信息。在进行内存回收时,将RememberedSet加入到GC根节点的枚举范围内,可以保证不会扫描到整个堆,不会有遗漏。如果不算维护RememberedSet的操作,G1收集器的操作大致可以分为以下几个步骤:嵌套TopMarkStart)值,以便用户程序在下一阶段并发运行时,可以在正确的Region中创建对象。这个阶段需要暂停线程,但是时间很短。ConcurrentMarking从GCRoot开始,分析堆中对象的可达性,寻找存活对象。这个阶段耗时较长,但可以与用户程序并发执行。最终标记(FinalMarking)虚拟机为了纠正标记记录中由于用户程序在并发标记期间继续运行而导致标记发生变化的部分,虚拟机将这段时间的对象变化记录在Remembered中设置线程的日志。在最后的标记阶段,将RememberedSetLogs的数据合并到RememberedSet中。这个阶段需要暂停线程,但可以并行执行。筛选恢复(LiveDataCountingandEvacuation)首先对每个Region中的恢复值和代价进行排序,根据用户预期的GC停顿时间制定恢复计划。其实这个阶段也可以和用户程序并发执行,但是因为只是回收了一部分Region,所以时间是用户可控的,暂停用户线程会大大提高回收效率。从下图中可以清楚的看到G1收集器运行步骤中的并发阶段和暂停阶段(在Safepoint):