EffectiveJava3 item8:避免使用回收器和清理器

我的看法

优先使用try-with-resource来对重要资源使用完毕之后进行回收是一个好习惯,使用finalizers和cleaners是很危险的,因为:结果不确定而且性能损耗大。他两只能在有限的场景下考虑使用,但是也要特别注意这个不确定性和性能损失。

快速记忆

 

翻译

这里约定finalizers=回收器,cleaners=清理器。

回收器是不可预测的,通常是很危险的,并且没有必要。 使用可能带来不稳定的行为和更差的性能,并且可能传播或者转移这种问题。回收器有一些合法的使用场景,后面会在这条规则中包含,但是原则上,你应该避免使用它,对Java9来说,回收器已经不建议使用了,但是却仍然被java的库使用,Java9替换回收器(finalizers)的是清理器(cleaners) ,清理器比回收器危险性小,但是仍然不可预测,仍然很慢,仍然没有必要 。

C++程序员被告诫不要去思考销毁器,就像Java语言中模拟C++的一样的回收器和清理器,在C++中,销毁器是回收对象关联资源的常规方法,是跟构造器一样必要的组件,在java语言中,当对象变得不可达的时候,垃圾回收器(garbage collector)会回收对象相关的存储空间,程序员无需花费特别的关注。C++的销毁器也被用来回收另外的非内存资源。在java语言中,使用 try-with-resources 或者 try-finally 语句块也是为了回收非内存资源的目的。

回收器和清理器的一个缺点是无法保证他们被及时的执行。在对象变为不可达到的时候回收器或者清理器运行起来可能花费不可预计的时间;这个意味着 你必须不能在回收器和清理器中做任何时间敏感的事情 ,举个例子,依赖清理器或者回收器去关闭文件流是一个天大的错误,因为文件的描述符是有限的资源,如果大量的文件因为系统的拖拉或者延迟运行回收器和清理器导致大量被打开,一个程序会因为无法再打开文件而运行失败。

及时的执行回收和清理是垃圾回收算法的核心功能,这个功能实现方式不同对及时执行概率差别很大。依赖回收器和清理器的及时执行来影响程序的行为结果同样差别很大。一个程序在你的测试JVM上跑的很完美而在你的一个最重要的优质客户那里不幸的失败是完全可能的。延迟回收不仅仅是一个假设的问题,为一个类提供一个回收器很可能随时延迟它的实例的回收,一个同事调试一个长时间运行的GUI程序神秘的被一个内存泄漏弄死了,分析披露了程序的死亡时间,这个程序有上千个图形对象在回收器队列中等着被回收。很不幸的是,回收器线程的运行优先级比另外一个应用内的线程低,所以对象无法在有资格回收的节奏下被回收。Java语言明确无法保证哪个线程将要被回收器回收,所以,没有一个等同的方法来避免这类问题,还不如忍住不使用回收器。

考虑到作者可以控制自己的清理线程,清理器比回收器好一些。但是清理器仍然跑在后端,在垃圾回收器的掌控之下,所以也没有任何保证会被及时调用,不仅仅是Java语言规约无法保证及时执行回收和清理,也无法保证他们最后会不会执行。在一些不可达的对象上程序中断了并没有执行清理器和回收器是很有可能的。基于这个结果,你不应该依赖回收器和清理器去更新持久化的状态 ,举个例子:依赖回收器或者清理器去释放一个在共享资源(比如说数据库上的持续存在的锁)是一个很好的方法让你的整个分布式系统完全停止。

不要被System.gc()  和 System.runFinalization()  引诱,这可能会提高回收器和清理器执行的概率,但是并不一定保证。这两个方法一致保证声称:System.runfinalizersOnExit()  和它的恶魔双胞胎 Runtime.runFinalizersOnExit() , 这两个方法有致命的瑕疵,并且已经被废弃十年了。

回收器的另外一个问题是在回收期间引起的异常是被忽略的,并且对象的回收被终止。无法捕获的异常可能让其他的对象处于错误的状态。如果有另外一个线程尝试使用这样一个处于错误状态的对象,可能产生任意不确定的行为。通常,一个不被捕获的异常会中断线程并打印堆栈信息,但是前提是不是发生在回收器中,它甚至不会打印一个警告。清理器不会有这样的问题,因为使用清理器的库控制自己的线程。

使用回收器和清理器有严重的性能问题 ,在我的机器上,创建一个自动关闭(AutoCloseable)的独享,使用try-with-resource的方式,当它被垃圾回收的时候耗费12纳秒,使用回收器替换,时间增加到550纳秒,换句话来说,使用回收器来创建和回收对象慢了50倍,这主要是因为回收器减低了垃圾回收器的效率。

如果使用清理器来清理类的所有实例,清理器比回收器相比较来说更快一些(在我的机器上大概500纳秒一个实例),但是如果你在上文提到的安全的网络中使用清理器更快,在这些情况下,我的机器上,创建,清理,销毁一个对象耗费66纳秒,这意味着如果你不使用清理器你需要5倍(不是50倍)关注安全网络的的可靠性。

清理器有一个严重的安全问题,它大开你的类,遭受回收器的攻击 ,回收器攻击后面的原因也很简单:如果一个异常从构造函数或者序列化( readObject  和 readResolve 方法   )抛出,运行在这个部分实例化的对象的一个恶意子类的回收器会死在藤蔓上。 这个回收器会记录静态成员的对象引用,防止它被垃圾回收。当这个畸形的对象被记录下来,在第一个地方,用这个不允许存在的畸形对象调用任意方法是小事一桩。

