Java虚拟机学习笔记-垃圾收集算法和收集器的总结

写在前面

  总结一下Java虚拟机的垃圾收集算法的思想和各个垃圾收集器的特点。


垃圾收集算法

标记 - 清除算法

  ”标记 - 清除“算法(Mark-Sweep)是最基础的收集算法,因为后续的算法都是基于它进行改进的。该算法一共分为“标记”和“清除”两大阶段:第一步标记出所有需要回收的对象,第二步回收所有的需要回收的对象。“标记“的过程通过引用计数算法或可达性分析算法来标记(Java使用后者),“清除”的过程就是回收所有需要被回收的内存。但是所有可回收的内存不可能是一块连续的内存,所以通过该算法进行垃圾收集的最大的不足就是会产生大量的不连续的空间碎片。

关于”标记 - 清除”算法的不足,整理如下:

  • 效率问题,标记和清除两个过程的效率都不高
  • 空间问题,也就是上面提到的会产生大量的不连续的空间碎片,如果之后要分配一个较大的对象时,无法找到足够的内存就会提前触发另一次垃圾收集动作

复制算法

  因为”标记 - 清除“算法存在效率的问题,所以出现了复制(Copying)算法。它的主要实现思想是将可用的内存分为大小相等的两块,每次只使用一块,当这一块内存用完了,就将这块内存中依旧存活的对象复制到另外一块未被使用的内存中,之后将之前那块已经使用过的内存空间一次清理掉。可是,复制算法的缺陷显而易见,就是每次只能使用一半的内存,显然造成了内存的浪费。

  不过,”复制“算法作为收集新生代内存的主要算法,显然不会去浪费一半的内存,而是有更好方案,但是基本的复制算法的思想是不变的。下面简单介绍一下HotSpot虚拟机(也就是目前Java的虚拟机)对”复制“算法的运用,首先要说明的是,下面只是介绍大概的步骤,具体的内存分配与回收策略会在之后的笔记中进行记录。

  介绍之前,需要明确以下几个概念:

新生代:Java对象出生的地方,Java中所有对象刚被建立后所存在的那块内存区域。

minor gc:对新生代内存的垃圾回收动作,一般使用复制算法收集。

老年代:在对新生代的垃圾收集中,长期都存活的对象将进入老年代的这块内存区域,一些大对象也会直接进入老年代。

major gc:对老年代内存的垃圾回收动作,一般使用标记 - 整理算法(下一个要介绍的算法)收集。

  在现在的虚拟机实现中,将新生代内存分为一块较大的Eden空间,和两块较小的Survivor区域,分别叫做From SurvivorTo Survivor。每次回收的时候,将EdenFrom Survivor中依旧存活的对象复制到另一块To Survivor,然后清理掉EdenFrom Survivor空间。在HotSpot虚拟机中,默认的Eden空间与Survivor空间的比值为8:1,也就是说Eden空间占80%,另外两个Survivor空间占剩下的20%,也可以理解为每次新生代中可用内存空间为整个新生代容量的90%。不过,我们不能保证每次存活的对象不多于10%,所以当空间不够用的时候,需要依赖老年代的内存进行分配担保(Handle Promotion)。


标记 - 整理算法

  ”复制“算法适合在对象存活率低时使用,所以不适合在老年代中使用。根据老年代的特点,出现了”标记 - 整理“算法(Mark-Compact)。

  ”标记 - 整理“算法的标记过程与”标记 - 清除“算法一样,之后的整理的过程是将所有存活的对象向一端移动,然后清空掉边界以外的内存。


分代收集算法

  目前商业虚拟机都采用”分代收集“(Generational Collection)算法。它根据对象存活周期的不同将内存划分为几块。一般是把堆分为新生代和老年代,然后根据各个年代的特点采用最适当的收集算法。新生代每次都会有大量的对象死去,老年代每次只会有少量的对象死去,所以新生代一般采用”复制“算法回收,老年代一般使用”标记 - 清除“算法或者”标记 - 整理“算法回收。


垃圾收集器

  书中说明,讨论的收集器都是基于JDK 1.7 Update 14之后的HotSpot虚拟机。

