垃圾回收算法手册读书笔记(六)特定语言相关内容

对象的终结

自动垃圾回收能够很好的管理其托管范围内的对象,但对于其托管范围外的对象无能为力。如果托管对象引用了一个非托管对象,当该托管对象不可达将被回收时,回收器也无法对该非托管对象做进一步处理,这有可能造成资源的泄漏。一个典型的例子就是程序打开一个文件,如果开发人员忘记关闭文件,垃圾回收器在回收文件对象时也不会帮助开发人员自动关闭文件。

但是对于一个文件对象被多个组件共享的场景,开发人员不一定能够准确的判断出关闭文件的时机。而对于垃圾回收器来说把握这个时机容易的多。只要组件在完成工作后将文件对象的引用置空,最终垃圾回收器在回收的过程中就能探测到该文件对象不可达,这时候就是安全关闭文件的好时机。

为了达到这个目的,我们需要在对象不可达后,执行一些用户自定义的行为(比如关闭文件)。典型的机制是用户指定运行一段代码,即终结方法

为了记录这些需要终结的对象,通常运行时系统会维护一张特殊的终结表,用于记录包含终结方法的对象,在需要终结对象创建时加入该表,在该对象终结方法执行完成后移除。从终结表指向需要终结对象的引用称为终结方法引用(finaliser reference)

何时调用终结方法

  • 回收器发现对象不可达时立刻调用
  • 推迟到垃圾回收完成后进行

当回收器发现对象不可达时立即调用终结方法似乎是最简单的方法,但是这样的问题在于,回收的中间状态可能无法执行一般的用户代码,同时终结方法的耗时严重影响垃圾回收过程的停顿时间,过长的停顿时间是不可接受的。因此一般终结方法都推迟到垃圾回收完成后进行。

终结方法由哪个线程调用

一般的实现方式是在后台运行一个终结线程用于调用终结方法,终结线程与赋值器线程并发的执行,因此需要考虑终结方法在并发环境下的安全问题。

是否允许终结方法并发执行

通常情况下为了加速终结方法的执行,有的终结机制会开启多个线程来处理终结对象,因此终结方法之间并发执行的安全性也需要开发人员考虑。(注:根据R大的知乎回答,Hotspot jvm 在终结方法后台线程处理太慢的时候,会开启另一个线程进行加速)

是否允许终结方法访问待终结对象

很多情况下,终结方法都需要一个上下文来执行某些操作,而上下文就可能与待终结对象相关联。就如上面的文件对象例子来说,调用操作系统接口关闭文件需要传入对应文件的描述符,而文件描述符就存在文件对象中,终结方法需要从中取得这个文件描述符。

总的来说,允许终结方法访问待终结对象具有更高的灵活性。这同时也要求待终结对象不能再终结方法未得到执行的情况下被回收。因此回收器在一次回收过程中仅记录需要终结的不可达对象(比如将其放入一个队列中),而不对其进行回收。只有这些对象的终结方法执行完成后,回收器才会在执行完成后的下一个回收中将其回收。

但是这样一来,终结机制将会明显的延长待终结对象的寿命,在分代垃圾回收中,这些对象被提升至老年代的可能性将增加,这是一个需要考虑的问题

对象复活

终结方法能够访问待终结对象意味则终结方法可以将待终结对象的引用写入到一些全局的域中使其再次可达,这称为对象的复活。尽管对象能够复活,根据不同的终结机制实现,有的语言(比如Java)每个对象的终结方法仅执行一次,而有的语言却能够 允许复活对象的再次终结

终结操作是否需要遵循某种顺序

终结顺序与应用程序紧密相关。例如一个BufferedStream对象包装一个FileStream对象,BufferedStream对象终结方法用于将缓冲区中的数据向文件中写入,FileStream对象的终结方法用于关闭文件。如果FileStream对象的终结方法执行先于BufferedStream对象的终结方法,则当BufferedStream对象的终结方法执行时将会发现文件已经被关闭而发生错误。

如果需要保证终结方法的顺序执行,需要引入更加复杂的机制,但就目前而言,Java 并不保证终结顺序。

终结机制的总结

就Java而言,很多的文章和书都建议不要使用对象的终结方法。但是一些特殊情况下,特别是多线程环境下,我们可能确实需要使用对象终结方法。我们需要充分了解当前语言对于终结机制的具体实现,正确的实现终结方法,同时考虑其对垃圾回收带来的额外开销与影响。

