AnthonyZero's Bolg

JVM-垃圾收集算法

前言-对象是否存活

如何判断Java中一个对象应该 “存活” 还是 “死去”,这是 垃圾回收器要做的第一件事

引用计数算法

Java 堆 中每个具体对象(不是引用)都有一个引用计数器。当一个对象被创建并初始化赋值后,该变量计数设置为1。每当有一个地方引用它时,计数器值就加1。当引用失效时,即一个对象的某个引用超过了生命周期(出作用域后)或者被设置为一个新值时,计数器值就减1。任何引用计数为0的对象可以被当作垃圾收集。当一个对象被垃圾收集时,它引用的任何对象计数减1。

  • 优点:引用计数收集器执行简单,判定效率高,交织在程序运行中。对程序不被长时间打断的实时环境比较有利。
  • 缺点:难以检测出对象之间的循环引用。同时,引用计数器增加了程序执行的开销。所以Java语言并没有选择这种算法进行垃圾回收。

可达性分析算法

可达性分析算法也叫根搜索算法,通过一系列的称为GC Roots 的对象作为起点,然后向下搜索。搜索所走过的路径称为引用链 (Reference Chain), 当一个对象到 GC Roots 没有任何引用链相连时, 即该对象不可达,也就说明此对象是 不可用的。

GC Roots 一般包含以下内容:

  • 虚拟机栈中局部变量表中引用的对象
  • 本地方法栈中 JNI 中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中的常量引用的对象

方法区的回收

因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,所以在方法区上进行回收性价比不高。
主要是对常量池的回收和对类的卸载。为了避免内存溢出,在大量使用反射和动态代理的场景都需要虚拟机具备类卸载功能。类的卸载条件很多,需要满足以下三个条件,并且满足了条件也不一定会被卸载:

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

方法区是存在垃圾回收的,只不过性价比比较低。在堆中,尤其在新生代,常规应用进行一次垃圾收集一般可以回收70%到95%的空间。

标记-清除算法

该算法分为“标记”和“清除”阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:

  1. 标记和清除的效率都不高
  2. 清除之后容易出现不连续内存,当需要分配一个较大内存时就不得不需要进行一次垃圾回收。

Alt text

复制算法

复制算法的存在,正是为了解决内存碎片问题。并且这个算法也是分代算法的基础。

将内存分为大小相等的两块,每次程序只使用其中一块,当GC发生的时候,把存活的对象复制到另外一块内存中,整齐的排列,然后清空原来的那块内存。
Alt text
可以看到,这种算法有点新生代转移到老年代的感觉。

新生代中分为一个 Eden 区和两个 Survivor 区。通常两个区域的比例是 8:1:1 ,使用时会用到 Eden 区和其中一个 Survivor 区。当发生回收时则会将还存活的对象从 Eden ,Survivor 区拷贝到另一个 Survivor 区,当该区域内存也不足时则会使用分配担保利用老年代来存放。
优点:

  1. 解决了内存碎片的问题。

缺点:

  1. 把内存可使用的空间减少了一半,造成空间的浪费。
  2. 对象存活数量较多的时候,复制较多,性能比较差

绝大多数最新被创建的对象会被分配到新生代,由于大部分对象在创建后会很快变得不可达,所以很多对象被创建在新生代,然后消失.所以在新生代(对象存活率低)会使用该算法

标记-整理算法

复制算法如果在存活对象较多时效率明显会降低,特别是在老年代中并没有多余的内存区域可以提供内存担保。

所以老年代中使用的时候标记整理算法,它的原理和标记清除算法类似,只是最后一步的清除改为了将存活对象全部移动到一端,然后再将边界之外的内存全部回收。
Alt text

优点:不会产生内存碎片。不足:需要移动对象,处理效率比较低。

分代回收算法

现代多数的商用JVM的垃圾收集器都是采用的分代回收算法,和之前所提到的算法并没有新的内容,只是将Java 分为了新生代和老年代。

  • 新生代: 每次垃圾回收时都发现有大批对象死去,只有少量对象存活,可采用 复制算法,只需要付出少量存活对象的复制成本就可以完成回收。
  • 老年代: 对象存活率高,没有额外空间对它进行分配担保,必须使用 标记-清除 或 标记-整理 算法进行回收。