垃圾收集

整理自《深入理解 Java 虚拟机》。

程序计数器、虚拟机栈、本地方法栈这几个区域不需要过多考虑回收问题,因为方法结束或线程结束时,内存自然就随着回收了。垃圾收集关注的是 Java 堆和方法区这部分内存。

1. 方法区的回收

HotSpot 虚拟机的方法区在永久代中实现,这部分主要回收两部分内容:废弃常量和无用的类。

常量池的回收

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

无用的类的回收

类要满足 3 个条件才能算是“无用的类”:

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

满足上述 3 个条件的无用类可以被回收,是否对类进行回收,可以由虚拟机 -Xnoclassgc 参数控制。

2. Java 堆的回收

如何判断一个对象是否“死亡”?:

引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它时, 计数器值就加 1 ; 当引用失效时,计数器值就减 1;任何时刻计数器为 0 的对象就是不可能再被使用的。

特点:实现简单,判定效率高。但主流虚拟机没有选用引用计数法来管理内存,最主要原因是它很难解决对象之间相互循环引用的问题。

		objA.instance = objB;
		objB.instance = objA;

		objA = null;
		objB = null;
		
		// 假设在这行发生GC,objA 和 objB 不能被回收
		System.gc();

可达性分析算法
该算法的基本思路就是通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为引用链(Reference Chain),当一个对象到 GC Roots没有任何引用链相连时(即从GC Roots节点到该节点不可达),则证明该对象是不可用的。
在这里插入图片描述
object1~object4 对 GC Root 都是可达的,说明不可被回收,object5 和 object6 虽然相互有关联,但对 GC Root 节点不可达,说明其可以被回收。

在 Java 中,可作为 GC Root 的对象包括以下几种:

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

Java 引用概念

  • 强引用(Strong Reference):强引用就是指在程序代码中普遍存在的,类似 Object obj = new Object() 这类的引用,只要强引用在,垃圾搜集器永远不会搜集被引用的对象。也就是说,宁愿出现内存溢出,也不会回收这些对象。
  • 软引用(Soft Reference):软引用是用来描述一些有用但并不是必需的对象,在 Java 中用 java.lang.ref.SoftReference 类来表示。对于软引用关联着的对象,只有在内存将要发生溢出之前 JVM 才会回收该对象。
  • 弱引用(Weak Reference):弱引用也是用来描述非必需对象的,当 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用 java.lang.ref.WeakReference 类来表示。
  • 虚引用(Phantom Reference):虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在 java 中用 java.lang.ref.PhantomReference 类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

判定对象死亡的过程
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。

  1. 第一次标记并进行一次筛选。筛选的条件是此对象是否有必要执行 finalize() 方法。当对象没有覆盖 finalize 方法,或者 finzlize 方法已经被虚拟机调用过,虚拟机将这两种情况都视 为“没有必要执行”,对象被回收。
  2. 第二次标记。finalize() 方法是对象脱逃死亡命运的最后一次机会,如果对象在 finalize() 中成功拯救自己——只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。

3. 垃圾收集算法

标记-清除算法

标记清除算法分为“标记”和“清除”两个阶段,首先先标记出那些对象需要被回收,在标记完成后会对这些被标记了的对象进行回收。
在这里插入图片描述
缺点:效率不高,造成内存碎片。

复制算法

复制算法是将内存分为两块大小一样的区域,每次是使用其中的一块。当这块内存块用完了,就将这块内存中还存活的对象复制到另一块内存中,然后清空这块内存。
在这里插入图片描述
现在商用的 JVM 中都采用了这种算法来回收新生代,因为新生代的对象基本上都是朝生夕死的,存活下来的对象约占10%左右,所以需要复制的对象比较少,采用这种算法效率比较高。hotspot 版本的虚拟机将堆内存分为了新生代和老年代,其中新生代又分为内存较大的 eden 区和两个较小的 survivor 区。当进行内存回收时,将 eden 区和 survivor 区的还存活的对象一次性地复制到另一个 survivor 空间上,最后将 eden 区和刚才使用过的 survivor 空间清理掉。hotspot 虚拟机默认 eden 和 survivor 空间的大小比例为8:1,也就是每次新生代中可用内存空间为整个新生代空间的90%(80%+10%),只会浪费掉10%的空间。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当survivor 空间不够用时,需要依赖于其他内存(这里指的是老年代)进行分配的担保。

标记-整理算法

复制算法在对象存活率较高的情况下就要进行较多的对象复制操作,效率将会变低。更关键的是,如果不想浪费 50% 的空间,就需要有额外的空间进行分配担保,用以应对被使用的内存中所有对象都 100% 存活的极端情况,所以在老年代一般不能直接选用这种办法。

根据老年代的特点,有人提出了标记-整理的算法,标记过程仍然与标记-清除算法一样,但后续步骤不是直接将可回收对象清理掉,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,算法示意图如下:
在这里插入图片描述

分代收集算法

分代收集算法将 heap 区域划分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

4. 垃圾收集器

Minor GC 和 Full GC

  • Minor GC 又称新生代 GC,指发生在新生代的垃圾收集动作,因为 Java 对象大多是朝生夕灭,所以Minor GC非常频繁,一般回收速度也比较快。
  • Full GC 又称 Major GC 或老年代 GC,指发生在老年代的 GC, 出现 Full GC 经常会伴随至少一次的 Minor GC(不是绝对,Parallel Sacvenge 收集器就可以选择设置 Major GC策略)。

Major GC 速度一般比 Minor GC 慢10倍以上。

使用分代垃圾收集器,基于以下观察事实:

  • 大多数分配对象的存活时间短
  • 存活时间久的对象很少引用存活时间短的对象

由此, HotSpot VM 将堆分为两个物理区空间,这就是分代。根据新生代和老年代各自的特点,我们应该分别为它们选择不同的收集器,以提升垃圾回收效率。
在这里插入图片描述
连线的表示可以搭配使用。

名称 工作区域 单线程/多线程 垃圾收集算法 特点
Serial 新生代 单线程 复制算法 垃圾收集时会暂停其他所有工作线程、简单高效、适合 Client 模式
Serial Old 老年代 单线程 标记整理算法 Serial 的老年代版本
ParNew 新生代 多线程 复制算法 Serial 的多线程版本、多核情况下减少 GC 时间
Parallel Scavenge 新生代 多线程 复制算法 吞吐量优先、可控制吞吐量、能够在较短的时间内完成指定任务,适合不需太多交互的后台运算
Parallel Old 老年代 多线程 标记整理算法 Parallel Scavenge的老年代版本
CMS 老年代 多线程 标记-清除算法 以最短回收停顿时间为目标、并发收集、缺点:吞吐量低、无法处理浮动垃圾,导致频繁 Full GC、使用"标记-清除"算法产生碎片空间
G1 新生代、老年代 多线程 整体来看基于标记-整理,局部来看基于复制算法 并发、追求低停顿、可预测的停顿、不会产生内存碎片
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章