JVM之垃圾回收过程

JVM之垃圾回收过程

本文主要学习自《深刻理解Java虚拟机》,内容顺序大体遵循书本顺序,加上自身对于其中算法的理解学习。今天小泉主要跟你们一块儿学习一下垃圾回收的整个流程。java

前言

垃圾回收(Garbage Collection),是一种自动内存管理机制。 按照惯例,下面放出垃圾回收的维基百科定义。程序员

维基百科
在计算机科学中,垃圾回收(英语:Garbage Collection,缩写为GC)是指一种自动的存储器管理机制。
当某个程序占用的一部份内存空间再也不被这个程序访问时,这个程序会借助垃圾回收算法向操做系统归还这部份内存空间。垃圾回收器能够减轻程序员的负担,也减小程序中的错误。算法

GC比Java诞生的更早,最先起源于Lisp语言,1960年MIT的大佬们就开始研究如何可以自动管理内存和垃圾回收技术了。通过这么多年的发展,JVM中的GC机制已经十分健壮与成熟了。缓存

断定垃圾回收的对象

回收垃圾以前,首先要找到须要被看成垃圾而回收的对象。markdown

JVM分为五个区域——程序计数器、虚拟机栈、本地方法栈、堆、方法区。咱们知道程序计数器与栈均是线程私有的,其生命周期取决于线程的生命周期。所以不须要考虑其中回收的问题。数据结构

JVM

而堆和方法区属于线程共享部分,是对象实例、变量存放的地方,是可以被动态分配的。所以根据前文定义以及此处的分析,咱们能够知道,须要被看成垃圾被回收的是堆和方法区中没法再次被访问的内存区域。函数

堆的回收

那么知道了垃圾回收的对象的定义,那么应当如何判断当前对象是否没法再次被访问即对象“死亡”呢?post

引用计数算法(Reference Counting)

引用计数算法相对来讲比较简单,对象实例化后该实例对象引用计数初始值为1,后续根据该实例对象的引用与释放来进行增一、减1操做,当这一实例对象的计数器归零,则断定该对象“死亡”。学习

引用计数算法原来相对简单,断定效率相对来讲也就高一些,许多地方都采用了引用计数算法,如微软的COM(Component Object Model)技术、Python语言、Object-C语言都采用了引用计数算法的内存管理方式。优化

固然引用计数算法也有着本身的弊端,好比对于对象之间的相互引用。举个书中给出的小栗子:

public clss ReferenceCountingGC {
  public Object instance = null;
  private int size = 10;//无实际意义
  
  public statice void testGC() {
    ReferenceCountingGC objA = new ReferenceCountingGC();
    ReferenceCountingGC objB = new ReferenceCountingGC();
    //相互引用
    objA.instance = objB;
    objB.instance = objA;
    
    objA = null;
    objB = null;
  }
}
复制代码

对象实例化计数器初始值为1,被引用+1,最后引用释放-1,最后两个对象的计数器值为1,对象未死亡,可在其余地方这两个对象再无使用,也就是该内存不可以被回收。

可达性分析算法(Reachability Analysis)

可达性分析算法是目前C#和Java主用的断定算法。其算法流程以下:

  1. 选定根对象(GC Roots)做为起始节点集
  2. 从起始节点开始,根据对象引用关系进行向下继续搜索节点,造成引用链

从数据结构与算法的角度重构这一过程:

以实例对象为节点,以对象的引用关系为以边造成的无向图,从无向图中选取特定的节点做为特定节点,选出与这些特定节点没有任何关联(直接或者间接能够访问到)的节点。

其实就是图论中无向图的遍历,若是只是从算法角度去考虑,这里能够考虑采用构建邻接矩阵或者邻接链表并使用DFS或者BFS的方式去处理,这个会在之后数据结构与算法系列中推出。

Reachability Analysis

如图,从GC Roots开始搜索到的节点均是存活对象,而未搜索到的节点则会被断定为“死亡”对象。

知道了算法流程,还须要知道一个前提,咱们如何选取特定节点也就是根对象?

在Java中,书中给出几种GC Roots的固定来源:

  • 虚拟机栈中引用的对象——临时变量、局部变量等
  • 方法区中类静态属性引用的对象——静态变量等
  • 方法区中常量引用的对象——字符串常量池等
  • 本地方法栈JNI(Native方法)引用的对象
  • JVM内部引用——基础数据类型、系统类加载器、常见异常对象(OOM等)等
  • 同步锁(sychronized关键字)持有的对象
  • 反映Java虚拟机内状况的JMXBean、本地代码缓存等等

