jvm 转存 自动_JVM的自动资源管理功能

jvm 转存 自动

重要要点

  • 了解C ++的RAII模式和Java终结处理之间的区别
  • 深入研究Hotspot的源代码,并了解如何注册终结器
  • 比较finalize()与Java 7 try-with-resources
  • 查看如何在字节码中实现TWR
  • 了解TWR为什么优于finalize()

本文中的材料经过Ben Evans和James Gough即将出版的“ Optimizing Java”一书的许可而改编。 该书由O'Reilly出版,现在可以从O'Reilly和亚马逊的Early Release中获得。

InfoQ最近报告了对Object类型的finalize()方法的建议弃用。 从Java 1.0开始就存在此方法,但是该方法被广泛认为是功能不完善,并且是平台中的重要遗留物。 但是,弃用存在于Java对象类型上的方法将是非常不寻常的步骤。

背景

finalize()机制是一种尝试提供自动资源管理的方式,类似于通过C ++和类似语言编写的RAII( 资源获取即初始化 )模式。 在这种模式下,提供了一个析构函数方法(在Java中称为finalize()),以实现自动清除和在对象被销毁时释放资源。

这样做的基本用例非常简单-创建对象时,它获取某些资源的所有权,并且该资源的对象所有权在对象的生存期内一直存在。 然后,当对象死亡时,资源的所有权将自动放弃。

让我们看一个简单的C ++简单示例,该示例演示如何在C样式文件I / O周围放置RAII包装。 该技术的核心是使用对象析构方法(在与该类相同的方法的开头以〜表示)用于清除:

class file_error {};

class file {
  public:
     file(const char* filename) : _h_file(std::fopen(filename, "w+")) {
         if (_h_file == NULL) {
             throw file_error();
         }
     }

     // Destructor
     ~file() { std::fclose(_h_file); }

     void write(const char* str) {
         if (std::fputs(str, _h_file) == EOF) {
             throw file_error();
         }
     }

     void write(const char* buffer, std::size_t numc) {
         if (numc != 0 && std::fwrite(buffer, numc, 1, _h_file) == 0) {
             throw file_error() ;
         }
     }

 private:
     std::FILE* _h_file;
};

这种方法的标准原理是观察到,当程序员打开文件句柄时,很容易忘记不再需要它时调用close()函数,因此将资源所有权与对象生存期联系起来是有意义的。 然后,自动摆脱对象的资源将成为平台而不是程序员的责任。

这将促进良好的设计,尤其是当存在类型的唯一原因是充当诸如文件或网络套接字之类的资源的“持有人”时。

在Java世界中,实现此方法的方法是使用JVM的垃圾收集器作为子系统,可以明确地说该对象已死亡。 如果在类型上提供了finalize()方法,则该类型的所有对象都会受到特殊处理。 覆盖finalize()的对象将由垃圾回收器专门处理。

注意

从Object。<init>(指定类型的最终超类构造函数)成功返回后,JVM通过在对象上运行特殊处理程序来注册可终结对象。

我们需要注意的Hotspot的一个细节是,除了标准Java指令之外,VM还具有一些特定于实现的特殊字节码。 这些专业字节码用于重写标准的字节码,以应对某些特殊情况。

可以在此处找到字节码定义的完整列表,包括标准Java和Hotspot特例。

对于我们的目的,我们关心特殊情况:return_register_finalizer指令。 这是必需的,因为JVMTI可以重写Object。<init>的字节码。 为了精确地遵守标准并在正确的时间注册终结器,有必要确定Object。<init>的完成点,而无需重写,并且使用特殊情况的字节码标记该点。

在Hotspot解释器中可以看到用于将对象实际注册为需要最终确定的代码。 文件hotspot / src / cpu / x86 / vm / c1_Runtime1_x86.cpp包含Hotspot解释器特定于x86端口的核心。 这必须是特定于处理器的,因为Hotspot大量使用了低级汇编/机器代码。 案例register_finalizer_id包含注册码。

一旦将对象注册为需要完成的对象,则该对象将经历以下延长的生命周期,而不是在垃圾回收(GC)周期内立即被回收:

  1. 终结对象由于事先注册而被识别。 它们被放置在特殊的完成队列中。
  2. 在GC之后应用程序线程重新启动之后,单独的终结线程将耗尽队列。
  3. 从队列中删除每个对象,并启动辅助终结线程以对此实例运行finalize()方法。
  4. 一旦finalize()方法终止,该对象将准备在下一个循环中进行实际收集。

