《深入理解Java虚拟机》2.垃圾回收_标记阶段

1.垃圾标记阶段

1.1引用计数算法

1.1.1概述

很多教科书判断对象是否存活的算法是这样的: 在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一,当引用失效时,计数器值就减一,任何时刻当计数器的值为0的对象就是不可能再被使用的。

客观的说,引用计数算法(Reference Counting)虽然占用了一些额外的内存空间来进行计数,但是它的原理比较简单,判断效率也很高,在大多数情况下都是一个不错的算法。

但是在Java领域,至少主流的Java虚拟机里面都没有选用这种算法来管理内存,主要原因是,这个看似简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确的工作,譬如单纯的使用这种算法就很难解决对象之间相互循环引用的问题。

1.1.2图示循环引用引发的问题

循环引用容易导致内存泄露。
在这里插入图片描述

1.2可达性分析算法

1.2.1概述

当前主流的商用程序语言(Java C# 。。。)的内存管理系统,都是通过可达性分析算法(Reachability Analysis)来判断对象是否存活的,这个算法的基本思路就是通过一系列称为"GC Roots"的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为"引用链"(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者从图论的话来说就是从GC Roots到这个对象不可达,则证明此对象是不可能再被回收的。

如下图所示,对象Object5, Object6, Object7虽然互有关联,但是他们到GC Roots是不可达的,因此他们会被判定为可回收的对象。

在这里插入图片描述

1.2.2GC Roots对象

在Java技术体系里,固定可以作为GC Roots的对象包括下面几种

  • 在虚拟机栈(栈帧中的局部变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量
  • 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
  • 所有被同步锁(synchronized关键字)持有的对象
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。

1.3对象的finalize()方法

Java提供了对象终止(Finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑

当垃圾回收器发现没有引用指向一个对象时,即垃圾回收此对象之前,总是回到用这个对象的finalize()方法,注意:finalize()是Object类中定义的方法 允许被重写

不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用,

  • 在finalize()中可能导致对象复活
  • 一个糟糕的finalize()会严重影响GC性能
  • finalize()方法的执行时间是没有保障的 它完全由GC线程决定 极端情况下 若不发生GC 则finalize() 方法将没 有执行机会

由于finalize()方法的存在 虚拟机中的对象一般处于三种可能的状态

如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用了。一般来说,此对象需要被回收。但事实上,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段。一个无法触及的对象有可能在某一个条件下“复活”自己,如果这样,那么对它的回收就是不合理的,为此,定义虚拟机中的对象可能的三种状态。如下:

  • 可触及的: 从根节点开始 可以到达这个对象
  • 可复活的: 对象的所有引用都被释放 但是对象有可能在finalize()中复活
  • 不可触及的:对象的finalize() 被调用 并且没有复活 就会进入不可触及状态 不可触及的对象不可能被复活 因为finalize() 只会被调用一次

以上三种状态 是由于finalize()方法的存在进行的区分 只有在对象不可触及时 才可以被回收

1.4垃圾标记阶段整个过程

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

①第一次标记

如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链(即不可达),那么这个对象将会被第一次标记。

②筛选

在进行第一次标记后,随机会进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法(主要目的是判断当执行了finalize()方法后,对象有没有可能复活)

假如对象没有重写finalize()方法或者finalize()已经被虚拟机调用过了,那么虚拟机认为没有必要执行。

问题来了:为什么这两种情况Java虚拟机会认为没有必要执行finalize()方法呢?

1.没有重写finalize(),说明还是父类的方法,Object中finalize()方法体是空的,没有进行重写,对象执行完此方法后一定不可能复活

2.首先要知道,任何一个对象的finalize()方法只会被系统自动调用一次,如果已经被调用过了,那么就没有必要执行了



虚拟机认为有必要执行finalize()方法。

如果对象重写了finalize()方法且还没有执行过,那么虚拟机就认为有必要执行finalize()方法,那么该对象就会被放置在一个F-Queue的队列中,并在稍后由一条虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法

这里所说的"执行"是指虚拟机会触发这个方法开始运行,但是不承诺一定会等待其运行结束,这样做的原因是如果某个对象的finalize()方法执行缓慢,或者更极端的发生了死循环,将很可能导致F-Queue队列中的其他对象永远处于等待,甚至导致整个内存回收子系统的崩溃。

③第二次标记

finalize()方法是对象逃脱死亡命运的最后一次机会,稍后收集器将对F-Queue中的对象进行第二次小规模的标记,如果对象在finalize()中成功复活(只要重新与引用链上的任何一个对象建立关联即可,例如把自己(this关键字)赋值给某个类变量或者对象的成员变量),那么在第二次标记时,它将被移出即将回收的集合,如果对象这时候还没有逃逸,那基本上它就真的要被回收了。

没有进入F-Queue中的对象即虚拟机认为没有必要执行finalize()的方法的对象(即必死无疑的对象),在第二次标记时就会被标记为垃圾,最终被回收。

整个标记阶段的流程图

在这里插入图片描述

1.5四种引用

强软弱虚引用详解

猜你喜欢

转载自blog.csdn.net/qq_46312987/article/details/120982034