JDK8线程池BUG引发的思考(下)

简介: JDK8线程池BUG引发的思考(下)

finalize



我们结合《effective Java》中的第八条了解一下终结方法是什么,这里会介绍终结方法的各种使用方法和隐患,以及如果重写finalize()在GC中会产生什么变化。


什么是finalize?


  • finalizer在JAVA中意味着JVM进行GC之前执行对象终结生命的特殊方法。
  • 在Java中,finalizer被称为***finalize()***方法。
  • Java 中的 finalize()在垃圾回收确定不再有对对象的引用时执行。
  • finalize()并不是保证被调用的,所以不会出现因为内存清理的操作导致OOM。
  • 对于C++程序员来说,finalizer不能被认为是析构函数,在C++中析构函数用于回收一个对象占用资源,是构造器必须的对应产物。


什么对象会被finalize?(重点)


这里也是从R大的回答里总结过来的,真可谓听君一席话,胜读十年书。对于一个对象来说我们需要区分重写finalize()和不重写finalize(),如果不重写finalize(),其实对象的生命周期只有一次,也就是一旦GC对象是不会经过finalize()直接进行回收的,这和《深入理解JVM虚拟机》中是有出入的(书中介绍的是对象经历GC至少需要两次考验,其实不重写finalize()一次考验就挂了),但是如果重写了finalize(),那么此时对象从失去强引用到真正被GC回收会经历两次GC,重写过finalize()的方法的对象的时候虚拟机会对这个对象做特殊处理, 把他放入一个finalize()的低优先级特殊队列,在GC的时候如果通过队列判断当前对象是一个不可达对象,如果是则会进行回收的操作。


finalize()不应该做什么?


  1. 避免做任何时间紧迫的事情,比如关闭一个文件流,或者关闭数据库,由于这个方法的线程优先级队列十分低并且哪怕是显式调用也依赖于垃圾收集器的效率和是否执行,所以禁止用它做任何系统资源的操作。
  2. 避免使用终结方法来更新线程状态。(神总是看的很远)
  3. 不要依赖***System.gcSystem.runFinalization***方法,这两个方法都不能促使终结方法提前执行,另一个甚至已经被废弃了10多年了。
  4. ***System.runFinalizersOnExitRuntime.runFinalizersOnExit***已被弃用。


finalize()潜在问题



对象broken问题


finalizers的一个潜在严重问题在终结的时候如果抛出未被捕获的异常,该对象的总结过程也会一并终止,并且此时对象会进入broken状态,如果此时这个对象被另一个对象使用,会出现不确定的行为,正常情况下未被捕获的异常基本会被Jvm捕获最终强制终止线程,并且打印堆栈,但是如果异常在终结方法中则完全报不出错来。清除方法虽然没有问题,但是清除方法有一个和finalize一样的性能问题。


性能问题


另一个问题是终结方法和清除方法一样存在很严重的性能问题,经过测试发现使用jdk7的AutoCLoseable接口和try-catch-resources,比增加一个终结方法要快上50倍,虽然使用清除方法清除类的实例比终结方法要快一些,但是也是五十步笑百步的区别。

清除方法:


安全问题


最后终结方法有严重的安全问题,那就是终结方法攻击,如果一个异常在构造方法或它的序列化等价方法-readObject()和readResolve()方法抛出,虽然构造方法可以正常抛出异常并且提前结束线程的生命周期,但是对于终结方法并不是如此,终结方法可以在静态字段中记录对象的引用,防止被垃圾回收,同时一旦被记录异常,那么就可以调用任何原本不应该允许出现的方法,所以从构造器抛出异常虽然足以阻止对象存活,但是对于终结方法来说,这一点无法实现


为了解决这个问题,最终建议的方法是重写一个空的并且为final的终结方法。同时如果想要让类似文件或者数据库的资源自动回收,最好的方式是实现jdk7之后提供的autoClosed接口,然后使用try-catch-resources自动关闭资源,即使遇到异常也会自动回收对象。和终结的漏洞不同的是,使用autoclosed必须记录自己是否关闭,同时如果资源是在被回收之后调用的,则必须要检查这个标记,最终抛出java.lang.IllegalStateException异常。