总的来说,这意味着所有要终结的对象必须首先通过GC标记识别为不可访问,然后再终结,然后必须再次运行GC才能收集数据。 这意味着可终结对象至少保留一个额外的GC周期。 对于已使用权的对象,这可能会花费大量时间。

该机制具有一些额外的复杂性-超出了我们的期望-因为排空线程必须启动实际上运行finalize()方法的辅助终结线程。 这对于防止finalize()阻塞的可能性很有必要。

如果finalize()在排空线程上运行,则写得不好的finalize()可能会阻止整个机制正常工作。 为了避免这种情况,我们被迫为每个需要终结的对象实例创建一个全新的线程。

不仅如此,终结线程还必须忽略所有引发的异常。 乍看起来这很奇怪,但是终结线程没有真正的方法来处理异常,并且创建终结对象的原始上下文也早已消失。 没有任何有意义的方式可以提供任何可以感知异常或从异常中恢复的用户代码。

为了澄清这一点,请回想一下Java中的异常提供了一种展开堆栈的方法,以在当前执行线程中找到可以从非致命错误中恢复的方法。 从这个角度来看,finalization忽略异常的限制更容易理解-finalize()调用发生在与创建或执行对象的线程完全不同的线程上。

大部分的最终实现实际上是用Java编写的。 JVM具有单独的线程来执行终结处理,这些线程与应用程序线程同时运行以完成大部分所需的工作。 核心功能包含在类java.lang.ref.Finalizer中,这是一个非常易于阅读的包私有类。

Finalizer类还提供了一些有关如何向运行时授予附加特权的类授予该特权的见解。 例如,它包含如下代码:

/* Invoked by VM */
static void register(Object finalizee) {
    new Finalizer(finalizee);
}

当然,在常规应用程序代码中,此代码将是荒谬的,因为它会创建未使用的对象。 除非构造函数有副作用(在Java中通常被认为是错误的设计决策),否则这将无济于事。 在这种情况下,目的是“挂钩”新的可终结对象。

终结处理的实现也严重依赖于FinalReference类。 这是java.lang.ref.Reference的子类,该类是运行时和VM专门处理的类。 像更知名的软引用和弱引用一样,FinalReference对象也由GC子系统进行特殊处理,该机制包括一种机制,可在VM和Java代码(平台和用户)之间提供有趣的交互。

不足之处

就其所有技术兴趣而言,由于与平台的内存管理方案不匹配,Java终结处理实现存在致命缺陷。”

在C ++情况下,内存是手动处理的,并且在程序员的明确控制下对对象进行明确的生命周期管理。 这意味着删除对象时可能会发生破坏,因此资源的获取和释放与对象的生存期直接相关。

Java的内存管理子系统是一个垃圾收集器,可以根据需要运行,以响应可用内存不足而分配。 因此,它以不确定的间隔运行(如果有的话),因此finalize()方法仅在某个未知时间收集到对象时运行。

如果使用finalize()机制来自动释放资源(例如文件句柄),则无法保证这些资源何时(如果有)实际可用。 这使得finalize()机制从根本上不适合其既定目的-自动资源管理。

尝试资源

为了安全地处理资源拥有的对象,Java 7引入了try-with-resources-一种新的语法功能,专门用于自动处理资源。 这种语言级别的结构允许在try关键字后的括号中指定要管理的资源。

这必须是一个对象构造子句-不允许使用常规Java代码。 Java编译器还将检查所创建的对象的类型是否实现AutoCloseable接口(这是Java 7中专门为此目的引入的Closeable的超级接口)。

因此,资源对象在try块主体的范围内,并且在try块范围的末尾,将自动调用close()方法,而不是使开发人员记住调用该函数。 close()方法的调用的行为就像是在final中一样,因此即使在业务逻辑中引发异常,该调用也会运行。

注意

清理的自动部分实际上会产生比人类更好的代码-因为javac知道关闭它们之间具有依赖性的资源(例如JDBC Connection和相关类型)的顺序。 这意味着使用该机制的首选方法是使用try-with-resources,而不是旧的手动关闭。

关键在于,现在将局部变量的生存期限制为单个作用域,因此自动清除将绑定到作用域,而不是对象生存期。 例如:

public void readFirstLine(File file) throws IOException {
    try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
        String firstLine = reader.readLine();
        System.out.println(firstLine);
    }
}

这个无害的try-with-resources代码被编译成相当大量的字节码,我们可以通过将-p切换到javap来转储反编译的形式来看到。

public void readFirstLine(java.io.File) throws java.io.IOException;
    Code:
       0: new           #2  // class java/io/BufferedReader
       3: dup
       4: new           #3  // class java/io/FileReader
       7: dup
       8: aload_1
       9: invokespecial #4  // Method java/io/FileReader."<init>":(Ljava/io/File;)V
      12: invokespecial #5  // Method java/io/BufferedReader."<init>":(Ljava/io/Reader;)V
      15: astore_2
      16: aconst_null
      17: astore_3
      18: aload_2
      19: invokevirtual #6  // Method java/io/BufferedReader.readLine:()Ljava/lang/String;
      22: astore        4
      24: getstatic     #7  // Field java/lang/System.out:Ljava/io/PrintStream;
      27: aload         4
      29: invokevirtual #8  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      32: aload_2
      33: ifnull        108
      36: aload_3
      37: ifnull        58
      40: aload_2
      41: invokevirtual #9  // Method java/io/BufferedReader.close:()V
      44: goto          108
      47: astore        4
      49: aload_3
      50: aload         4
      52: invokevirtual #11 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
      55: goto          108
      58: aload_2
      59: invokevirtual #9  // Method java/io/BufferedReader.close:()V
      62: goto          108
      65: astore        4
      67: aload         4
      69: astore_3
      70: aload         4
      72: athrow
      73: astore        5
      75: aload_2
      76: ifnull        105
      79: aload_3
      80: ifnull        101
      83: aload_2
      84: invokevirtual #9  // Method java/io/BufferedReader.close:()V
      87: goto          105
      90: astore        6
      92: aload_3
      93: aload         6
      95: invokevirtual #11 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
      98: goto          105
     101: aload_2
     102: invokevirtual #9  // Method java/io/BufferedReader.close:()V
     105: aload         5
     107: athrow
     108: return
    Exception table:
       from    to  target type
          40    44    47   Class java/lang/Throwable
          18    32    65   Class java/lang/Throwable
          18    32    73   any
          83    87    90   Class java/lang/Throwable
          65    75    73   any

尽管具有相同的设计意图,但最终化和尝试资源却彼此根本不同。 终结处理依赖于解释器中深处的汇编代码来注册要终结的对象,并使用垃圾收集器使用参考队列和单独的专用终结处理线程来启动清理。 特别是,字节码中几乎没有任何机制的痕迹,并且该功能由VM内的特殊机制提供。

相比之下,try-with-resources是一种纯粹的编译时机制,可以看作是语法糖,它仅产生常规字节码,而没有其他特殊的运行时行为。 try-with-resources唯一可能的可见效果是,由于它发出大量自动字节码,因此可能会影响JIT编译器有效内联或编译使用它的方法的能力。 但是,这不是避免它的原因。

总而言之,对于资源管理以及几乎所有其他情况,最终确定都不适合目的。 最终确定依赖于GC,GC本身是一个不确定的过程,因此任何依赖最终确定的内容都无法保证何时释放资源。

无论是否最终淘汰最终导致将其删除,建议均保持不变。 永远不要编写重写finalize()的类,并始终重构您在自己的代码中找到的所有类。

建议使用资源尝试机制来实现类似于C ++ RAII模式的最佳实践。 它确实将模式的使用限制在块范围的代码中,但这是由于Java平台缺乏对对象生存期的低级可见性。 Java开发人员在处理资源对象时必须简单地遵守纪律,并尽可能地扩大它们的范围-这本身就是一种好的设计实践。

关于作者

是jClarity的联合创始人,该公司是一家提供性能工具和服务以帮助开发和运营团队的创业公司。 他是LJC(伦敦的JUG)的组织者和JCP执行委员会的成员,帮助定义Java生态系统的标准。 他是Java冠军。 3次JavaOne Rockstar扬声器; 是《 The Well-Grounded Java Developer》和《 Java in a Nutshell》新版的合著者,并且是Java平台,性能,并发性和相关主题的定期演讲者。 Ben可以进行口语,教学,写作和咨询服务-请联系以获取详细信息。

翻译自: https://www.infoq.com/articles/Finalize-Exiting-Java/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1

jvm 转存 自动

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值