[JVM入门指南02]GC垃圾回收机制

477 阅读9分钟

概述

在JVM中主要的结构为:虚拟机栈、堆、方法区。其中虚拟机栈的栈帧在编译器就已经确定大小的,随着方法的结束或线程的技术,虚拟机栈的内存也随着回收。而Java堆和方法区这两个区域则有很显著的不确定性,这部分内存的分配和回收都是动态的,GC所关注的真是这部分内存该如何管理。

本篇文章就以下三方面GC所要完成的三件事:

  • 哪些内存需要回收?(对象存活算法)
  • 什么时候回收?(触发GC的条件)
  • 如何回收?(GC的工作原理)

哪些内存需要回收

如何判定哪些对象需要回收?在堆里面存放了几乎所有的对象实例,在GC之前第一件事就是要确定这些对象哪些还“存活”着,哪些已经“死去”。其中判断对象是否存活有两种算法:可达性分析算法、引用计数算法。

可达性分析算法

通过一系列的**“GC Roots”根对象作为始节点,开始往下遍历有引用**关系的对象形成一条“引用链”,通过这条引用链的可达的对象是存活的,不可达的对象则是不再使用。

那么什么样的对象可以GC Roots呢?主要是以下对象:

  1. 虚拟机栈中的引用对象
  2. 方法区的类静态的引用对象、常量的引用对象
  3. Java虚拟机内部引用:基本数据类型的Class对象、异常对象、系统类加载器
  4. 所有同步锁持有的对象
  5. 反映虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存。

可达性分析算法是大多数系统使用的对象存活判断算法。

引用计数算法

在对象中添加一个引用计数器,每当有一个地方引用它,计数器就+1,当一个引用失效,计数器就-1,当计数器>0时就表示对象为存活使用状态,不得回收。大多数不会用这个办法来判断对象存活,因为当对象实例相互引用时,当栈中的引用已经失效,对象也还是不能回收。


上面的可达性分析算法和引用计数算法都用到了引用,这种引用默认是强引用。在JDK1.2版本之后,Java除了有强引用外,还增加了三种引用:

  • 软引用(SoftReference): 内存溢出前,可对持有该引用的对象回收
  • 弱引用(WeakReference):GC就会被回收,相当于没有引用。
  • 虚引用(PhantomReference): 无法通过其获得对象实例,唯一目的是当对象被回收时能收到一个系统通知。

如果对象被可达性分析算法引用计数算法识别为无使用对象就可以被GC回收吗?不是,一个对象要至少要经过两个的标志过程。其还会查询对象是否重写了finalize()方法,如果重写了就会用另一个Finalize线程去执行这个finalize方法,这个方法可以时对象重新被引用,但官方并不推荐这样做。


堆中的对象实例可以使用可达性分析算法和引用计数算法来决定是否回收。但方法区的常量池和类型信息是否回收却另有条件:

  • 常量池的字符串和符号引用 虚拟机中没有任何对象有引用到它。

  • 类型卸载 1.该类的所有实例被回收 2.该类的ClassLoader被回收 3.Class对象没有被任何地方引用 以上三种条件同时被满足。

什么时候回收

根据两个分代假说:绝大多数的对象都是朝生夕死熬过越多次的GC回收的对象就越难回收。把堆进行了分代:新生代(Eden、From、To)、老年代,在GC时也进行了分代回收。

image.png

Minor GC: 回收新生代的无使用对象,新生代的对象的特性是大多数是朝生夕死的。触发时机有:

  • Eden区空间不足,触发Minor GC 由于Eden空间大小有限,所以Minor GC触发的更加频繁,这就需要收集算法速度快、效率高,一般使用标记-复制算法对这一区域进行回收(后面讲)。

Major GC:回收老年代的无使用对象。一般使用标记-清除算法标记-整理算法进行回收。

Full GC: 回收堆和方法区的无使用对象。Full GC回收范围比较大,执行的时间较长可能会造成卡顿,所以要尽量减少Full GC的次数。触发时机大致有:

  • 老年代的空间不足 由新生代对象的进入老年代、大对象直接进入老年代等,如果在老年代的最大连续空间上无法存放这些对象时,就会进行一次Full GC回收。

  • 方法区的空间不足 方法区主要存储类型信息和常量池,也有空间不足的风险,会进行Full GC回收

  • System.gc()被显示调用会Full GC回收