构造函数中抛出一个异常防止对象存在是绰绰有余的,但是对于回收器来说不是这样的。 这样的攻击有可怕的后果,Final类可以免疫回收器攻击因为不能创建恶意子类,为了保护回收器攻击非Final类,写一个final的回收器方法没啥鸟用 ,所以,在一个类的对象资源需要中断,比如文件或者线程,你应该用什么东西去替代回收器和清理器呢?

让你的类实现AutoCloseable接口,让调用该类的客户端在不需要实例的时候调用close方法即可 , 通常也可以使用try-with-resource 来保证终止,即使在面对异常的场景。 另外一个值得提起的细节是实例必须关注实例是否被关闭掉了,close方法必须记录在成员变量上,对象不再有效,别的方法必须检查这个成员变量,当这个对象被关闭的时候去调用必须抛出 IllegalStateException 。

所以,有哪些场景适合回收器和清理器呢? 他们或许有两种合法的用处。

一个是位于一个安全网络,资源的拥有者疏忽了调用它的close方法,尽管没法保证回收器和清理器会不会及时的执行,稍后释放资源比如果客户端失败从来不释放要更好,如果你在考虑写一个这样的安全网络回收器,长远的想一想这个保护是否值得,有一些java的工具类,比如 FileInputStream,FileOutputStream,ThreadPoolExecutor,java.sql.Connection 为了安全网络的原因,有回收器。

另外一个合理使用清理器是考虑到本地同位对象,一个本地同位对象是一个(普通的对象通过本地方法委托)本地的非Java对象,垃圾收集器不知道它,当java同位对象回收的时候,它不会回收,假设性能是可以接受的,本地同位对象持有非关键资源,回收器或者清理器也许是这个任务【回收资源】的一个比较好的手段;如果性能无法接受,或者本地同位对象持有的资源必须及时的回收,如前描述,这个类必须有一个close方法。

清理器使用起来有一点困难,下面是一个Room类,展示了这个事实。让我们假设rooms必须被清理在他们回收之前,Room类实现了AutoCloseable接口,使用清理器自动清理安全网络的事实只是一个实现细节。不像回收器,清理器不会污染公共API;

 

// An autocloseable class using a cleaner as a safety net
public class Room implements AutoCloseable {
  private static final Cleaner cleaner = Cleaner.create();
/ / Resource that requires cleaning. Must not refer to Room! 
  private static class State implements Runnable {
    int numJunkPiles; // Number of junk piles in this room
    State(int numJunkPiles) {
      this.numJunkPiles = numJunkPiles;
    }
  // Invoked by close method or cleaner 
    @Override public void run() {
      System.out.println("Cleaning room");
      numJunkPiles = 0; 
    }
  }
  // The state of this room, shared with our cleanable 
  private final State state;
  // Our cleanable. Cleans the room when it’s eligible for gc 
  private final Cleaner.Cleanable cleanable;
  
  public Room(int numJunkPiles) {
      state = new State(numJunkPiles); 
      cleanable = cleaner.register(this, state);
    }
    @Override public void close() {
      cleanable.clean();
    }  
}

      

静态内部类State持有被cleaner清理房间的资源,在这个场景下,它是一个简单的numJunkPiiles成员,代表了房间的混乱度,更切合实际的是,它也许是一个final的long包含了一个本地同位对象的指针,State实现了Runable,它的run方法至多被调用一次,当我们在Room的构造函数中使用cleaner注册State实例,我们拿到Cleanable,run方法会被两个中的一个条件触发:通常被Room的close方法调用Cleanable的clean方法触发,如果客户端调用close方法的时候垃圾回收器有资格回收Room实例导致失败,清理器会调用State的run方法。

一个State实例没有引用Room实例这个很重要,如果存在引用,会创建一个循环,导致Room实例无法被垃圾收集器回收(并且别自动清理),所以,State必须是一个静态的内嵌类,因为非静态的内嵌类包含他们环绕实例的引用,使用lambda也是不明智的,因为他们可以很轻松的占据诶呦关闭对象的引用。

如前所说,Room的cleaner只在安全网络下使用,如果客户端使用try-with-resoure块包围Room实例,自动清理是用不上的,良好行为的客户端类似这样:

 

public class Adult {
  public static void main(String[] args) {
    try (Room myRoom = new Room(7)) {
      System.out.println("Goodbye");
    } 
  }
}

如你所期待的那样,运行Adult程序打印Goodbye,伴随着Cleaning Room ,但是病态行为从来不清理房间的程序应该是怎样的?

 

public class Teenager {
  public static void main(String[] args) {
    new Room(99);
    System.out.println("Peace out"); }
}

你也许会期待打印Peace out , 伴随着Cleaning room,但是在我的机器上,它从来不打印Cleaning Room , 只是结束,这是我们之前提到的不确定性,清理器专区提到:在System.exit期间清理器的行为随着实现的不同而不同。无法保证关联的清理动作是否会调用。尽管专区没有提到,常规程序结束的时候结果也是一样。添加一行 System.gc() 到Teenager的main方法足够是的它打印Cleaning room 在它结束之前,但是无法保证在你的机器上看到相同的行为结果。

总结:不要使用清理器,回收器,即使是在安全网络下中断不重要的本地资源,也要意识到不确定性和性能的影响。

  

原创不易,转载请注明出处,一起学习Effective java 3,提高代码质量,编程技能。欢迎一起讨论。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值