除了固定GC Roots集合,还有一些“临时性”对象会被加入到该集合中,共同构成GC Roots集合。这一部分在之后再继续讲解吧。

那么是否是只要与GC Roots集合无关联即不可达对象就会被回收呢?

不可达就要被回收了吗?

不可达还不意味着必定就会被回收。JVM在通过可达性分析算法断定某一对象与GC Roots无关后,会对这一对象作一个标记,后续会进行二次断定,而二次断定的内容是:是否有必要实行对象的finalize(),若是对象没有覆盖finalize()或者finalize()方法已经被虚拟机调用了,那么便可认定为“没有必要执行”。

若是二次断定为“有必要执行”,那么对象会被放置在F-Queue的队列中,以后会有一条由JVM自动创建且低调度优先级的Finalizer线程去执行队列中对象的finalize(),执行机位触发方法而不等待方法执行结束

所以,不可达对象在此时还有着拯救本身的方法,那就是重写finalize()函数,但注意finalize()函数也只会进入一次,也就是不可达对象仅有一次拯救本身的办法。

方法区的回收

对于方法区的回收,在前文介绍JVM内存区域已经说起到了。

方法区回收

针对回收的对象,方法区回收主要分两类,一类是常量池中的废弃常量,一类是再也不使用的类型。

废弃常量

废弃常量的断定和堆中对象的断定十分相近,再也不赘述,可参考对回收中的引用计数。

再也不使用的类型

一样,针对“再也不使用”,须要规定其范围。

  1. 该类型(包括派生子类)全部实例已经被回收
  2. 该类型的类加载器已经被回收(难以达成)
  3. 其java.lang.Class对象没有被引用

达到上述三个要求的类型才可以容许被当垃圾被回收,然而这里须要注意的是,此处也只是“容许”而非必然。

垃圾收集

分代收集理论

在肯定了垃圾回收的对象,那么还须要有着回收垃圾的管理者,也就是垃圾收集器,目前大多数垃圾收集器遵循着分代收集理论,该理论基于两个假说提出:

  1. 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
  2. 强分代假说(Strong Geneartional Hypothesis):熬过越屡次垃圾收集过程的对象就越难以消亡。

基于以上两个假说,咱们的垃圾收集器会将Java堆划分为多个不一样区域,同时将回收对象依据对象熬过垃圾收集的次数划分到各个区域,回收垃圾则在回收时会对某一个或者某几个区域进行回收。

虽然不一样的垃圾收集器划分的区域不一样,但区域大体会划分为新生代(Young Generaion)和老年代(Old Generation)两个区域。正是因为这样的划分也致使了一个问题——两个区域的对象有着引用关系(称之为跨代引用),这样就对某个区域的回收形成了影响(必须再去查询区域中的存活对象)。

为此引入了第三点假说:

  1. 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来讲仅占极少数。

有了第三点假说,咱们就能够认为当发生新生代和老年代的跨代引用,老年代的难以消亡特色会使得引用的新生代对象得以存活并随着垃圾收集次数的增长而晋升到老年代。

同时第三点假说告诉咱们跨代引用的部分会不多不多,所以咱们只须要创建一种数据结构用来标记住老年代中进行了跨代引用的内存区域,将其加入到GC Roots中进行新生代存活对象的断定。

垃圾收集种类

根据垃圾收集的区域不一样,主要分为如下几种垃圾回收的方式:

  1. Partial GC:局部垃圾收集,指的是目标不是整个Java堆扽的垃圾收集
    • Minor GC :新生代收集,目标仅针对新生代
    • Major GC :老年代收集,目标仅针对老年代
    • Mixed GC :混合收集,目标是收集新生代以及部分老年代
  2. Full GC :整堆收集,目标是整个Java堆和方法区

垃圾收集器

垃圾收集器这里先不作介绍,等深刻学习后再跟你们分享。

垃圾收集算法

根据断定回收垃圾对象的方式,垃圾回收的算法也分为两种,一种称为“引用计数式垃圾回收”(ReferenceCounting GC),另外一种称为“追踪式垃圾回收”(Tracing GC)。因为JVM采用的是可达性分析算法进行“垃圾”的断定,所以一下主要介绍后一种收集算法。

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