使用finalize需要注意什么?


如果需要重写finalize需要注意下面的事项。

  • 如果子类重写了终结方法,则必须要使用superd调用父类的终结方法,因为终结链不会自动执行。
  • 如果用户忘记调用显式终止方法,终结器应记录警告。
  • 对于本地对等点(普通对象通过本地方法委托给的本地对象,而垃圾收集器不知道也无法回收它)。


finalize 的正确用法


finalize当然并不是完全一无是处,因为在java中确实有不少常见的类进行使用,所以有必要介绍一下他的正确方法,当然还是建议读者不要去触碰使用终结方法和避免使用清除对象Cleaner,即使终结方法能发挥一定作用,也很容易不正确的使用导致上面提到一些问题,有的问题甚至会导致对象被污染攻击,所以需要十分小心。下面来看一下终结方法有哪些用途:

  1. 终结的第一个用途,是作为资源释放的一个安全网,保证客户端在无法正确操作的情况下依然可以用一道安全网作为资源释放的兜底,但是需要考虑这样使用的代价,下面我们从fileInputStream类的终结方法看一下它是如何安全使用的。从下面的代码可以看到,如果当前被释放的资源不为Null并且不是System#in控制流的时候就释放资源。


/**
确保在不再引用此文件输入流时调用该文件输入流的 close 方法。
*/
protected void finalize() throws IOException {
      if ((fd != null) &&  (fd != FileDescriptor.in)) {
          /* 如果 fd 是共享的,则 FileDescriptor 中的引用
            * 将确保仅在以下情况下调用终结器
            * 这样做是安全的。 所有使用 fd 的引用都有
            * 变得无法访问。 我们可以调用 close()
            */
          close();
      }
  }


从个人的的角度来看,这样的使用方式一方面是由于JDK早期版本没有try-catch-resource导致某些异常情况下IO流无法正常关闭所以使用这样的写法,另一方面是考虑极端情况下依然需要释放资源的情况,所以重写finalize作为一张安全网释放IO资源。

  1. 终结的另一个用途是本地方法对等体,本地对等体指的是一个本地对象(非JAVA对象),普通方法通过本地方法委托给一个本地对象,因为本地方法是不受JVM控制所以当JAVA对象被回收的时候它并不会回收,所以如果本地方法没有关键资源并且性能足够被接受,就可以使用终结或者清除方法来回收这些对象。


本地对等体的解释和实际使用(机翻):

一个AWT组件通常是一个包含了对等体接口类型引用的组件类。这个引用指向本地对等体实现。  以java.awt.Label为例,它的对等体接口是LabelPeer。LabelPeer是平台无关的。  在不同平台上,AWT提供不同的对等体类来实现LabelPeer。在Windows上,对等体类是WlabelPeer,它调用JNI来实现label的功能。  这些JNI方法用C或C++编写。它们关联一个本地的label,真正的行为都在这里发生。  作为整体,AWT组件由AWT组件类和AWT对等体提供了一个全局公用的API给应用程序使用。  一个组件类和它的对等体接口是平台无关的。底层的对等体类和JNI代码是平台相关的。

下面是原文,英语水平还不错的可以尝试阅读一下:

(An AWT component is usually a component class which holds a reference with a peer interface type. This reference points to a native peer implementation.

Take java.awt.Label for example, its peer interface is LabelPeer.

LabelPeer is platform independent. On every platform, AWT provides different peer class which implements LabelPeer. On Windows, the peer class is WlabelPeer, which implement label functionalities by JNI calls.

These JNI methods are coded in C or C++. They do the actual work, interacting with a native label.

Let's look at the figure.

You can see that AWT components provide a universal public API to the application by AWT component class and AWT peers. A component class and its peer interface are identical across platform. Those underlying peer classes and JNI codes are different. )


值得一提的是,在《effective Java》这本书的最后部分,给到了一个正确使用的案例,但是在最后通过一个客户端的错误使用发现依然会导致各种奇怪的现象,这里也说明了finalize这个方法的不确定性,同时一旦重写了这个方法就要考量对于程序性能的影响,因为它的调用与否取决于GC的实现。


