• 哪些内存可以回收?
  • 什么时候回收?
  • 如何回收?

    01|对象存活判断算法

    引用计数法

    在对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器数减1;任何时刻计数器为零的对象就是不可能再被使用。

  • 优点:原理简单,判定效率高

  • 缺点:需要额外空间来进行计数,单纯的引用计数很难解决对象之前相互循环引用的问题

    可达性分析算法

    通过一系列“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用连相连,则证明此对象是不可能再被使用的。
    固定可作为GC Roots的对象包括以下几种:

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等

  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  • 在方法区中常量引用的对象,譬如字符串常量池里的引用
  • 在本地方法栈中JNI(Native方法)引用的对象
  • Java虚拟机内部的引用
  • 所有被同步锁持有的对象
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

    引用

  • 强引用(Strong Reference)

传统“引用”定义,无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。

  • 软引用(Soft Reference)

用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围进行第二次回收,如果这次回收还没有足够的内存,将抛出OOM

  • 弱引用(Weak Reference)

描述非必须对象,强度弱于软引用,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉被弱引用关联的对象。

  • 虚引用(Phantom Reference)

一个对象是否有虚引用的存在,不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能够在这个对象被收集器回收时收到一个系统通知。

生存还是死亡?

当可达性分析算法判定为不可达的对象还需要经历两次标记过程才会被真正回收。如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize方法,或者finalize方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。如果这个对象被判定为有必须执行finalize方法,那么改对象将会被放置到名为F-Quene的队列之中,并在稍后由一条由虚拟机自动建立的,低调度优先级的Finalizer线程去执行它们的finalize方法,如果此时在finalize方法中重新建立引用链,此对象将不会被回收。

回收方法区

方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。回收废弃常量与回收Java堆中的对象非常类似。举个常量池中字面量回收的例子,假如一个字符串“java”曾经进入常量池中,但是当前系统又没有任何一个字符串对象的值是“java”,换句话说,已经没有任何字符串对象引用常量池中的“java”常量,且虚拟机中也没有其他地方引用这个字面量。如果在这时发生内存回收,而且垃圾收集器判断确有必要的话,这个“java”常量就将会被系统清理出常量池。常量池中其他类(接口)、方法、字段的符号引用也与此类似。

02|垃圾收集算法

从如何判断对象消亡的角度拆发,垃圾收集算法可以划分为:

  • 引用计数式垃圾收集(Reference Counting GC)(直接垃圾收集)
  • 追踪式垃圾收集(Tracing GC)(间接垃圾收集)

    分代收集理论(Generational Collection)

    分代收集建立在两个分代假说之上:

  • 若分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的

  • 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
  • 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占少数

假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将堆划分出不同的区域,然后将回收对象依据其年龄分配到不同的区域中存储。针对跨代引用,只需要在新生代上建立一个全局的数据结构(记忆集:Remembered Set),这个结构把老年代划分成若干小块,标示老年代的哪一块内存会存在跨代引用。当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。
GC方式:

  • 部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集其中又分为
    • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集
    • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集
    • 混合收集(Mixed GC):指目标时收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器有这种行为。
  • 整堆收集(Full GC):收集整个Java堆的方法区的垃圾收集

标记-清除算法

算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收有所未被标记的对象
缺点:

  • 执行效率不稳定:如果Java堆中包含大量对象,而其中大部分是需要被回收的,这时必须进行大量的标记和清除的动作,导致标记和清除两个过程的执行效率都随着对象数量的增长而降低
  • 内存空间碎片化:标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前出发另一次垃圾收集动作

标记-复制算法

半区复制(Semispace Copying)

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完,就将还活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

Appel式回收

HotSpot虚拟机的Serial、ParNew等新生代收集器均采用了这种策略来设计新生代的内存布局。Appel式回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾收集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1。当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(通常是老年代)进行分配担保。

标记-整理算法

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

03|HotSpot的算法细节实现

根节点枚举

固定可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中,如要逐个检查所有的GC Roots需要耗费大量的时间。迄今为止,所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的,与整理内存碎片一样会面临相似的“Stop The World”的困扰。

  • 准确式垃圾收集:

所以当用户线程停顿下来之后,其实并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得到哪些地方存放着对象引用的。在HotSpot的解决方案里,是使用一组称为OopMap的数据结构来达到这个目的。一旦类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,在即时编译过程中,也会在特定的位置记录下栈里和寄存器里哪些位置是引用。这样收集器在扫描时就可以直接得知这些信息了,并不需要真正一个不漏地从方法区等GC Roots开始查找。