弱引用

弱引用(weak reference)是一种具有特殊含义的引用,如果从根到某一对象的所有路径上都至少含有一个弱引用,则称该对象弱可达,弱可达的对象可以被垃圾回收器回收。

不同的指针强度

在强引用外,若引用可以泛化成多种不同强度的指针,强度之间具有相对顺序。我们做如下定义:

设A代表某一指针强度

  • 若从根到某一对象的所有路径上的指针强度均不小于A,这称该对象A*可达
  • 若从根到某一对象的所有路径上都存在至少一个强度为A的指针,且至少一个路径上不包含强度小于A的指针,则称该对象A可达。

在回收过程中。垃圾回收器在一定的条件下,通过将不同强度的指针置空,破坏对象的可达路径,从而达到回收对象的目的。例如,对于弱引用,如果某对象没有强引用指向,则将指向该对象的弱引用置空,使得该对象不可达而被回收。

JAVA中的引用类型

  • 强引用:普通的引用,回收器任何情况下都不会将其置空
  • 软引用:回收器根据当前的空间使用率决定是否置空软引用
  • 弱引用:某对象没有更强的引用指向,则将指向该对象的弱引用置空。
  • 终结方法引用:终结表到待终结对象的引用,不暴露给开发人员
  • 虚引用:最弱的一种引用,程序无法通过虚引用获取对象,为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时得到通知。Java中程序需要为虚引用指定一个引用队列,虚引用被回收时将会被加入到这个引用队列中以供程序取出做进一步处理。

支持多种不同强度的引用,需要在垃圾回收过程中引入额外的遍历。下面以复制式回收为视角,描述引用处理的过程:

  1. 从根开始进行追踪,对强可达的对象进行持续追踪与复制,同时找出被软,弱,虚引用直接引用的对象(但不对这些对象进行追踪)。
  2. 判断是否需要置空软引用,如果需要,直接置空软引用。如果不需要,持续追踪复制这些软引用直接引用的对象,同时在过程中找出新的弱,虚引用直接引用的对象(但不对这些对象进行追踪)。
  3. 处理弱引用,如果弱引用指向的对象已经被复制,则更新弱引用,否则置空弱引用。
  4. 在尚未复制的对象中,找出待终结的对象加入到终结队列中,并回到第一步以终结队列为根重复第一,第二,第三步。
  5. 处理虚引用,如果虚引用指向的对象未被复制,则将其加入指定的引用队列(reference queue)中,并对其进行追踪复制。

弱引用的通知机制

有些情况下,对于弱引用的置空,我们也希望一个通知机制。因此针对弱引用的处理也可以像虚引用一样,指定关联一个引用队列,当弱引用被置空时,将该引用加入到这个引用队列中。

使用虚引用控制终结顺序

由上述引用处理的顺序,我们可以得知,当一个虚引用被加入引用队列时,其终结方法一定已经被执行。我们可以通过这一结论,使用虚引用来控制对象的终结顺序。

如图所示,如果我们想要A的终结方法执行先于B的终结方法,可以按照图中所示使用一个继承于PhantomReference 的虚引用A’对像指向A,并且添加一个强引用域指向B。同时使用一个B’虚引用对象指向B。 当A不再被强引用时,垃圾回收时首先将A加入到终结队列中等待其终结方法的执行,此时由于A’对象仍然强引用B,B并不会被回收。A的终结方法执行完成后的一次垃圾回收中,虚引用对象A’将会被加入到引用队列中,程序从引用队列中取出A’并将指向B的强引用置空,如果B没有被任何强引用指向,则B将会被回收。

最后,我们很容易对该策略进行扩展,实现控制三个或者更多的对象终结方法顺序限制。(将B’扩展成类似A’一样的对象,持有一强引用指向另一个对象C,实现A->B->C的终结方法执行顺序)

终结方法顺序

需要考虑的问题

弱引用与终结机制将会引入额外的追踪过程,这些额外的追踪过程通常可以很快完成,因此不会造成性能问题。但是如果,应用中存在过多的弱引用(这里泛指所有非强引用的引用)或者待终结的对象,则这些开销是一个需要考虑的问题,通常在GC日志中可以看到针对引用处理的耗时,可以作为优化的依据。