JVM必会Java GC,助你过五关斩六将

前言

都知道《深入理解Java虚拟机》这本书对Java开发是非常重要的,几乎达到了一个必读的重要性。但是在校期间一直看不深入,就停留在最表面的意思,无法深入理解。经历了几个月的码农生活,再次回过头来看这本书的时候,我发现了许多以前不懂的东西都慢慢看懂了,所以借此机会写下这一篇Java GC。

Java GC的目标

遇到一个问题或者一个知识点,我们首先要理解和思考为什么要解决这个问题。说到GC的目的是什么呢?显然是回收JVM的内存,因为JVM分配的内存是有限的,随着程序创建的对象越来越多,如果不进行GC的话就会导致内存垃圾越来越多,最后程序崩溃。既然目的是回收垃圾内存,那么新的问题就是,哪些对象可以被回收?什么时候进行回收?怎么回收?

哪些对象可以被回收?

简单的说就是没有用的对象就可以被回收。那么什么是没有用的对象呢?《深入理解Java虚拟机》给出了两种方法,一种叫引用计数法,一种叫可达性分析法。

引用计数法

《深入理解Java虚拟机》中是这样讲的:在对象中添加一个引用计数器,每当有一个地方引用它的时候,计数器值就加一;当引用失效的时候,计数器值就减一;在任何时刻计数器为零的对象就是不可能再被使用的,这个时候会被GC回收。但是这个方法会出现一个问题,就是循环引用问题。比如对象A引用了对象B,但是对象B也引用了对象A,那么这个时候对象A和对象B的引用计数器都不会为0,但是这两个对象都没有被其他对象引用,理论上说是要被GC回收的。

可达性分析法

可达性分析(Reachability Analysis)算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的,这个时候会被GC回收。

一般作为GC Roots的对象包括以下几种:

  • Java栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI引用的对象

如下图所示,因为从CGRoots作为起点可以找到对象1和对象2,所以对象1和对象2是有用的。对象3和对象4和对象5都没有在GCRoots的链下面,所以此时的对象3,对象4和对象5都是没有用的,会被GC回收。
在这里插入图片描述

谈谈引用

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否引用链可 达,判定对象是否存活都和“引用”离不开关系。在JDK 1.2版之前,Java里面的引用是很传统的定义: 如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该reference数据是代表 某块内存、某个对象的引用。这种定义并没有什么不对,只是现在看来有些过于狭隘了,一个对象在 这种定义下只有“被引用”或者“未被引用”两种状态,对于描述一些“食之无味,弃之可惜”的对象就显 得无能为力。譬如我们希望能描述一类对象:当内存空间还足够时,能保留在内存之中,如果内存空 间在进行垃圾收集后仍然非常紧张,那就可以抛弃这些对象——很多系统的缓存功能都符合这样的应 用场景。

在JDK 1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用(Strongly Re-ference)、软 引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强 度依次逐渐减弱。

  • 强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回 收掉被引用的对象。
  • 软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内 存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存, 才会抛出内存溢出异常。在JDK 1.2版之后提供了SoftReference类来实现软引用。
  • 弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只 能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只 被弱引用关联的对象。在JDK 1.2版之后提供了WeakReference类来实现弱引用。
  • 虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的 存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚 引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供 了PhantomReference类来实现虚引用。

什么时候回收?

上面讲了哪些对象会被回收,接下来就来讲什么时候进行垃圾回收呢?在排除人为的调用,垃圾回收都是发现在对新生对象进行内存分配的时候,这个时候如果JVM内存空间不足就会触发GC进行垃圾回收。

怎么回收?

现在知道了哪些对象可以被回收,什么时候进行回收,现在要解决的就是怎么进行回收了。垃圾回收根据实现的方式不同有多种不同的算法实现。《深入理解Java虚拟机》讲了标记清除算法,复制算法,标记整理算法,分代回收算法等,下面分别一一介绍一下。

标记清除算法

标记清除算法主要分为两步,一步是标记出哪些是垃圾对象,另一步是清除这些被标记的对象。这个算法会出现一个问题就是内存碎片化严重。
在这里插入图片描述

在这里插入图片描述

由上图可以看出来,标记清除算法会导致内存碎片化,可利用率不高。

复制算法

复制算法就是把内存分为两部分,在平时使用的时候只用其中的固定一部分,在当需要进行GC的时候,把存活的对象复制到另一部分中,然后将已经使用的内存全部清理掉。
在这里插入图片描述
在这里插入图片描述

复制算法解决了标记清除的碎片化问题,但是很明显的一个问题就是内存使用率大大降低,能使用的内存只有原来的一半。

标记整理算法

标记整理算法是结合了标记清除算法和复制算法。标记阶段和标记清除算法一样,先标记出需要回收的部分,然后把存活的对象往内存的一端进行移动,然后清除剩下的部分。
在这里插入图片描述
在这里插入图片描述

标记整理算法可以解决上面两个算法的问题,一般应用在老年代,但是整体的效率偏低。

分代回收算法

分代回收算法是目前使用较多的一种算法,这个不是一个新的算法,而是将内存进行划分,不同区域的内存使用不同的算法。根据对象的存活时间将内存的划分为新生代和老年代,其中新生代分为Eden区和From Survivor区,To Survivor区。在新生代使用的是复制算法,在进行对象内存分配的时候只会使用Eden区和From Survivor区,当发生GC的时候,会将存活的对象复制到To Survivor区,然后循环进行复制。当某个对象进行了15次GC后依旧存活,那么这个对象就会进入老年代。老年代因为每次回收的对象都会特别少,因此使用的是标记整理算法。

垃圾回收器

讲完了垃圾回收算法,我们再熟悉一下垃圾回收器,这些垃圾回收器都不必全部弄懂,只需要重点关注CMS和G1就可以了。

  • Serial垃圾收集器(单线程,复制算法)
  • ParNew垃圾收集器(Serial+多线程)
  • Parallel Scavenge收集器(多线程复制算法,高效)
  • SerialOld收集器(单线程标记整理算法)
  • ParallelOld收集器(多线程标记整理算法)
  • CMS收集器(多线程标记清除算法)
  • G1收集器
    在这里插入图片描述

上图展示了七种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。上面的垃圾收集器我们只需要重点学习CMS和G1这两款相对复杂而广泛使用的收集器,深入了解它们的部分运作细节即可,感兴趣的可以自行学习。

参考

  1. 《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》
  2. Java GC机制详解