安全点

在“特定的位置”记录OopMap,这些位置被称为安全点。安全点的设定,决定了用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到达安全点后才能够暂停。
安全点选取规则:“是否具有让程序长时间执行的特征”。
垃圾收集发生时让所有线程都跑到最近的安全点策略:

  • 抢先式中断:不需要线程的执行代码主动去配合,如果发现有用户线程中断的地方不在安全点上,就恢复这条线执行,让它一会在重新中断,知道跑到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程响应GC事件。
  • 主动式中断:当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志位,各个线程执行过程时会不停的主动去轮询这个标志,一旦发现中断标志为真就自己在最近的安全点上主动中断挂起。轮询标志的地方和安全点是重合的,另外还要加上所有创建对象和其他需要在Java堆上分配内存的地方,这是为了检查是否即将要发生垃圾收集,避免没有足够内存分配新对象。

    安全区域

    安全点机制保证了程序执行时,在不太长的时间内就会遇到可进行入垃圾收集过程的安全点。但是,程序“不执行”(程序没有分配处理器期间,用户线程处理Sleep或者Blocked状态)。此时线程无法响应虚拟机的中断请求,不能再走到安全的地方去中断挂起自己,虚拟机也不可能持续等线程重新被激活分配处理器时间。对于这种情况,就必须引入安全区域来解决。
    安全区域时指能够确保在某一段代码之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被拓展延伸来的安全点。

记忆集与卡表

  • 记忆集:

记忆集是一种用于记录从非收集区域志向收集区域的指针集合的抽象数据结构,目的是解决分代收集时,跨代引用所带来的问题。

  • 卡表:

卡表是记忆集的一种具体实现。每个记录精确到一块内存区域,该区域内有对象含有跨代指针。卡表最简单的形式可以只是一个字节数据,数组汇总每一个元素对应着其标示的内存区域中一块特定大小的内存块,这个内存块被称作“卡页”。通常卡页大小都是以2的N次幂的字节数(HotSpot使用的卡页是2的9次幂,即512字节)。一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的标识为1,称为这个元素变脏(Dirty),没有泽标识为0。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易的出哪些卡页内存中包含跨代指针,把它们加入GC Roots中一并扫描

写屏障

维护卡表状态

04|经典垃圾收集器

Serial 收集器

Serial收集器是一个单线程工作的收集器,但它的“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。
优点:简单高效,对于内存资源受限的环境,它是所有收集器里额外内存消耗最小的;对于单核处理器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,可以获得最高的单线程收集效率。

ParNew收集器

Serial收集器的多线程并发版本

Parallel Scavenge收集器

Parallel Scavenge收集器也是能够并行收集的多线程收集器,基于标记-复制算法实现,其特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值:
02|垃圾收集器和内存回收策略 - 图1

Serial Old收集器

Serial Old是Serial收集器的老年代版本。基于标记-整理算法实现。

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本。基于标记-整理算法实现。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,使用于B/S系统的服务端。基于标记-清除算法实现,它的运作过程相对于签名集中收集器来说要更复杂一些,整个过程分为四个步骤,包括:

  • 1)初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,速度很快;
  • 2)并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并非运行;
  • 3)重新标记:为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但页远比并发标记阶段的时间短;
  • 4)并发清除:清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所有这个阶段也是可以与用户线程同时并发的。

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

  • CMS收集器对处理器资源非常敏感。事实上,面向并发设计的程序都对处理器资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程(或者说处理器的计算能力)而导致应用程序变慢,降低总吞吐量
  • 无法处理“浮动垃圾”,在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,期间还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一个部分垃圾就称为“浮动垃圾”。
  • 基于“标记-清除”算法实现的收集器,收集结束时会有大量空间虽碎片产生。

Garbage First收集器

  • Mixed GC模式:

在G1收集器出现之前的所有其他收集器,包括CMS在内,垃圾收集的目标范围要么时整个新生代,要么就是整个老年代,再么就是整个Java堆。G1可以面向堆内存任何部分来组成回收集进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大。
G1不再坚持固定大小以及固定区域(Region),每一个Region都可以根据需要扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间,熬过多次收集的旧对象都能获取很好的收集效果。
Region中还有一类特殊的Humongous区域,专门用来存储大对象。对象大小超过了一个Region容量的一半即可判断为大对象。

  • G1垃圾收集处理思路:

G1收集器会跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间,优先处理回收价值收益最大的哪些Region,这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率。

低延迟垃圾收集器