【Java】垃圾回收 - 分代垃圾回收

Posted by 西维蜀黍 on 2019-03-26, Last Modified on 2021-09-21

为什么要分代?

分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的

因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。

在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。

试想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长。同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是他们依旧存在。

因此,分代垃圾回收采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。

如何分代?

如图所示,虚拟机中的共划分为三个代:新生代(Young Generation)老年代(Tenured Generation)持久代(Permanent Generation)。其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。新生代和老年代的划分是对垃圾收集影响比较大的。

新生代(Young Generation)

所有新生成的对象首先都是放在新生代的。新生代的目标就是尽可能快速的收集掉那些生命周期短的对象。

新生代分三个区。一个Eden区,两个Survivor区(一般而言)。大部分对象在Eden区中生成。

需要注意,Survivor的两个区是对称的,没先后关系。所以同一个区中,可能同时存在从Eden复制过来对象和从前一个Survivor复制过来的对象。而复制到年老区的只有从第一个Survivor区过来的对象。而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在新生代中的存在时间,减少被放到老年代的可能。


长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,当对象经过了1次Minor GC后,对象就会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。

老年代(Tenured Generation)

在新生代中经历了N次垃圾回收后仍然存活的对象,就会被放到老年代中。因此,可以认为老年代中存放的都是一些生命周期较长的对象。

大对象通常也直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝。

持久代(Permanent Generation)

持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=<N>进行设置。

注意,在 JDK 1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制

什么情况下触发垃圾回收

由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Young GC(Minor GC)Full GC(Major GC)


针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:

  • Partial GC:并不收集整个GC堆的模式
    • Young GC(Minor GC):只收集新生代的GC
    • Old GC:只收集老年代的GC。只有CMS的concurrent collection是这个模式
    • Mixed GC:收集整个新生代以及部分老年代的GC。只有G1有这个模式
  • Full GC:收集整个堆,包括新生代、老年代、持久代(如果存在的话)等所有部分的模式。

Young GC(Minor GC)

一般情况下,当新对象生成,并且在Eden申请空间失败时(或者说,新生代的Eden区满),就会触发Young GC(Minor GC),对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。

然后整理Survivor的两个区。这种方式的GC是对新生代的Eden区进行,不会影响到老年代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。

因而,一般在这里需要使用速度快、效率高的算法,使Eden区能尽快空闲出来。

注意young GC中有部分存活对象会晋升到老年代,所以young GC后,老年代的占用量通常会有所升高。

新生代通常存活时间较短通常基于Copying算法进行回收,所谓Copying算法就是扫描出存活的对象,并复制到一块新的完全未使用的空间中,对应于新生代,就是在Eden和FromSpace或ToSpace之间copy。新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从Eden到Survivor,最后到老年代。

Old GC

老年代与新生代不同,老年代对象存活的时间比较长、比较稳定,因此通常采用标记(Mark)算法来进行回收,所谓标记就是扫描出存活的对象,然后再进行回收未被标记的对象,回收后对用空出的空间要么进行合并、要么标记出来便于下次进行分配,总之目的就是要减少内存碎片带来的效率损耗。

Full GC(Major GC)

Full GC 就是收集整个堆,包括新生代,老年代,永久代等收集所有部分的模式。

针对不同的垃圾收集器,Full GC的触发条件可能不都一样。按HotSpot VM的serial GC的实现来看,触发条件是:

  • 当准备要触发一次 young GC 时,如果发现统计数据显示:之前 young GC的平均晋升大小比目前的 old gen剩余的空间大,则不会触发young GC而是转为触发 full GC (因为HotSpot VM的GC里,除了垃圾回收器 CMS的concurrent collection 之外,其他能收集old gen的GC都会同时收集整个GC堆,包括young gen,所以不需要事先准备一次单独的young GC)
  • 如果有永久代(perm gen),要在永久代分配空间但已经没有足够空间时,也要触发一次 full GC
  • System.gc(),heap dump带GC,默认都会触发 full GC

HotSpot VM里其他非并发GC的触发条件复杂一些,不过大致原理与上面说的其实一样。

Reference