五、Jvm系列(2)——GC 相关

Java 与 C++ 之间有一堵由 内存动态分配和垃圾收集技术 所围成的 “高墙”,墙外面的人想进去,墙里面的人却想出来。

说起 垃圾收集(Garbage Collection, GC),我们需要思考 3件事情。

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

1. 哪些内存需要回收?

  • 程序计数器虚拟机栈本地方法栈 3个区域 随线程而生,随线程而灭。
  • 每一个栈帧中分配多少内存基本上是在 类结构 确定下来时就已知的。因此这几个区域的内存分配和回收都具有确定性,在这几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟着回收了。

  • Java 堆和方法区 不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的。即:垃圾收集器所关注的。

总结:Java 堆和方法区是垃圾收集器所关注的内存区域。

2. 什么时候回收?

在堆里面存放着 Java 世界中几乎所有的对象实例,所以哪些对象已经 “死去”(即不可能再被任何途径使用的对象)。就回收哪些。那么如何判断对象是否存活呢?

2.1 引用计数算法(Reference Counting)

  • 优点: 简单、判定效率高的算法。 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值 +1;当引用失效时,计数器值 -1;任何时刻计数器为 0 的对象就是不可能再被使用的。
  • 缺点:但是主流的 Java 虚拟机里面没有用这个算法来管理内存的。 最主要的原因是它 很难解决对象之间相互循环引用的问题。

2.2 可达性分析算法(Reachability Analysis)

基本思想:通过一系列的称为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为 引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。用一个图能很容易的理解。

从图中知道 GC Roots 的重要性,那么 Java 中,可作为 GC Roots 的对象包括下面几种:

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象;
  • 方法区中 类静态属性 引用的对象;
  • 方法区中 常量 引用的对象;
  • 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象。

2.3 Java 的四种引用,强软弱虚

2.3.1 强引用(Strong Reference)

强引用是指在代码中普遍存在的,如 Object obj = new Object() 这类的引用,只要强引用还在,GC 永远不会回收被引用的对象。

2.3.1 软引用(SoftReference)

用来描述一些 还有用但并非必需的对象,对于软引用关联的对象,在将要发生内存溢出之前,会将这些对象列进回收范围之中进行第二次回收。 如果这次回收后还是没有足够的内存,才会抛出内存溢出异常。

2.3.1 弱引用(WeakReference)

用来描述 非必需的对象。但是它的强度比 软引用 更弱一点。当 GC 工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。也就是说被 弱引用关联的对象 只能生存到下一次 垃圾收集 发生之前。

2.3.1 虚引用(PhantomReference)

也被称为 幽灵引用或者 幻影引用,最弱的一种引用关系。无法通过 虚引用 来取得一个对象实例。
为一个对象设置 虚引用 关联的唯一目的就是 能在这个对象被收集器回收时收到一个 系统通知。

2.4 回收方法区

方法区也被称为永久代,这里的垃圾收集主要回收两部分内容: 废弃常量无用的类

2.4.1 废弃常量

以常量池中 字面量 的回收为例,假如一个字符串 “abc” 已经进入了常量池中,但是当前系统没有任何一个 String 对象是叫做 “abc” 的,换句话说,就是没有任何 String 对象引用常量池中的 “abc” 常量。如果这时候发生内存回收,而且必要的话,这个 “abc” 常量就会被系统清理出常量池。常量池中的其他 类(接口)、方法、字段的符号引用也与此类似。

2.4.2 无用的类

判定一个类是否是 “无用的类” 的条件则苛刻许多。需同时满足下面3个条件才能算是 无用的类。

  • 该类所有的实例都已经被回收,也就是Java 堆中不存在该类的实例。
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

当一个类满足上述3个条件时,虚拟机 可以 对它进行回收。但也只是 “可以”,而并不是和对象一样,不使用了就一定会回收。也就是说虚拟机也可以不回收它。
是否对类进行回收,HotSpot 虚拟机提供了 -Xnoclassgc 参数进行控制,

3. 如何回收?

了解了 哪些内存区域需要回收、四种引用的区别 以及 什么时候会进行回收,那么现在要解决的就是 如何进行回收了。即:垃圾收集算法

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

概述:算法分为两个阶段 标记清除 两个阶段:首先标记出所有需要回收的对象,在标记完成后统一进行回收。

缺点:
1、效率问题:标记和清除两个过程的效率都不高;
2、空间问题:标记清除后悔产生大量不连续的内存碎片,空间碎片太多可能会导致以后需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次 GC。

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

概述:与 标记-清除算法一样,但是后续步骤不是直接对 可回收对象 进行清理,而是 让所有存活的对象都向一端移动,然后直接清理掉 端边界之外的内存,这样就解决了 内存碎片的问题。

3.3 复制算法(Copying)

概述:将可用的内存按容量分为大小相等的两块,假设为 A块 和 B块,每次只使用其中的一块。当A块的内存用完了,就将还存活的对象复制到 B块 上面,然后再把 A块使用过的内存空间一次清理掉。
缺点:代价太高,将内存缩小为了原来的一半。

改进:现在的商业虚拟机都采用这种收集算法来回收 新生代。 而研究表明,新生代中的对象一般情况下98% 是 朝生夕死的,所以并不需要按照 1:1 的比例来划分内存空间。

  • 将内存分为一块较大的 Eden 空间两块较小的 Survivor 空间,每次使用 Eden其中一个 Survivor
  • 当回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。
  • HotSpot 虚拟机默认 Eden : Survivor 是 8 : 1, 也就是每次新生代可用内存空间为整个新生代容量的90%(80%+10%)
  • 当 Survivor 空间不够用时,需要依赖 老年代 进行分配担保(Handle Promotion)

3.4 分代收集算法(Generational Collection)

概述:当前商业虚拟机的 GC 都采用 “分代收集”算法。 这种算法没有什么新思想,只是根据对象存活周期的不同将内存划分为几块。
一般是将 Java 堆分为 新生代老年代,这样就可以根据每个年代的特点采用最适当的 收集算法。

  • 新生代中,每次 GC 都发现有大批对象死去,只有少量存活,那就使用 复制算法
  • 老年代中,对象存活率高,没有额外空间对它进行分配担保,就必须使用 标记-清除 或者 标记-整理算法进行回收。

参考

深入理解 Java 虚拟机