垃圾回收策略和垃圾收集器

前言

《深入理解Java虚拟机》一书中这样对比Java与C++的开发人员:

Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人想出来。

吐槽一下:描述的很真实,但是你不要连续好几章都是这句话开头啊。。。

垃圾收集算法

各算法的示意图网上有很多,介于我的小学绘画水平就不自己画了,下面示意图均引用纯洁的微笑大佬博客的图片。

分代收集算法(Generational Collection)

分代收集算法主要是一种思想:根据对象存活周期的不同将内存划分为几块,分别使用适合自己的垃圾收集算法进行内存回收。一般将Java堆内存分为新生代和老年代,分别存放存活时间较短和较长的对象。
注:HotSpot虚拟机(目前使用最广的虚拟机)中将方法区称为永久代,意指方法区中存储的数据(已加载的类信息、常量、静态变量等)一般情况下很少会被回收,因为回收的条件很复杂,下面的垃圾回收算法指的都是堆内存的回收。

标记 - 清除算法(Mark-Sweep)

标记清除算法是最基础的垃圾收集算法,它把整个回收阶段分为“标记”和“清除”两个阶段:首先标记所有需要被回收的对象,然后统一将已被标记的对象回收。
垃圾回收策略和垃圾收集器
很容易理解,缺点也很明显:

  1. 标记和清除的效率太低
  2. 标记清除之后的内存中有大量不连续的内存碎片,如果此时有一个较大的对象需要分配内存,很有可能会无法找到足够的连续内存。

复制算法(Copying)

初始的复制算法将内存按容量分为大小相等的两块,每次使用只使用其中的一块,当这一块的内存用完了,就将还存活的对象复制到另一块内存,然后将这块空间全部清理掉。这样就保证了每次回收只会回收整个内存的一半,自然也就不存在没有足够的连续内存分配给对象的情况了。
垃圾回收策略和垃圾收集器
既然说它是初始的复制算法,那么肯定具有一定缺陷,那就是每次只能使用总内存的一半,另一半虽然存在但却不能使用,浪费了一半的内存资源。

HotSpot将初始的复制算法进行了改进,并应用在新生代中:将内存分为三块,一块较大的称为Eden空间,两块较小的成为Survivor空间,它们的大小比例为8:1:1。每次使用的时候,使用Eden区和其中一块Survivor区,回收的时候,将Eden区和Survivor区中还存活的对象复制到另一块Survivor区中,然后将这两块空间清理掉,这样每次使用的内存空间就能达到总空间的90%。

自己画了一个小学水平的示意图:
垃圾回收策略和垃圾收集器

之所以这样分隔内存空间,是因为新生代中的对象98%(IBM研究出来的)都是“朝生夕死”的,也就是说每次回收基本上会将所有对象回收掉,只有少部分对象能够存活,而一块Survivor区完全放得下这些存活的对象。

当然,IBM再厉害也不能保证每次回收剩余的对象都不超过10%,当存活对象超过10%的时候,Survivor区的空间就不够存放,这时就出现了分配担保机制(Handle Promotion)。老年代的内存区对Survivor区进行担保,如果Survivor区的空间不够存放上一次新生代回收剩余的存活对象,这些对象就会通过分配担保直接进入老年代。
注:如果一个新创建的对象需要占用特别大的内存的时候,它会直接进入老年代,所以不用担心一个对象就把Survivor区占满的情况。

标记 - 整理算法(Mark-Compact)

之前也说了,复制算法是适用于新生代的,因为对象存活时间较短,一次回收会回收大部分对象。那么复制算法对于对象存活时间较长的老年代,就不再适用了,取而代之的是“标记-整理”算法,也叫标记压缩算法。
标记整理算法与标记清除算法不同的地方在于:清除的时候不是直接对可回收对象进行清理,而是先让所有存活的对象向一端移动,然后直接清理掉边界外的内存。
垃圾回收策略和垃圾收集器

垃圾收集器

如果说垃圾算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

Serial收集器

最基本、最古老的收集器,单线程收集器,简单高效,它的串行在于进行垃圾收集时,必须暂停其他所有工作线程,直到收集结束。也就是说,程序在你不知道的某一时候,把你正常工作的线程停掉,进行垃圾收集。一个很形象的描述:Stop The World。
Serial有对应的老年代版本Serial Old,均为串行收集器。

ParNew收集器

Serial收集器的多线程版本,属于新生代收集器,能在GC时有效的利用系统资源,但如果是单CPU环境,其效果并不如Serial收集器,因为存在线程交互的开销。

Parallel Scavenge收集器

类似于ParNew收集器,也是新生代收集器,但它更关注于系统的吞吐量。高吞吐量意味着高效率地利用CPU时间,更快完成程序任务,适用于后台计算而交互较少的情况。
另外,Parallel收集器有自适应调节策略,能根据当前系统的运行情况收集性能监控信息,动态调整新生代大小、Eden与Survivor区的比例、晋升老年代的对象大小等参数以提供最合适的停顿时间或者最大的吞吐量。
开启自适应调节参数:-XX:+UseAdaptiveSizePolicy

Parallel Old收集器

Parallel收集器的老年代版本,老年代收集器,出现于JDK1.6以后,使用多线程和“标记 - 整理”算法。

CMS收集器(Concurrent Mark Sweep)

CMS收集器以获取最短回收停顿时间为目标,基于“标记 - 清除”算法,老年代收集器。

主要分为四步:初始标记、并发标记、重新标记、并发清除。其中,初始标记会标记GC Roots能直接关联到的对象(仍存活的对象),并发标记会找到需要回收的对象,重新标记是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的对象的标记记录,并发清除则将对象回收。

其中,耗时较长的为并发标记和并发清除过程,而这两个过程都可以与用户线程并行工作,因此整体上看,CMS收集器的内存回收过程是与用户线程一起并发执行的(初始标记和重新标记仍需要“Stop The World”)。

优点:并发收集、低停顿。
缺点:

  1. 由于是并行程序,对于CPU资源非常敏感。
  2. 由于是并行程序,对于并发清理过程中用户线程继续运行产生的浮动垃圾(Floating Garbage)无法处理。
  3. 基于“标记 - 清除”算法实现,会产生空间碎片。

G1收集器(Garbage First)

G1收集器是面向服务端应用的垃圾收集器。

优点:

  1. 充分利用多CPU、多核环境,使用多个CPU缩短Stop The World停顿的时间。
  2. 基于“标记 - 整理”算法,不会产生空间碎片
  3. 能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超过N毫秒。

G1收集器不局限于新生代或老年代,它将整个Java堆内存分为多个大小相等的独立区域(Region),根据各个Region里的垃圾的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,由此保证有限时间内尽可能高的回收效率。

G1收集器是一款非常优秀的垃圾收集器,但更适合堆内存大的应用,只有内存很大,JDK版本较高的情况下,使用起来才有明显效果。如果是普通的本地程序,使用G1收集器并不合适,因为会浪费很大内存。

注:查看当前垃圾收集器命令:java -XX:+PrintCommandLineFlags -version