深入理解Java虚拟机(四)-垃圾收集算法

时间:2022-07-02 10:03:40

概述

当前的商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批的对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。

深入理解Java虚拟机(四)-垃圾收集算法

新生代(Young Generation)

新生代是所有新对象产生的地方。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。年轻代分为3个部分:Enden区和Survivor From和Survivor To区。
年轻代的主要特点:

  • 大多数新建的对象都位于Eden区。
  • 当Eden区被对象填满时,就会执行Minor GC。并把所有存活下来的对象转移到其中一个Survivor 区。
  • Minor GC同样会检查存活下来的对象,并把它们转移到另一个Survivor 区。这样在一段时间内,总会有一个空的Survivor 区。
  • “熬过”多次Minor GC后,仍然存活下来的对象会被转移到年老代内存空间。如果对象在Eden出生并经过第一次Minor GC后任然存活,并且能被Survivor 容纳的话,它将被移动到Survivor 空间中,并且对象年龄设为1。对象在Survivor 区中每熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。

新生代大小可以通过参数 -Xmn10M来控制;Eden区和Survivor区的大小可以通过参数 -XX:SurvivorRatio来进行控制,默认为8:1。

年老代(Old Generation)

年老代包含了长期存活的对象和经过多次Minor GC后依然存活下来的对象。通常会在老年代内存被占满时进行垃圾回收。老年代的垃圾收集叫做Major GC,Major GC会花费更多的时间。

Minor GC与Major GC的区别

  • 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多具备朝生夕灭的特性,所以Minor
    GC非常频繁,一般回收速度也比较快。
  • 老年代GC(Major GC/Full GC):指发生在老年代的垃圾收集工作,出现了Major GC,经常会伴随至少一次的Minor
    GC(但并非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程),Major
    GC的速度一般会比Minor GC慢10倍以上。

垃圾收集算法

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

原理:
标记-清除算法(Mark-Sweep)分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
缺点:
1.效率问题,标记和清除两个过程的效率都不高;
2.空间问题,标记清除后会产生大量的不连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法

原理:
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已经使用过的内存一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
缺点:
需要复制,效率降低、浪费空间。

现在的商业虚拟机都采用这种收集算法来回收新生代。IBM公司的专门研究表明,新生代中对象98%都是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。

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

原理:
标记过程任然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法(Generational Collection)

分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。

垃圾收集器

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。Java虚拟机规范对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商,不同版本的虚拟机所提供的垃圾收集器都可能会有很大差别,并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。这里讨论的收集器基于JDK1.7 Update 14之后的HotSpot虚拟机(在这个版本中正式提供了商用的G1收集器,之前G1仍处于试验状态),这个虚拟机包含的所有收集器如下图所示:

深入理解Java虚拟机(四)-垃圾收集算法

上图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。收集器所处的区域,则表示它是属于新生代收集器还是老年代收集器。

在介绍这些收集器各种的特性之前,我们先来明确一个观点:虽然我们是在对各个收集器进行比较,但并非为了挑选出一个最好的收集器。因为直到目前为止还没有最好的收集器出现,更加没有万能的收集器,所以我们选择的只是对具体应用最合适的收集器。

Serial收集器

它是一个单线程的收集器,但它的“单线程”的意义并不仅说明它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。

ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样。

Parallel Scavenge收集器

Parallel Scavenge收集器是一个新时代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。

Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能的缩短垃圾收集时用户线程暂停的时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughout)。所谓吞吐量就是CPU运行用户代码的时间和总耗时的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。

Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。

G1收集器

G1收集器

内存分配与回收策略

Java技术体系中所提倡的自动内存管理最终可归结为自动化的解决了两个问题:给对象分配内存以及回收分配给对象的内存。
对象的内存分配,往大方向上讲,就是在堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接地栈上分配),对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。