网络异常,图片无法展示
|


finalize()总结


总之不要使用终结器,除非将其用作安全网终止非关键本机资源。在极少数情况下,如果您确实使用终结器,请记住调用super.finalize()。最后如果使用终结器作为安全网,请记住从终结器中记录无效使用情况。

最后再提一句:JDK9已经将finalize废弃,但是为了兼容考虑依然还有类在使用。


总结


在这篇文章中笔者根据一篇文章总结了一些个人学习到的GC的细节知识,同时根据文章提到的知识点去回顾了关于方法内联优化以及终结方法的细节,从这篇文章看出一个简单的BUG就能牵扯出如此多的知识点,最后甚至涉及到了优化器和解释器的设计层面,当然如果读者不是从事JVM领域的研究或者涉及的人,其实只要简单知道优化器会干出一些正常逻辑下“不能理解”的事情即可,比如this局部变量表中的对象如果this引用没有被使用很容易被JIT给内联优化掉。


最后,希望这篇文章可以切实的帮到你,学习任何内容一定不要简单的复制粘贴形成惯性和固定思维,而是要广泛阅读和汇总思考不断的纠错和回顾,最后形成的观点才有可能的是正确的,毕竟就连周大神的JVM书籍里也有让读者会误解的知识点。


写在最后


又是一篇长文,新的一年里希望能给读者带来更多更有质量的文章。

相关文章
|
6月前
|
Java 调度 开发者
JDK 21中的虚拟线程:轻量级并发的新篇章
本文深入探讨了JDK 21中引入的虚拟线程(Virtual Threads)概念,分析了其背后的设计哲学,以及与传统线程模型的区别。文章还将讨论虚拟线程如何简化并发编程,提高资源利用率,并展示了一些使用虚拟线程进行开发的示例。
1073 4
|
6月前
|
缓存 安全 Java
JDK8线程池BUG引发的思考
JDK8线程池BUG引发的思考
152 0
|
6月前
|
存储 缓存 并行计算
【面试问题】JDK并发类库提供的线程池实现有哪些?
【1月更文挑战第27天】【面试问题】JDK并发类库提供的线程池实现有哪些?
|
1月前
|
Dubbo Java 应用服务中间件
剖析Tomcat线程池与JDK线程池的区别和联系!
剖析Tomcat线程池与JDK线程池的区别和联系!
105 0
剖析Tomcat线程池与JDK线程池的区别和联系!
|
25天前
|
监控 数据可视化 Java
如何使用JDK自带的监控工具JConsole来监控线程池的内存使用情况?
如何使用JDK自带的监控工具JConsole来监控线程池的内存使用情况?
|
2月前
|
监控 数据可视化 Java
使用JDK自带的监控工具JConsole来监控线程池的内存使用情况
使用JDK自带的监控工具JConsole来监控线程池的内存使用情况
|
3月前
|
缓存 Java 调度
【Java 并发秘籍】线程池大作战:揭秘 JDK 中的线程池家族!
【8月更文挑战第24天】Java的并发库提供多种线程池以应对不同的多线程编程需求。本文通过实例介绍了四种主要线程池:固定大小线程池、可缓存线程池、单一线程线程池及定时任务线程池。固定大小线程池通过预设线程数管理任务队列;可缓存线程池能根据需要动态调整线程数量;单一线程线程池确保任务顺序执行;定时任务线程池支持周期性或延时任务调度。了解并正确选用这些线程池有助于提高程序效率和资源利用率。
52 2
|
3月前
|
算法 Java
JDK版本特性问题之想控制 G1 垃圾回收器的并行工作线程数量,如何解决
JDK版本特性问题之想控制 G1 垃圾回收器的并行工作线程数量,如何解决
|
5月前
|
存储 网络协议 Java
【JDK21】详解虚拟线程
【JDK21】详解虚拟线程
188 0
|
6月前
|
安全 Java
【JDK 源码分析】HashMap 线程安全问题分析
【1月更文挑战第27天】【JDK 源码分析】HashMap 线程安全问题分析