JVM垃圾回收几乎成了Java程序员面试标配了,我自己在被面和面试别人的过程中都曾反复面试这些内容。今天就把之前了解到的垃圾回收知识整理一下。

前几天被面试的时候,就问到JVM垃圾回收,一般面试这个知识的基本就是P6,P7级别了(对标阿里),虽然在实际工作中用到的时候极少,但能当作过滤候选人的初步门槛。

0. 基础概念

对象头(Header)

对象头主要包含两部分信息:

  • 对象的大小
  • 对象的种类。

在Hotspot中对象头也分为两部分内容:

  • 第一部分是存储对象自身的运行时数据

    如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等信息;

  • 第二部分是类型指针

    在Hotspot中这部分是类型指针,即是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

堆(Heap)

堆是JVM运行时数据区,所有实例和数组的内存均由此分配,堆是在JVM启动时创建的。堆也是GC主要的操作区域,一般我们所说的垃圾回收都是在说堆上的垃圾回收。

分块(Chunk)

分块是指为利用对象实现准备出来的空间。初始状态下Heap是被一个大的chunk占据,在新建对象时会根据要求把chunk划分为合适的大小使用。活动对象不久后会被转化为垃圾回收,被回收的空间将会再次被分块,为下次被利用做准备。因此内存中各个chunk重复着分块->活动对象->垃圾->分块->…… 这种过程。

根(Root)

根是指向对象的对象的起点部分,是GC使用引用计数法等算法做对象可达性分析时的起始点。在JVM中是GC Roots,主要包含以下几种:

  • 虚拟机栈(帧栈中的本地变量表)中引用的对象;
  • 方法区中类静态属性引用的对象;
  • 方法区中常量引用的对象;
  • 本地方法栈JNI(Native方法)引用的对象。

停顿时间(Stop-The-World)

不管选择哪种GC算法,stop-the-world都是不可避免的。Stop-the-world意味着从应用中停下来并进入到GC执行过程中去。一旦Stop-the-world发生,除了GC所需的线程外,其他线程都将停止工作,中断了的线程直到GC任务结束才继续它们的任务。GC调优通常就是为了改善stop-the-world的时间。

1. 垃圾回算法

1.1 标记-清除算法

分为标记和清除两个阶段。

  • 标记

    是把所有活动对象都做上标记。

  • 清除

    是把那些没有标记的对象也就是非活动对象清理回收。

优点

  • 算法简单
  • 与保守式GC(保守式 GC(Conservative GC)指的是“不能识别指针和非指针的 GC”)算法兼容

缺点

  • 碎片化

    在标记-清除算法使用过程中会逐步产生更细小的分块(chunk),不久就会产生无数小分块散步在堆的各处。即使堆中分块的总大小够用,也会因为一个个的分块都太小而不能执行内存分配。

  • 分配速度

    由于堆中的分块是不连续的,每次在做内存分配时都需要遍历堆中的空闲链表寻找合适大小的分块,最坏的情况就是遍历整个堆。

1.2 复制算法(Copying)

复制算法就是将一个空间(From空间)内的活动对象复制到另一个空间(To空间)中,然后将原来的空间回收清理掉。现在的商业JVM都使用复制算法来回收新生代(Yong)。

优点

  • 优秀的吞吐量

    复制算法的时间消耗是搜索活动对象(标记阶段)所花费的时间和搜索整体堆(清除阶段)所花费的时间之和,其消耗时间是和活动对象成比例的,当堆越大时其吞吐量也就越优秀。

  • 可实现高速分配

    复制算法的分块是一块连续的区域,不用像标记-清除算法那样遍历空闲链表寻找合适大小的块(chunk)。

  • 不会产生碎片化

  • 与缓存兼容

缺点

  • 堆使用效率低下

    复制算法需要将堆分为两部分,只能使用其中一部分,使用率低下。现在商业JVM在使用复制算法时,并不是均分两部分。IBM公司专门研究表明,新生代中98%对象是“朝生夕死”,是将内存分为一块较大的Eden区和两个较小的Survivor区。

  • 不兼容保守式GC算法

    GC 复制算法必须移动对象重写指针,所以有着跟保守式 GC 算法不相容的 性质 。

  • 递归调用函数

    复制某个对象时要递归复制它的子对象,因此在每次复制时都需要调用递归函数。

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

标记-整理算法是将标记-清理算法和复制算法相结合的产物,其标记阶段与标记-清理算法的标记是一样的,在标记完成之后让所有存活的对象向一端移动,之后直接清理掉边界之外的内存。

优点

  • 可有效利用堆

    相比与复制算法,其堆利用率高很多,且不会产生内存碎片。

缺点

  • 整理阶段花费计算成本

1.4 分代回收算法

在对象中引入年龄的概念,优先回收容易成为垃圾的对象,提高垃圾回收效率。

堆结构

