Java垃圾回收机制

2019-02-09

Java内存运行时数据区域的各个部分,其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,所以这几个区域不需要过多考虑回收的问题。

Java垃圾回收关注的是Java堆和方法区这两块区域的内存。

垃圾回收的基本步骤

  1. 查找内存中不再使用的对象
  2. 释放这些对象占用的内存

1 什么是垃圾

“垃圾”在这里指的就是所有不再存活的对象。常见的判断是否存活有两种方法:引用计数算法可达性分析算法

1.1 引用计数算法

引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象(不是引用)都有一个引用计数。当一个对象被创建时,且将该对象分配给一个变量,该变量计数设置为1。每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
优点:简单,高效。
缺点:无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0。

1.2 可达性分析算法

为了解决上面的循环引用问题,Java采用了一种新的算法:可达性分析算法。 这个算法的基本思想是:通过一系列的称为“GC Roots”(每种具体实现对GC Roots有不同的定义)的对象作为起点,向下搜索它们引用的对象,可以生成引用树,树的节点视为可达对象,反之视为不可达,则将它作为垃圾收集。在对象遍历阶段,GC必须记住哪些对象可以到达,以便删除不可到达的对象,这称为标记(marking)对象。
可达性分析算法
在Java中可以作为GC Roots的对象有:

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

1.3 Java引用类型

在JDK1.2后把对象的引用分为强引用、软引用、弱引用和虚引用。

  • 强引用:类似“Object o=new Object()”,只要强引用还在,垃圾回收器永远不会回收被引用的对象。
  • 软引用:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。
  • 弱引用:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存
  • 虚引用:虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

2 常用的垃圾回收算法

2.1 标记-清除算法(Mark-Sweep)

标记-清除算法分为两个阶段:标记阶段清除阶段

  • 标记阶段的任务是标记出所有需要被回收的对象;
  • 清除阶段就是回收被标记的对象所占用的空间。

优点:是简单,容易实现。
缺点:是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。
适用用于存活对象多,回收对象少
标记-清除

2.2 复制算法(Copying)

复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。
优点:就是,实现简单,运行高效且不容易产生内存碎片;
缺点:将能够使用的内存缩减到原来的一半。
适用于存活对象很少。回收对象多
复制

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

该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。
适用用于存活对象多,回收对象少
标记-整理

2.4 分代收集算法

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是:根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation)
老年代的特点是每次垃圾收集时只有少量对象需要被回收,一般使用的是Mark-Compact算法。而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,基本上都采取Copying算法
在堆区之外还有一个代就是永久代(Permanet Generation),它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量无用的类
JDK1.8前的分代 注意:JDK 1.8中永久代被全部移除,元空间取代了永久代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。主要用于存储类的信息、常量池、方法数据、方法代码等,由所有线程共享。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存
JDK1.8的分代

3 HotSpot实现

3.1 枚举根节点

为了保证引用关系不发生变换,在系统GC时必须停顿所有的Java执行线程。目前主流的Java虚拟机主要采用的是准确性GC,所以当执行系统都停顿下来之后,并不需要一个不漏的检查完所有执行上下文和全局的引用位置,虚拟机应该有办法直接知道哪些地方存放着对象引用。
在HotSpot的实现中,用一组叫做OopMap的数据结构来实现这个目的,在类加载完成后记录下对象内的数据类型与偏移量,在JIT编译过程中记录下栈和寄存器中哪些位置是引用。这样,在GC扫描的时候就可以直接得到这些信息了。

3.2 安全点

导致OopMap变化的指令非常多,我们只在特定的地点做记录,我们把这些点叫做安全点,也就是说让所以线程(不包括JNI调用的线程)跑到最近的安全点再停顿下来,所以安全点不能让GC等待时间过长,也不能太频繁。这里我们分为两种方式:

  • 抢占式中断:在GC的时候停下所以线程,再让没有到安全点的跑到安全点,目前几乎没有虚拟机采用这种方式进行GC
  • 主动式中断:当GC需要中断线程的时候,在安全点上设置中断标志,线程执行时主动轮询中断标志,发现标志为真时则主动中断挂起线程。轮询标志的地方和安全点重合。

3.3 安全区域

安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中任意地方开始都是安全的。在线程执行到Safe Region中的代码时,就标记自己已经进入了Safe Region,这样JVM在发起GC时就跳过这些线程。在线程要离开Safe Region时,它要检查系统是否已经完成了枚举(或GC过程),如果完成了线程就继续执行,否则就等待。
注意:安全点机制保证程序被执行时遇到GC可以进入安全点,而安全区域则保证了程序当前没有被执行时可直接放入安全区域。

4 内存分配与回收策略

Java Heap被分为两部分:Young Generation 和 Old Gereration。Perm并不属于Heap。如下图:
JDK1.8前的分代
大多数情况下,new出来的对象都放在Young Gen,当Young Gen满了, 就会执行Garbage Collection (GC), 此时的GC称为Minor GC。 Young Gen被分成三部分:Eden Memory和两个Survivor Memory。

  • 多数情况下,对象都在新生代Eden区中分配,但一些大对象可能会直接进入到老年代。虚拟机提供了一个 -XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接进老年代分配,这样做的目的是避免在Eden区以及两个Survivor区之间发生大量的内存拷贝。
  • 当Eden分区满了,JVM就会执行Minor GC。被引用的对象都会存活下来,它们将被移到Survivor区域里,也就是 图中的S0或S1。
  • 同一时间的两个Survivor区,一个用来保存对象,另一个是空的;每次进行Minor GC垃圾回收时,就把Eden和From的可达对象复制到To区域中,一些生存时间长的就复制到老年代,接着就清除Eden和From空间,最后把原来的To空间变为From空间,原来的From空间变为To空间。( 有点类似于双缓冲队列 )
  • 虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivro区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold来设置。
  • 动态对象年龄判定如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
  • 空间分配担保在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。

区别Minor GC、Major GC和Full GC:
新生代GC(Minor GC):指发生在新生代的垃圾回收动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
老年代GC(Major GC):指发生在老年代的GC,出现了Major GC,经常会伴随一次的Minor GC(但并非绝对),Major GC的速度一般比Minor GC慢10倍以上。
Full GC:是清理整个堆空间,包括年轻代和老年代。

参考文献

Java垃圾回收机制
Java 技术之垃圾回收机制
理解Java垃圾回收机制
Java中的四种引用类型
Java堆内存管理
Java8内存模型—永久代(PermGen)和元空间(Metaspace)