垃圾收集算法与垃圾收集器

如何判断对象是否应该被回收?

在堆里面存放着Java世界中几乎全部的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要肯定这些对象哪些应该被回收。java

引用计数算法

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任什么时候刻计数器为零的对象就是不可能再被使用的。web

存在的问题:很难解决对象之间循环引用的问题。算法

主流的Java虚拟机都没有选择该方式来管理内存。缓存

可达性分析算法

基本思路:经过一系列称为“GC Roots”的根对象做为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过 程所走过的路径称为“引用链”,若是某个对象到GC Roots间没有任何引用链相连, 或者用图论的话来讲就是从GC Roots到这个对象不可达时,则证实此对象是不可能再被使用的。以下图多线程

可达性分析算法
可达性分析算法

图片来源:《深刻理解Java虚拟机并发

固定可做为GC Roots的对象包括如下几种:编辑器

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等。
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  • 在方法区中常量引用的对象,譬如字符串常量池里的引用。
  • 在本地方法栈中Native方法引用的对象。
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(好比 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  • 全部被同步锁(synchronized关键字)持有的对象。
  • 反映Java虚拟机内部状况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

Java四种引用类型

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

Java引用类型
Java引用类型

finalize()方法

若是对象在进行可达性分析后发现没有与GC Roots相链接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种状况都视为“没有必要执行”。post

对象能够在finalize()方法中经过将本身与引用链上的对象从新关联,来逃脱垃圾回收。测试

PS:至今没写过这个方法。不推荐使用。

/**  * 此代码演示了两点:  * 1.对象能够在被GC时自我拯救。  * 2.这种自救的机会只有一次,由于一个对象的finalize()方法最多只会被系统自动调用一次  *  * 代码来源:《深刻理解Java虚拟机》  * @author : fuyuaaa  * @date : 2020-06-03 15:58  */ public class FinalizeEscapeGC {  public static FinalizeEscapeGC SAVE_HOOK = null;   public void isAlive() {  System.out.println("yes, i am still alive :)");  }   @Override  protected void finalize() throws Throwable {  super.finalize();  System.out.println("finalize method executed!");  FinalizeEscapeGC.SAVE_HOOK = this;  }   public static void main(String[] args) throws Throwable {  SAVE_HOOK = new FinalizeEscapeGC();  //对象第一次成功拯救本身  SAVE_HOOK = null;  System.gc();  // 由于Finalizer方法优先级很低,暂停0.5秒,以等待它  Thread.sleep(500);  if (SAVE_HOOK != null) {  SAVE_HOOK.isAlive();  } else {  System.out.println("no, i am dead :(");  }    // 下面这段代码与上面的彻底相同,可是此次自救却失败了,由于finalize只会被执行一次  SAVE_HOOK = null;  System.gc();  // 由于Finalizer方法优先级很低,暂停0.5秒,以等待它  Thread.sleep(500);  if (SAVE_HOOK != null) {  SAVE_HOOK.isAlive();  } else {  System.out.println("no, i am dead :(");  }  } }  result: finalize method executed! yes, i am still alive :) no, i am dead :( 复制代码

方法区的垃圾收集

方法区的垃圾收集主要回收两部份内容:

废弃的常量:没有地方引用这个常量

再也不使用的类型:(同时知足三个条件)

  • 该类全部的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
  • 加载该类的类加载器已经被回收。
  • 该类对应的java.lang.Class对象没有在任何地方被引用,没法在任何地方经过反射访问该类的方法。

垃圾收集算法

标记-清除算法

首先标记出全部须要回收的对象,在标记完成后,统一回收掉全部被标记的对象;也能够反过来,标记存活的对象,统一回收全部未被标记的对象。

缺点

  • 执行效率不稳定,标记和清除两个过程的执行效率都随对象数量增加而下降。
  • 内存空间的碎片化问题,可能会致使大对象没法分配内存而不得不提早gc。

标记-复制算法

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,而后再把已使用过的内存空间一次清理掉。

缺点

  • 将会产生大量的内存间复制的开销
  • 浪费一半空间

如今的商用Java虚拟机大多都优先采用了这种收集算法去回收新生代。

标记-整理算法

标记过程仍然与“标记-清除”算法同样,但后续步骤不是直接对可回收对象进行清理,而是让全部存活的对象都向内存空间一端移动,而后直接清理掉边界之外的内存。

缺点

  • 若是有大量对象存活,移动存活对象并更新全部引用这些对象的地方将会是一种极为负重的操做,并且这种对象移动操做必须全程暂停用户应用程序才能进行。

经典垃圾收集器

HotSpot虚拟机的垃圾收集器
HotSpot虚拟机的垃圾收集器

PS:

  • 上图为HotSpot虚拟机的。有连线表明能够搭配使用。

  • 因为维护和兼容性测试的成本,在JDK 8时将Serial+CMS、 ParNew+Serial Old这两个组合声明为废弃(JEP 173),并在JDK 9中彻底取消了这些组合的支持(JEP 214)。

  • 并行(Parallel):并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工做,一般默认此时用户线程是处于等待状态。

  • 并发(Concurrent):并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器线程与用户线程都在运行。因为用户线程并未被冻结,因此程序仍然能响应服务请求,但因为垃圾收集器线程占用了一部分系统资源,此时应用程序的处理的吞吐量将受到必定影响。

Serial 收集器

  • 最基础、历史最悠久
  • 新生代收集器
  • 单线程
  • 标记-复制算法
  • 会形成“Stop The World”
Serial:Serial Old收集器运行示意图
Serial:Serial Old收集器运行示意图

ParNew 收集器

  • Serial垃圾收集器的多线程并行版本
  • 新生代收集器
  • 多线程并行
  • 标记-复制算法
  • 会形成“Stop The World”
ParNew:SerialOld收集器运行示意图
ParNew:SerialOld收集器运行示意图

Parallel Scavenge 收集器

  • 新生代收集器
  • 多线程并行
  • 标记-复制算法
  • 会形成“Stop The World”
  • 收集器目标是达到一个可控制的吞吐量
    • 控制最大垃圾收集停顿时间 -XX:MaxGCPauseMillis
    • 直接设置吞吐量大小 -XX:GCTimeRatio
    • 自适应的调节策略 -XX:+UseAdaptiveSizePolicy

Serial Old 垃圾收集器

  • Serial收集器的老年代版本
  • 单线程
  • 标记-整理算法
  • 会形成“Stop The World”
Serial:SerialOld收集器运行示意图2
Serial:SerialOld收集器运行示意图2

Paraller Old 垃圾收集器

  • Parallel Scavenge收集器的老年代版本
  • 多线程并发
  • 标记-整理算法
  • 始于JDK6
ConcurrentMarkSweep收集器运行示意图
ConcurrentMarkSweep收集器运行示意图

CMS 收集器

  • 标记-清除算法
  • 以获取最短回收停顿时间为目标
ConcurrentMarkSweep收集器运行示意图
ConcurrentMarkSweep收集器运行示意图

具体步骤:

CMS收集器步骤
  • 优势:
    • 并发收集、低停顿
  • 缺点:
    • 对处理器资源敏感,在并发阶段会占用线程致使总吞吐量变低。
    • 没法处理“浮动垃圾”(并发阶段产生的新的垃圾)。
    • gc时须要预留内存空间给用户线程,若是这个内存不够,会致使“并发失败”,会临时使用Serial Old收集器进行老年代收集(会“Stop The World”)。
    • 标记-清除,没法处理空间碎片(CMS默认开启了内存碎片的合并整理过程)。

Garbage First 收集器

G1把连续的Java堆划分为多个大小相等的独立区域(Region),每个Region均可以扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器可以对扮演不一样角色的 Region采用不一样的策略去处理。

Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个 Region容量一半的对象便可断定为大对象。

可预测的停顿时间模型:将Region做为单次回收的最小单元,让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所得到的空间大小以及回收所需时间的经验值,而后在后台维护一个优先级列表,每次根据用户设定容许的收集停顿时间,优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。

G1收集器运行示意图
G1收集器运行示意图

具体步骤:

G1收集器步骤

PS

  • TAMS:在收集线程与用户线程同时运行的时候,确定会有新对象的分配。G1为每个Region设 计了两个名为TAMS(Top at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程当中的新对象分配,并发回收时新分配的对象地址都必需要在这两个指针位置以上。
  • STAB:原始快照算法,用于处理并发时可能产生的“对象”消失问题。能够瞅瞅这个: 连接
  • 与CMS相似,若是内存回收的速度赶不上内存分配的速度,G1收集器也要被迫冻结用户线程执行,致使Full GC(多线程)而产生长时间“Stop The World”。

参考

深刻理解Java虚拟机

图片来源《深刻理解Java虚拟机

本文使用 mdnice 排版

相关文章
相关标签/搜索