绝大多数的java 对象都是初始化在eden区,两个Survivor区在任何时刻都会有一个处于空闲状态,
分代回收严格来说也算不上是算法,是对堆内存的一个分类处理,在不同的分代上使用不同的垃圾回收算法。

2. 垃圾回收器

垃圾回收器是垃圾回收算法的具体实现。

垃圾回收器运行图

  • HotSpot VM里多个GC有部分共享的代码。有一个分代式GC框架,Serial/Serial Old/ParNew/CMS都在这个框架内;在该框架内的young collector和old collector可以任意搭配使用,所谓的“mix-and-match”。
  • ParallelScavenge与G1则不在这个框架内,而是各自采用了自己特别的框架。

2.1 Serial收集器

  • 使用单线程进行垃圾回收

    垃圾回收过程中只有单个GC线程处理。

  • 独占式的垃圾回收

    Serial 垃圾收集器会暂停所有其他系统线程进行垃圾处理。

Serial可应用与Client环境。

2.2 并行收集器

  • ParNew收集器

    ParNew 是Serial的多线程版本,使用多条线程进行Server版本的垃圾回收,除此之外,其他功能与Serial基本一致。
    ParNew也是唯一可以与CMS收集器配合的新生代垃圾回收器。

  • Parallel Scavenge收集器

    新生代垃圾收集器,使用复制算法,多线程进行垃圾回收,是一个吞吐量优先的回收器。
    Parallel Scavenge和ParNew抽象来说是同一思路的GC,而后者可以跟CMS搭配使用。Parallel Scavenge没有使用原本HotSpot其它GC通用的那个GC框架,所以不能跟使用了那个框架的CMS搭配使用。

  • Parallel Old收集器

    Parallel Old是Parallel Scavenge的老年代版本,使用多线程的标记-整理算法。有了Parallel Old之后有可与Parallel Scavenge收集器配合的老年代收集器,使得整个垃圾收集过程变为并行。

2.3 CMS(Concurrent Mark Sweep)回收器

CMS使用标记-清除算法,是一种以获取最短回收停顿时间为目标的回收器。

其工作步骤为:

  • 初始标记

    初始标记也就是标记一下 GC roots 关联到的对象。为了收集应用程序的对象引用需要暂停应用程序线程,该阶段完成后,应用程序线程再次启动。

  • 并发标记

    从第一阶段收集到的对象引用开始,遍历所有其他的对象引用。

  • 重新标记

    重新标记是为了修正并发标记期间因系统程序继续运作而产生变动的那部分对象的标记记录。这个阶段的停顿时间一般比初始标记阶段长,但远比并发标记阶段时间短。

  • 并发清除

    采用标记-清除算法并发清理所标记的垃圾。

  • 并发重置

    并发重置是指在垃圾回收完成后,重新初始化 CMS 数据结构和数据,为下一次垃圾回收做好准备。

优点
并发收集,低停顿

缺点

  • 对CPU资源特别敏感
  • 无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。
  • 产生内存碎片。

2.4 G1收集器

在G1收集器中,继续保持了分代的概念,但不同于其他的分代回收算法,G1将堆空间分成一组大小相等的区域,每块区域对应虚拟机的一篇连续内存,每块区域既有可能属于O区、也有可能是Y区,且每类区域(Region)空间可以是不连续的。虽然在清理这些区块时G1仍然需要暂停应用线程、但可以用相对较少的时间优先回收包含垃圾最多区块。

G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内获可以获取尽可能高的收集效率。

工作步骤:

  • 初始标记(Initial Marking)

    仅标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。

  • 并发标记(Concurrent Marking)

    从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。

  • 最终标记(Final Marking)

    为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。

  • 筛选回收(Live Data Counting and Evacuation)

    首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,从Sun透露出来的信息来看,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。

G1 适用场景:

  • 服务端多核CPU、JVM内存占用较大的应用(至少大于4G);
  • 应用在运行过程中会产生大量内存碎片、需要经常压缩空间;
  • 想要更可控、可预期的GC停顿周期;防止高并发下应用雪崩现象。

G1相对于CMS的区别在:

  • G1在压缩空间方面有优势;
  • G1通过将内存空间分成区域(Region)的方式避免内存碎片问题;
  • Eden, Survivor, Old区不再固定、在内存使用效率上来说更灵活;
  • G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象;
  • G1在回收内存后会马上同时做合并空闲内存的工作、而CMS默认是在STW(stop the world)的时候做;
  • G1会在Young GC中使用、而CMS只能在O区使用;

References

垃圾回收的算法与实现
深入理解Java虚拟机(第2版)
JVM 垃圾回收器工作原理及使用实例介绍
Java Garbage Collection Basics
Java SE 6 HotSpot Virtual Machine Garbage Collection Tuning
Types of Java Garbage Collectors
深入理解G1垃圾收集器