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

概述

垃圾收集(GC)并非Java所独创,1960年诞生于MIT的Lisp是第一个开始使用内存动态分配和垃圾收集技术的语言。其作者思考过垃圾收集需要完成的三件事情:

  • 哪些内存需要回收
  • 什么时候回收
  • 如何回收

经过半个世纪的发展,现在内存动态分配和垃圾收集技术已经相当成熟,为什么还要去了解垃圾收集和内存分配?答案很简单:当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,就必须对这些”自动化“技术实施必要的监控和调节。

Java内存运行时区域分为

  • 程序计数器

  • Java虚拟机栈

  • 本地方法栈

  • Java堆

  • 方法区

  • 运行时常量池

  • 直接内存

其中程序计数器、虚拟机栈和本地方法栈随着线程而生,随线程而灭,每一个栈帧中分配多少内存基本上是在类结构上就已知的,因此这几个区域的内存分配和回收都有确定性。

而Java堆和方法区有何很高的不确定性:一个接口的实现类需要的内存可能不一样,一个方法所执行的不同条件分支需要的内存也可能不一样,只有运行时才知道会创建哪些对象,创建多少对象,这部分的分配和回收是动态的。垃圾收集器所关注的就是这部分内存如何管理。

垃圾收集算法

堆里面存放着几乎所有对象实例,垃圾收集器在对堆进行回收前,第一件事情就是确定哪些对象”活着“,哪些已经”死去“。从这个角度出发,垃圾收集算法可以划分为引用计数式垃圾收集追踪式垃圾收集两大类,这两类也被称为直接垃圾收集间接垃圾收集,Java中所有垃圾回收算法均属于追踪式垃圾收集。

分代收集理论

当前商业虚拟机的垃圾收集器,大多数都遵循了分代收集的理论进行设计,一般把Java堆划分为新生代老年代两个区域。在新生代,每次垃圾收集都有大批对象死去,每次回收后存活的少量对象,将会逐步晋升到老年代存放。

标记-清除算法

最早出现也是最基础的垃圾收集算法时标记-清除算法。算法分为”标记“和”清除“两个阶段:首先标记处所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来使用。

之所以说是最基础的收集算法,因为后续的收集算法大多都是以标记-清除算法为基础,对其缺点进行改造而得到的。

它的主要缺点有两个:

  • 执行效率不稳定
  • 内存空间碎片化

标记-复制算法

为了解决标记-清除算法面对大量可回收对象执行效率地的问题,提出了称为”半区复制“的垃圾收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。

当这一块的内存用完了,就将存活的对象复制到零另外一块上面,再把使用过的空间一次清理掉。如果内存中多数对象都是存活的,这种算法会产生大量的内存复制开销,但是对于多数对象都是可回收的情况,仅需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,也不用考虑空间碎片的复杂情况,只需要移动堆顶指针,按顺序分配即可。

这样实现简单高效,不过代价是将可用内存缩小为了原来的一半,空间浪费太多。

标记-整理算法

标记-复制算法在对象存活率较高时需要进行较多的复制操作,效率会变低,更关键的是需要浪费一般的空间,所以老年代一般不能直接选用这种算法。

针对老年代对象的存亡特征,提出了标记-整理算法,其标记过程和标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。

经典垃圾收集器

Serial收集器

Serial收集器是最基础、历史最悠久的收集器,在JDK1.3.1之前是HotSpot虚拟机新生代收集器的唯一选择。顾名思义,这是一个单线程收集器,它的”单线程“不仅仅说明它只会使用一个处理器或者一条收集线程完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束,”Stop The World“。

事实上,迄今为止,Serial收集器依然是HotSpot运行在客户端模式下的默认新生代收集器,有着优于其他收集器的地方就是它简单而高效,对于内存资源受限的环境,它是所有收集器里额外消耗最小的;对于单核处理器或者处理器核心较少的环境来说,Serial收集器没有线程交互的开销,可以获得最高的单线程收集效率。对于一般几十兆到一两百兆的新生代来说,垃圾收集的停顿时间完全可以控制在十几、几十毫秒,最多一百毫秒以内,只要不是频繁发生收集,这点停顿时间对于许多用户来说完全可以接受。

ParNew收集器

ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集以外,其余的行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则,回收策略等都与Serial收集器完全一致,在实现上两种收集器也共用了相当多的代码。

Parallel Scavenge收集器

Parallel Scavenge收集器是一款新生代收集器,同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器,基本上和ParNew非常相似。

Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。也就是处理器用于运行用户代码的时间与处理器总消耗时间的比值

Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。这个收集器的主要意义是供客户端模式下的HotSpot使用。

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并行收集,基于标记-整理算法实现。这个收集器直到JDK6才开始提供,在此之前,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old收集器以外别无选择,其他表现良好的老年代收集器如CMS无法与他配合工作。

直到Parallel Old收集器出现后,吞吐量优先收集器终于有了比较名副其实的搭配组合,在注重吞吐量或者处理强资源较为稀缺的场合,都可以考虑Parallel Scavenge加Parallel Old收集器这个组合。

CMS收集器

CMS(Concurrent Mark Sweep) 收集器是一种获取最短回收停顿时间为目标的收集器,目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务器上,这种应用通常较为关注服务的相应速度,系统系统缩短停顿时间,以给用户带来良好的交互体验。CMS收集器就非常符合这类应用的需求。

CMS收集器是基于标记-清除算法实现的,它的运行过程相对于前面几种收集器要更复杂一些,整个过程分为四步:

  • 初始标记(CMS initial mark)
  • 并发标记(CMS concurrent mark)
  • 重新标记(CMS remark)
  • 并发清除(CMS concurrent sweep)

其中初始标记、重新标记这两个步骤仍然需要”Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快;并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程消耗时间长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常比初始标记长一些,但也远远小于并发标记阶段是时间;最后是并发求清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个线程也可以与用户线程同时并发。

CMS是一款优秀的收集器,它最主要的优点在名字上已经体现出来:并发收集,低停顿。CMS是HotSpot追求第停顿的第一次成功尝试,但是它还远远达不到完美的程度,至少有以下三个明显的缺点:

  • 处理器资源非常敏感
  • 无法处理浮动垃圾(Floating Garbage)
  • 基于标记-清除算法导致会有大量空间碎片产生

Garbage First收集器

G1收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。从JDK8 Update40之后,G1才被Oracle官方称为”全功能的垃圾收集器“(Fully-Featured Garbage Collector)。

G1是一款主要面向服务端应用的垃圾收集器。HotSpot团队最初赋予它的期望是未来可以替换掉JDK5中的CMD收集器。

G1收集器出现之前的所有其他收集器,垃圾收集的目标范围都是基于分代思想进行回收,而G1可以面向堆中任何部分来组成回收集进行回收,衡量标准不是它属于哪个分代,而是那块内存中皴法垃圾中最多,回收收益最大,这就是G1的mixed gc模式。