The Garbage Collection Mechanism of the JVM


 

The Garbage Collection Mechanism of the JVM

JVM垃圾收集机制

一. 概念

什么是垃圾收集?

垃圾收集(Garbage Collection,GC),指的是对Java堆和方法区的死对象进行回收。它诞生于1960年 MIT 的 Lisp 语言,经过半个多世纪,现已十分成熟。

二. 收集的范围

哪些内存需要回收

在Java内存运行时区域中,程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭,栈帧随着方法的进入和退出做入栈和出栈操作,加上基本认为栈帧所分配的大小随类的结构确定而保持不变,因此这些区域的的内存分配和回收都具有确定性,所以我们的内存垃圾回收主要集中于 Java 堆和方法区中,在程序运行期间,这部分内存的分配和回收都是动态的 。

三. 收集目标

  • Java堆:回收目标为死对象,即那些不会不可能再被任何途径使用的对象。
  • 方法区,即Hotspot VM中的永久代(Permanet Generation):收集效率非常低,其中进行内存垃圾回收的两个主要内容是废弃常量和无用的类

四. 判活算法

怎么判断一个对象是否死对象?

1. 引用计数(Reference Counting)

即每个对象都有一个引用计数器,当有引用连接至对象时,引用计数加1;当有引用离开作用域或被置为null时,引用计数减1。此方法简单、高效,但无法解决对象相互循环引用的问题。

若对象间存在循环引用,可能出现“对象应该被回收,但引用计数却不为零”的情况。定位这样的交互自引用的对象组所需的工作量极大。引用记数常用来说明垃圾收集的工作方式,但似乎从未被应用于任何一种Java虚拟机实现中。

2. 可达性分析(Reachability Analysis)

img

从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。

GC停顿:亦称Stop The World(STW),即在整个可达性分析期间,整个执行系统看起来就像是被冻结在某个时间点上,不能出现在分析过程中对象引用关系还在不断变化的情况。

在Java语言中,GC Roots包括:

  • 虚拟机栈中引用的对象
  • 方法区中类静态属性实体引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI引用的对象

五. 收集方法

有哪些垃圾收集算法?它们各有什么特点?

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

( 1 )原始标记-清除算法 (Naïve mark-and-sweep)

img

每个对象在内存中都有一个标记位用于垃圾回收,标记该对象到 GC roots 是否可达,然后进行 标记-清理 过程

  • 标记:遍历整个GC Roots ,标记被 GC root 指向的对象和这些对象指向的对象的状态位为 in-use
  • 清除:扫描所有待回收的内存,若对象未被标记为 in-use ,将它们的标记为清除,否则将对象清除

之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。

img

( 2 )三色标记 (Tri-color marking)

三色标记算法的数据结构中包含有三个集合:White Set , Black SetGray Set

  • 白色集合对象:需要被回收的对象。
  • 黑色集合对象:没有对白色集合对象的外部引用,并且是GC Root可达的对象。这些对象将不会被回收。
  • 灰色集合对象:集合中的对象全都是GC Root可达的对象,但是正在扫描或正在等待扫描其对“白色集合对象”的引用,这些对象也不会被回收,并且会在扫描结束之后被移入黑色集合。

在大多数算法实现中,黑色集合初始是空,灰色集合中保存有与 GC Roots 对象直连的所有老年代对象,白色集合中包含有其他对象。内存中的任意对象在任意时间都仅存在于这三个集合当中的一个。

算法步骤:

img

  1. 从灰色集合中取出一个对象放入黑色集合
  2. 遍历第1步取出的对象的所有白色集合对象引用,并将它们移入灰色集合。这保证了这个对象和它的引用对象都不会被GC
  3. 重复上述两步,直到灰色集合为空

由于非GC Root直接可达的节点都被加入到了White Set,并且对象只能从白色集合移动到灰色集合,从灰色集合移动到黑色集合,所以算法体现了一个重要特性:黑色集合中的对象不会引用到白色集合中的对象。这就保证了在灰色集合为空时,我们可以放心地释放白色空间中的对象。这被称作三色不变式(The Tri-color Invariant)。

主要缺点

  • 效率:标记和清除过程的效率都不高;
  • 空间:标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

2. 复制算法(Copying)

img

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

这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低。

3. 分代收集算法(Generational Collection)

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域,再针对不同区域的特点采用不同的收集算法

一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

( 1 ) 年轻代中的GC

目前大部分垃圾收集器对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为三块:一块较大的Eden空间两块较小的 Survivor空间,这两块Survivor空间分别是From区To区 ,顾名思义分别是内存复制的源头和终点,但From与To又是相对的,它们会随着每次垃圾收集而发生变化。

img

大体上年轻代的清理过程如下

  • 在GC开始的时候,对象只会存在于Eden空间Survivor空间的From区Survivor空间的 To区是空的
  • 紧接着进行GC,Eden空间中所有存活的对象都会被复制到Survivor空间的To区
  • 而在Survivor空间的From区中,仍存活的对象会根据他们的年龄值来决定去向
    • 年龄达到一定值(年龄阈值,可通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中
    • 没有达到阈值的对象会被复制到Survivor空间的To区
  • 经过这次GC后,Eden空间和Survivor空间的From区已被清空。From区To区交换角色(即FromTo会交换他们的角色,即新的To就是上次GC前的From,新的From就是上次GC前的`To),等待下一次GC。

不管怎样,都会保证 Survivor空间的To区 是空的。Minor GC会一直重复这样的过程,直到 To 区被填满,To区 被填满之后,会将所有对象移动到年老代中。

img

( 2 ) 老年代中的GC

由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法。

从定义及特点上看,以上收集算法的用于对Java堆的对象进行回收,但是理论上这些算法均适用于Java堆和方法区,因为有些虚拟机(如Hotspot VM)的GC分代收集就扩展到了方法区,这时就可像管理Java堆那样管理方法区,从而省去了为方法区独立编写收集内存管理代码的功夫。

六. 参考

  • 《深入理解Java虚拟机》(第二版,周志明著,机械工业出版社)
Java, Security developer https://jordonyang.github.io/ Guangzhou, China 本站所有文章如未说明均为原创,请勿随意转载,如需转载请联系我 (linfengit@qq.com)