Serial收集器

  Serial收集器是最基本、发展历史最悠久的收集器,是一个单线程的收集器。它在进行垃圾收集时,必须暂停其它所有的工作线程,直到收集结束。

  收集算法:复制算法

  回收内存空间:新生代

  劣势:在于上面提到的,在工作时需要暂停其它工作线程,会带给用户不良体验

  优势:与其它收集器的单线程相比,Serial收集器简单而高效,因为在单个CPU环境中,它没有线程交互的开销,专心去做垃圾收集

  用途:到目前为止,Serial收集器是虚拟机运行在Client模式下默认的新生代收集器


ParNew收集器

  ParNew收集器是Serial收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余行为都与Serial相同。在实现上,它们也共用了很多代码。

  收集算法:复制算法

  回收内存空间:新生代

  劣势:单CPU环境下不会比Serial收集器有更好的效果

  优势:多CPU环境下,可以有效的利用系统资源。默认开启的收集线程数和CPU的数量相同。除了Serial收集器,只有它能与CMS收集器配合工作

  用途:是很多运行在Server模式下的虚拟机首选的新生代收集器


Parallel Scavenge收集器

  Parallel Scavenge收集器看上去和ParNew一样,是新生代的收集器,也是使用复制算法,也是并行的多线程收集器。不过,该收集器的关注点和其它收集器是不同的,其它收集器的关注点是尽可能的缩短垃圾收集用户的停顿时间,而Parallel Scavenge收集器的目的是达到一个可控制的吞吐量(Throughput)。

吞吐量:CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。

  所以,Parallel Scavenge收集器也被称为“吞吐量优先”收集器。

  下面简单介绍一个概念:

自适应的调节策略(GC Ergonomics):虚拟机根据当前系统的运行情况收集性能监控信息,动态调整新生代的大小、Eden空间和Survivor,空间的比例以及晋升老年代对象大小等细节参数以提供最合适的停顿时间或者最大的吞吐量。

  Parallel Scavenge收集器就采用这种策略,这是与ParNew收集器的一个重要区别。

  收集算法:复制算法

  回收内存空间: 新生代

  劣势:该收集器无法与CMS收集器配合,所以在很长的一段时间都处于尴尬的位置。而且它注重吞吐量,所以在很多需要太多交互的场景中不适合使用

  优势:该收集器关注点是吞吐量优先,所以可以高效率的利用CPU时间,最快的完成运算效率

  用途:适用在需要后台运算而没有太多交互的业务场景


Serial Old收集器

  Serial Old收集器是Serial收集器的老年代版本,也是单线程的收集器。

  收集算法:标记 - 整理算法

  回收内存空间:老年代

  劣势:收集器工作时需要停止其它所有用户进程

  优势:与Serial收集器的优势差不多

  用途:

  1. 虚拟机在Client模式下使用
  2. Server模式下,一是在JDK 1.5之前的版本中与Parallel Scavenge收集器搭配使用,二是作为CMS收集器的后备预案,当CMS工作时,预留内存不足会临时启用Serial Old收集器

PS:关于用途中第二条的第一点,Parallel Scavenge收集器架构中有PS MarkSweep收集器来进行老年代的收集,并不是直接使用Serial Old收集器,知识因为这两收集器的实现比较接近,在官方的很多资料中都是直接用Serial Old代替PS MarkSweep讲解。所以,书中的作者也是这样讲解的,我也就这样记录下来。、


Parallel Old收集器

  Parallel Old收集器是Parallel Scavenge收集器的老年代版本,多线程并且采用标记 - 整理算法。在JDK 1.6才开始提供,该收集器的出现才使Parallel Scavenge收集器从尴尬的位置”解脱“。因为在这之前,如果新生代采用Parallel Scavenge收集器的话,老年代收集器就只能采用Serial Old或者PS MarkSweep收集器,而这两个收集器又是单线程的收集器,所以,在多CPU的环境下,效率可想而知。

  所以说,Parallel Old收集器的出现“解脱”了Parallel Scavenge收集器。这两收集器终于可以组合起来作用于注重吞吐量以及CPU资源敏感的场合。

  收集算法:标记 - 整理算法

  回收内存空间: 老年代

  劣势: 在需要很多交互的场景不适合

  优势:注重吞吐量优先

  用途:可以与Parallel Scavenge收集器组合,应用于一些注重吞吐量与CPU资源敏感的场合


CMS收集器

  CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。该收集器采用的是“标记 - 清除”算法。

  下面先简单介绍一个概念:

Stop The World:GC进行时必须停顿所有Java执行进程  

  接着介绍一下CMS收集器的过程:

  1. 初始标记:需要Stop The World,该步骤只是标记一下GC Roots能直接关联的对象,速度很快
  2. 并发标记:是进行GC Roots Tracing的过程
  3. 重新标记:需要Stop The World,该步骤是为了修正在并发标记期间因用户程序继续运作而导致标记发生变动的那一部分对象的标记记录,时间比初始标记要长,但远远短于并发标记的时间
  4. 并发清除:清除那些无用的对象

  其中,整个过程中耗时最长的并发标记与并发清除阶段可以与用户线程一起工作,做到了很短的回收停顿时间,所以Sun公司的一些文档也叫它为“并发低停顿收集器”(Concurrent Low Pause Collector)。看上去CMS很不错,可是它也有缺点,如下:

  1. CMS对CPU资源非常敏感。在并发阶段,会占用线程而导致应用程序变慢,总吞吐量降低。CMS默认启动的回收线程数是:(CPU数量 + 3)/ 4,我们可以看出,CPU数量在4个以上时,垃圾收集线程会随着CPU数量的增加而下降,可是如果不足4个时,比如2个,CMS对用户程序的影响就可能变的很大
  2. CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC(也就是上文提到的老年代收集)的产生。那么,什么是浮动垃圾?浮动垃圾就是CMS收集器在并发清理阶段用户线程也同时在运行,会产生新的垃圾,而CMS收集器在这次收集中无法处理,只能等待下一次GC时再清理掉
  3. CMS采用“标记 - 清除算法”,而上文我也记录了该算法的特点,就是不产生不连续的空间碎片,当过多时,如果有大对象需要分配内存时,无法找到足够大的内存,就需要提前触发一次Full GC。不过,CMS收集器有参数(-XX:+UseCMSCompactAtFullCollection,默认是开启的)可以控制是否进行内存整理,不过整理的过程是无法并发执行的,所以会使停顿时间变长。还提供了一个参数(XX:+CMSFullGCsBeforeCompaction,默认为0)用来设置执行多少次不压缩,也就是不整理的Full GC后,来一次带压缩的Full GC

  收集算法:标记 - 清除算法

  回收内存空间:老年代

  劣势:同上面的缺点介绍

  优势:并发收集、低停顿

  用途:重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验的业务场景


G1收集器

  G1(Garbage-First)收集器直到JDK 7u4才被Sun公司认为它达到足够成熟的商用程度。它是一款面向服务端应用的垃圾收集器。相比其它收集器,它有如下几个特点:

  1. 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势
  2. 分代收集:分代的概念在G1中依然保留。虽然G1可以不需要其它的收集器配合就能自己管理整个GC堆,但它能采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果
  3. 空间整合:G1从整体来看是基于“标记 - 整理”算法实现,从局部上来看是基于“复制”算法实现。这两种算法都不会产生内存空间碎片,所以G1收集器收集后能提供规整的可用内存
  4. 可预测的停顿:G1相比于CMS的一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型

  使用G1收集器时,Java堆的内存布局与其它收集器有很大区别,虽然还保留有新生代和老年代的概念,但是它们之间不是物理隔离,而它们都是一部分Region(区域)的集合。

  下面简单介绍下G1收集器的步骤:

  1. 初始标记:标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确的Region创建对象,这个阶段需要停顿线程,耗时很短
  2. 并发标记:进行可达性分析,找出存活对象,耗时较长,不过可以与用户线程并发执行
  3. 最终标记:为了修正在并发标记期间因用户程序继续运行而导致标记产生变动的那一部分标记记录,需要停顿线程,但是可以并行执行
  4. 筛选回收:首先对每个Region(区域)的回收价值和成本进行排序,根据用户所期望的GC停顿时间才制定回收计划,并行执行,不过可以做到与用户线程并发执行

  可以看出,G1收集器的运作过程与CMS收集器有很多相似之处。

  收集算法:从整体来看基于“标记 - 整理”算法,从局部来看基于“复制算法”

  回收内存空间:新生代、老年代

  劣势:…

  优势:见上面提到的特点

  用途:面向服务端应用的垃圾收集器


小结

  简单记录了一个Java虚拟机中垃圾回收算法以及垃圾收集器,加深一下对这一块的印象。


参考资料

Author: HowieLi
Link: https://www.howieli.cn/posts/java-gc-algorithm-or-machine.html
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.