标记清除算法是垃圾收集算法的基础。它的思路相对来讲是最简单的,按字面意思理解,先“标记”,再“清除”:

  1. 标记

    首先按照可达性分析算法所述,先断定可达性再进行二次断定后标记为可回收

  2. 清除

    将标记为可回收的区域清除回收

标记-清除算法

标记-清除的过程很简单,所以也有着许多缺点,主要可归为两点:

  1. 效率较低

    标记和清除的过程主要在于扫描对象:先扫描全部对象进行标记,再扫描判断标记进而清除,所以当可回收对象的数量增多时其效率就会下降。

  2. 碎片化内存

    内存碎片化是可想而知的,可回收的对象不必定会保存在连续一段的内存空间,所以不经任何处理直接清除,会形成诸多的碎片化空间,类比于操做系统的内存管理操做。

标记-复制算法(Mark-Copy)

标记-复制算法是在标记-清除算法基础上进行了优化。

1969年Fenichel在标记-清除算法的基础上提出一种“半区复制”的垃圾收集算法,其算法流程(标记过程已经省略,标记-整理算法一样省略该过程)以下:

  1. 内存区域一分为二,一次使用一半区域,另外一半做为保留区域。
  2. 当一半区域使用完以后,先将已使用区域中可达(存活)对象分配到保留区域。
  3. 再回收已使用彻底的区域中可回收内存区域,而后使用原先的保留区域。

半区复制

这样的标记-复制算法对于须要回收大量内存的情形十分友好,而且进行内存复制操做以后不须要考虑内存碎片化问题。 可是相应的也有着其缺点。

  1. 在内存复制搬移过程当中产生了较大的内存复制开销,而且随着存活对象的增长,效率也会下降。
  2. 内存实际可用的区域变为了原先区域的二分之一,浪费内存空间

对于新生代的垃圾收集,这种方式却是一种有效的思路,只不过比例再也不须要是1:1,IBM针对分代收集理论的第一个假说,新生代中98%的对象熬不过第一轮收集。 而在此基础上,1989年Andrew Appel提出了一种更加优化的算法。

Appel式回收

将新生代分为三部分:80%的Eden空间、10%的Survivor(称为from)空间、10%的Survivor(称为to)空间。

可供分配使用的内存为80%的Eden空间和10%的名为from的Survivor空间,当可供分配的使用完以后,JVM将进行垃圾回收,其步骤以下:

  1. 将Eden与from空间的可达对象复制到to空间
  2. 清除回收Eden与from空间内全部可回收对象
  3. 原先的from空间变为了下一次的to空间,原先的to空间就变为了下一次的from空间。

Appel式回收

若是存活对象超过这10%的Survivor空间又该怎么办呢?别急,还有老年代兜着底,拿出一部分用来放置多出的存活对象。

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

标记-复制算法对于新生代的垃圾回收相对来讲十分有效,但对于老年代来讲,这种算法不太适宜,由于老年代须要考虑极端状况即内存全被占用,所以1974年Edwaed LueDers提出了另外有针对性的算法即标记-整理算法。 其算法流程以下:

  1. 将全部标记的可达对象移动到内存区域的一端,造成可达对象边界区
  2. 清理除边界以外的全部对象

标记-整理算法

与标记-清除算法相比,标记-整理算法其实只是多了一个移动可达对象的操做,这样作的好处是内存分配时不用考虑碎片问题,而这样作的坏处是内存回收会变得更加复杂。

总结

学习完以后,咱们知道,JVM中的垃圾回收主要针对的是Java堆,而本文也主要针对Java堆的垃圾收集进行分析:垃圾回收的对象断定(引用计数算法 && 可达性分析以及二次断定)、划分不一样区域并针对不一样区域提出的垃圾收集算法。这一过程仍是比较复杂的,仍是须要你们多多学习呀~

针对垃圾收集算法,总结了以下一张小表格给你们,但愿可以帮助你们快速了解下三种算法的优缺点。

垃圾收集算法 优势 缺点
标记-清除算法 算法设计简单 效率较低,回收后容易内存碎片化
标记-复制算法 针对新生代相对高效,不用考虑内存碎片化问题 浪费部份内存空间,且有内存复制的开销
标记-整理算法 针对老年代相对高效,不用考虑内存碎片化问题 设计较为复杂,且有内存移动的开销

垃圾回收的过程就是JVM回收内存的过程,这对于之后排查内存问题(如内存泄漏、内存溢出等等)有着重大的帮助,但愿你们看完以后,能有所收获。

相关文章
相关标签/搜索