三种垃圾收集算法:

1. 标记-清除算法

原理:用可达性分析算法将不可用的对象进行标记,然后对无用的对象进行清除。 缺点:在对象很多的情况下,标记的效率低。清除对象之后会产生内存碎片,内存不连续。 作用:在老年代回收中一些收集器会使用此算法

2. 标记-复制算法

原理:将内存空间一分为二,一半用于对象的存放,一半空闲。如果存放对象的区域满了,使用可达性分析算法把存活的对象移动标记出来,然后复制到另一个空的区域,同时把之前的区域全部清空变成空的连续空间。 缺点:如果存活对象很多,要产品大量的内存复制开辟。内存空间只能用一半优点浪费资源。 作用:在新生代朝生夕死的对象中一般用此回收算法。但新生代中对复制算法进行了优化,但这种算法加入了分配担保机制防止存活对象过多分配不了的情况。使用了一种Appel式的回收算法:

image.png

3. 标记-整理算法

原理:标记的过程跟标记-清除算法一样,然后整理存活的对象往一端移动,然后存活边界之外的对象全部清除。 缺点:移动对象有一定的风险。对象太多效率不高 作用:主要作用在老年代。

image.png

如何回收

GC使用的垃圾收集器进行回收,随着不断的发展,垃圾收集器也越来越多,这里列举常规的垃圾收集器并进行分为三类:单线程收集器、多线程收集器、并发收集器。

单线程收集器

单线程的收集器的组合有:Serial/Serial Old收集器。它们不仅仅用一个收集线程去完成收集操作,而且在收集线程工作的时候,用户线程必须停止等待,直到收集完成为止。如图是Serial/Serial Old收集器示意图:

image.png 如果客户端的内存资源受限,处理器核心数较少或单核处理器来说,其简单高效的可以使收集器最快的工作完。

多线程并行收集器

多线程的收集器有:ParNew、Parallel Scavenge、Parallel Old,其中Parallel Scavenge/Parallel Old为组合收集器。这些多线程收集器仅仅是增加了垃圾收集线程,用户线程依然是停止等待垃圾收集的。

image.png

parNew收集器:其实就是Serial的多线程版本,目前能与Serial收集器和CMS收集器合作。

Parallel Scavenge收集器:一般配合Parallel Old收集器使用。相比于parNew收集器,它更加注重是吞吐量 的控制,吞吐量就是用户线程执行的时间占总CPU运行的时间,吞吐量当然是越大越好。

多线程一般用服务端,因为多线程的执行,有时间片轮转的消费时间,如果对于单处理器来说无疑处理效率更慢。但对于资源很好,不用与用户交互的分析运算的服务端却可以增加执行效率。

并发收集器

并发收集器有:CMS收集器,是一款以系统停顿的时间尽量较短,用户体验较好为目标的收集器。它的收集线程可以与用户线程并发执行。CMS有三次的标记(初始标记、并发标记、重新标记)和一次清理(并发清理),在三次的标记中有两次标记需要较短用户线程停止,一次较长的与用户线程并发的标记,和与用户线程并发的清除。

image.png

初始标记:标记GC Roots关联的第一个对象,时间很短 并发标记:和用户线程并发执行GC Roots的引用链(可达性分析算法),时间较长 重新标记:重新查找在并发标记阶段,用户线程运行生成的新的引用链。时间比初始标记长一点。 并发清除:用标记-清除算法把无用对象进行清除。

三大缺点:一:CPU敏感,并发对核心数少的处理器对用户线程的运行可能会造成影响。二:浮动垃圾:在并发清理阶段产生的垃圾只能等下一次GC回收。三:内存碎片,标记-清理法会产品大量的不连续的内存空间。

小结

本文从那些内存需要回收什么时候回收如何回收作为执行分别写出了两个对象存活判断算法、Class区回收的条件、回收的分代机制与收集时机、三个收集算法和常用的垃圾收集器。