JDK8线程池BUG引发的思考
引言
某一天逛网上帖子的时候,突然发现了下面这一篇文章,但是着实没有想到一篇文章能牵扯出这么多东西,这篇文章介绍的是由于使用了JDK的线程池引发的一个BUG,牵扯到了GC和方法内联的优化对于代码运行产生的影响,线程池BUG在JDK8中就已经存在但是直到JDK11才被修复,这里在原文的基础上补充相关的知识点,算是给自己做一个笔记。
原文:一个JDK线程池BUG引发的GC机制思考
知识点总结:
这里先说明一下这篇文章的相关知识点直接进行一个总结,如果读者对于相关内容十分熟悉的话这里也不浪费各位的时间,可以直接关闭本文了(哈哈)
- jdk并发线程设计中存在的BUG,关于Executors.newSingleThreadExecutor 的实现细节上的问题讨论。
- finalize() 终结器的介绍,以及终结器对于GC的影响,这里用《effective Java》中的第八条总结了一波。
- JVM中的JIT内联方法优化可能会导致对象的生命周期可能并不能坚持到一个栈帧出栈,这也导致了
Executors.newSingleThreadExecutor
中通过finalize()方式回收资源导致线程池提前回收的BUG。 - JDK官方网站的关于
Executors.newSingleThreadExecutor().submit(runnable)
方法会抛出异常的讨论(参考下方资料第四条),但是直到JDK11才进行修复。
参考资料
下面这些参考资料都是十分优质,花不多的时间就能有很大的收获,特别是R大的回答,简直就是移动的百科全书,赞。
Java 中, 为什么一个对象的实例方法在执行完成之前其对象可以被 GC 回收?(必读)
Can java finalize an object when it is still in scope?
Executors.newSingleThreadExecutor().submit(runnable) throws RejectedExecutionException
JVM Anatomy Quark #8: Local Variable Reachability
Jdk 的并发线程设计中存在的BUG
这里有点同情写Executors.newSingleThreadExecutor();
这个方法的老哥了,网上的文章基本都要拿他写的代码来反复鞭尸(当然JDK官方错误使用finalize() 确实不应该),这里我也不客气也要来鞭尸一番,为了分析这些内容我们必须要了解源代码怎么写的。这里先给一下个人参考各种资料之后得出的结论:
Executors.newSingleThreadExecutor();
在Jdk1.8中存在较大的隐患,当线程调用执行完成的同时如果此时线程对象的this引用没有发挥作用的时候,此时JIT优化和方法内联会提前判定当前的对象已死,GC会立马将对象资源释放,导致偶发性的线程池调用失败抛出拒绝访问异常。- 当出现多线程切换的时候GC线程会把没有进的this引用的对象提前进行回收,通过方法内联的方式探知某个对象在方法内的“生命周期”,所以很有可能线程池还在工作。
- 当对象仍存在于作用域(stack frame)时,
finalize
也可能会被执行,本质上由于JIT优化以及方法中的对象生命周期并不是直到方法执行结束才会结束,而是有可能提前结束生命周期。 Executors.newSingleThreadExecutor
的实现里通过finalize来自动关闭线程池的做法是有Bug的,在经过优化后可能会导致线程池的提前shutdown从而导致异常。
下面我们一步步来解释这个BUG的来源,以及相关的知识点,最后我们再讲讲如何规避这个问题。
环境
JDK版本:代码异常是在 HotSpot java8 (1.8.0_221)
模拟情况中出现的(实际上直到jdk11才被正式修复)。
问题介绍
下面我们从原文直接介绍一下这个线程池的BUG带来的奇怪现象。
问题:线上偶发线程池的问题,线程池执行带有返回结果的任务,但是发现被拒绝执行。
ava.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@a5acd19 rejected from java.util.concurrent.ThreadPoolExecutor@30890a38[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0]
原因分析:线程池中的线程被提前回收,下面给出一段模拟线程池的操作代码,在模拟代码中虽然futureTask显然是在线程池里面,同时按照正常的理解思路线程池对象肯定是在栈帧中存活的,但是实际上对象却在方法执行的周期内直接被GC线程给回收了,导致了“拒绝访问”的BUG(也就是出现了线程池关了,内部的任务却还在执行的情况):
public class ThreadPoolTest {
public static void main(String[] args) {
final ThreadPoolTest threadPoolTest = new ThreadPoolTest();
// 创建8个线程
for (int i = 0; i < 8; i++) {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
// 获取一个任务
Future<String> future = threadPoolTest.submit();
try {
String s = future.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (Error e) {
e.printStackTrace();
}
}
}
}).start();
}
//子线程不停gc,模拟偶发的gc
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.gc();
}
}
}).start();
}
/**
* 异步执行任务
* @return
*/
public Future<String> submit() {
//关键点,通过Executors.newSingleThreadExecutor创建一个单线程的线程池
// PS:注意是单线程的线程池
ExecutorService executorService = Executors.newSingleThreadExecutor();
FutureTask<String> futureTask = new FutureTask(new Callable() {
@Override
public Object call() throws Exception {
Thread.sleep(50);
return System.currentTimeMillis() + "";
}
});
// 执行异步任务
executorService.execute(futureTask);
return futureTask;
}
}
个人的运行情况和原文不太一样,有时候是刚跑起来直接抛出异常,有时候在执行几秒之后才报错,所以这一段代码在不同电脑上呈现的效果是不太一样的,但是可以确定的是JDK的写法是存在BUG的。
JIT优化
对于上面的代码,我们要如何验证JIT编辑器提前结束对象生命周期?这里我们接着引申一下,这里摘录了Stackflow中的一篇文章,来验证一下JIT优化的导致对象提前结束生命周期的一个理解案例。这篇文章的老哥提了一个很有意思的问题,原文问的是Can java finalize an object when it is still in scope? 也就是当对象依然在栈帧里面对象会提前结束生命周期么?这里我们挑回答中给的一个测试代码进行分析。
public class FinalizeTest {
@Override
protected void finalize() {
System.out.println(this + " was finalized!");
}
public static void main(String[] args) {
FinalizeTest a = new FinalizeTest();
System.out.println("Created " + a);
for (int i = 0; i < 1_000_000_000; i++) {
if (i % 1_000_000 == 0)
System.gc();
}
// System.out.println(a + " was still alive.");
}/*运行结果:
不放开注释:
Created FinalizeTest@c4437c4
FinalizeTest@c4437c4 was finalized!
放开注释:
Created FinalizeTest@c4437c4
com.zxd.interview.cocurrent.FinalizeTest@c4437c4 was still alive.
*/
}
在上面这一段代码中,如果把最后一行注释,发现在进行GC的时候,虽然A这时候应该还是存在于main的栈帧中,可以看到如果不放开注释出现了很奇怪的现象那就是对象a被提前终止生命周期了,这也就导致和前文一样的现象,对象在方法块内提前结束了自己的生命周期,或者换个角度说由于垃圾收集线程的切换,此时发现a已经没有任何this引用被释放掉内存。当然如果我们加上被注释这段代码的效果就比较符合预期了,对象a的生命周期被保住了直到整个程序运行完成,这里就引出了一个结论:当对象仍存在于作用域(stack frame)时,finalize
也可能会被执行。
那么为什么会出现上面的奇怪现象呢?在原文中讨论的是toString()
方法的底层是否会延长对象的生命周期,其实这是一种和JIT优化对抗的处理方式,使用打印语句将a的生命周期延长到方法出栈,这样就不会出现for循环执行到一半对象a却提前“死掉”的情况了。在JIT的优化中,上面的代码中的对象A被认为是不可达对象所以被回收,这种优化和我们长久的编程习惯可能会背道而驰,作为编程人员来说我们总是希望对象的生命周期能坚持到方法完成,但是实际上JIT和方法内联会尽可能的回收不可达的对象,下面我们就来了解一下什么是方法内联。
内联优化
在结论中讲述了内联优化代码的情况,下面我们来看一下《深入理解JVM虚拟机》是如何介绍方法内联的,方法内联简单的理解就是我们常说的消灭方法的嵌套,尽量让代码“合并”到一个方法体中执行,这样做最直观的体现就是可以减少栈帧的入栈出栈操作,我们都知道虽然程序计数器不会内存溢出,但是虚拟机栈的大小是有限的,并且在JDK5之后每一个线程具备的虚拟机栈大小默认为1M,显然减少入栈和出栈的次数是一种“积少成多”的优化方式,也是能直观并且显著的提升效率的一种优化手段。
为了更好理解方法内联,这里我们举一个案例,下面的方法是有可能进行方法内联的:
public int add(int a, int b , int c, int d){
return add(a, b) + add(c, d);
}
public int add(int a, int b){
return a + b;
}
值得注意的是只有使用invokespecial指令调用的私有方法、实例构造器、父类方法和使用invokestatic
指令调用的静态方法才会被方法内联,也就是说如果可以话我们还是尽量把方法设置为private
、 static
、 final
,特别是静态方法可以直接内联到一个代码块。除此之外大部分的实例方法都是无法被内联的,因为他设计的是分派和多态的选择,并且由于java是一门面向对象的语言,最基本的方法函数就是虚方法,所以对虚方法的内联是一个难题。
小贴士:
非虚方法:如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不变的,这样的方法称为非虚方法。
虚方法:静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。
顺带一提是方法内联绝对不是在代码中完成的,其实仔细想想也是可以理解,如果在代码里面完成方法的合并那么原有的逻辑就乱套了,所以为了解决上面这一系列问题Java的虚拟机首先引入叫做类型继承关系分析(class Hierarchy Analysis CHA)技术,个人理解这种优化方式为“富贵险中求”,主要是分析某个类的继承树以及重写或者重写方法的信息,如果发现是非虚的方法,直接内联就可以了,但是如果是虚方法,则对于当前的方法进行检查,如果检查到“可能”只有一个版本尼玛就可以假设这一段代码就是它最终完成的样子,这种方法也被叫做“守护内联”,当然由于Java动态连接的特性还有代理等情况,所以这种守护内联的方式最终是要留下逃生门的,一旦这样的激进优化出现失败或者异常,则需要马上切回到纯解析的模式进行工作。
吐槽:这种优化方式有点像考试作弊,老师没有发现就能一直瞄一直抄,效率提升200%,但是一旦被老师发现,哼哼,成绩不仅全部作废,你还要单独安排到一个教室全部重考!所以作弊是富贵险中求呀。
当然这种激进优化一旦出问题并不是马上就放弃优化,这时候还有一个技术叫做“内联缓存”,内联缓存 大致的工作原理如下:
- 未发生方法调用,内联缓存为空。
- 第一次调用,记录方法接受者的版本信息到缓存中,后续的每次调用如果接受的版本都是一样的,这时候会使用单态内联缓存,通过缓存的方式调用要比不内联的虚方法调用多一次类型判断。
- 但是如果版本信息不一致,一样要退化成超多态的内联缓存形式,开销相当于查找虚方法表的方法分派。
以上就是方法内联干的一些事情,既然了解了方法内联和JIT优化
简单分析newSingleThreadExecutor
虽然我们通过一系列的手段排查发现了一个GC中隐藏的“漏洞”,但是我们可以发现这其实归根结底是JDK代码的BUG导致了这一系列奇怪的问题产生,下面我们回过头来简单分析一下这个BUG,下面是JDK 源代码:
/**
* 创建一个执行器,它使用单个工作线程操作无界队列,
并在需要时使用提供的 ThreadFactory 创建一个新线程。
与其他等效的 {@code newFixedThreadPool(1, threadFactory)} 不同,
返回的执行程序保证不能重新配置以使用其他线程。
*
* @param threadFactory 创建新工厂时使用的工厂
* 线程
*
* @return 新创建的单线程 Executor
* @throws NullPointerException 如果 threadFactory 为空
*/
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
这个方法的JavaDoc的描述如下(英语水平有限,直接把API文档拿来机翻了):
创建一个执行器,它使用单个工作线程操作无界队列,并在需要时使用提供的 ThreadFactory 创建一个新线程。 与其等效的 {@code newFixedThreadPool(1, threadFactory)} 不同,返回的实例并不能保证给其他的线程使用,其实从名字也可以看出,这里就是新建一个单线程的线程池。
这里可以看到FinalizableDelegatedExecutorService这个类重写了finalize方法,并且实际上内部调用的是一个包装器对象的终结方法,这样也就是一切奇怪现象的“罪魁祸首”了:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
static class FinalizableDelegatedExecutorService
extends DelegatedExecutorService {
FinalizableDelegatedExecutorService(ExecutorService executor) {
super(executor);
}
// 重写了 finalize 方法
protected void finalize() {
super.shutdown();
}
}
关于finalize的相关细节会在下文进行总结,这里稍安毋躁,在代码中可以看到newSingleThreadExecutor
返回的是一个包装类而不是本身,所以它是通过调用包装类的的顶级接口的super.shutdown();
进行资源关闭的,同时super.shutdown();
的动作是在终结方法里面完成,其实可以看到代码本身的意图是好的,让线程池在不再使用的情况下保证随着GC会进行回收。但是其实只要稍微了解finalize的同学就应该清楚,抛开JIT和方法内联不谈这种写法的代码本身执行结果就是“不确定”的,因为finalize执行取决于垃圾收集器的实现,同时他也不一定会调用(比如main线程刚执行完成就exits了),所以如果对象的生命周期和作用域控制不当就会在垃圾收集线程GC的时候出现this引用丢失提前回收问题(当然这里是多线程切换导致的GC)。
此时我们可以回顾一下之前原文中的测试代码,线程池在开启之后他看上去好像就没有在干活了(实际上内部有线程对象在执行任务),显然这里想要利用finalize()作为“安全网”进行线程资源的回收的手段有失偏颇,最后这个关于JDK的线程池BUG是在JDK11修复的,他的处理代码如下:
JUC Executors.FinalizableDelegatedExecutorService
public void execute(Runnable command) {
try {
e.execute(command);
} finally { reachabilityFence(this); }
}
提示:当然还有一种方法是在代码中手动执行一下关闭线程池,也可以规避JIT优化带来的奇怪现象。
如何规避?
如何解决上面的问题以及如何和JIT和方法内联对抗?以JDK9为界的两种方法(技巧),JDK官方给出这种解决办法也说明了目前情况下不会对于这种JIT优化做兜底处理,意思就是说不能让编译优化器去配合你的代码工作,而是要让你的代码可以符合预期行为,个人来看其实四个字:关我屁事。
- 在Java 9里只要加上一个reachabilityFence()调用就没事了
Reference.reachabilityFence(executor); // HERE
- JDK8以及之前的版本中则需要 手动调用的方式让对象不会因为线程切换this引用被GC误判为不可达:
executor.shutdown(); // HERE
其实通篇看下来发现最后好像有点实际技巧和理论的东西好像就这两个方法。NONONO,软件的领域有一句话说的好,叫做知其然知其所以然,如果我们以为的选择去知其然那么很有可能沦为“代码工具人”而不是一个会认真思考的程序员。
下面我们再挖掘一下终结方法的使用和细节,这些教条在《effective Java》这本神书里面介绍了,所以我们直接从书中的内容进行总结吧。
- 其实还有一种方式,就是使用
try - final
的方式进行处理,在学习基础的时候我们都知道final语句是必然会执行的,也可以保证this引用直到final执行完成才被释放引用。
补充:阿里巴巴开发建议
其实阿里巴巴的手册很早之前就禁止使用Executors去创建线程,现在看来这里其实隐藏着另一个陷阱,那就是SingleThreadPool重写了finalize方法可能因为JIT优化和方法内联而进坑里面,也算是不使用的一个理由吧。
下面是手册的介绍,这里直接贴过来了:
线程池不允许使用 Executors 去创建,而是通过ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
1) FixedThreadPool 和 SingleThreadPool :允许的请求队列长度为 Integer.MAX_VALUE ,可能会堆积大量的请求,从而导致 OOM 。
2) CachedThreadPool 和 ScheduledThreadPool :允许的创建线程数量为 Integer.MAX_VALUE ,可能会创建大量的线程,从而导致 OOM
(感兴趣的同学可以翻翻手册的“并发”部分,在靠前的几条就有介绍(说明翻车的人还挺多?))
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()不应该做什么?
- 避免做任何时间紧迫的事情,比如关闭一个文件流,或者关闭数据库,由于这个方法的线程优先级队列十分低并且哪怕是显式调用也依赖于垃圾收集器的效率和是否执行,所以禁止用它做任何系统资源的操作。
- 避免使用终结方法来更新线程状态。(神总是看的很远)
- 不要依赖System.gc)和System.runFinalization)方法,这两个方法都不能促使终结方法提前执行,另一个甚至已经被废弃了10多年了。
- System.runFinalizersOnExit)和Runtime.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,即使终结方法能发挥一定作用,也很容易不正确的使用导致上面提到一些问题,有的问题甚至会导致对象被污染攻击,所以需要十分小心。下面来看一下终结方法有哪些用途:
- 终结的第一个用途,是作为资源释放的一个安全网,保证客户端在无法正确操作的情况下依然可以用一道安全网作为资源释放的兜底,但是需要考虑这样使用的代价,下面我们从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资源。
- 终结的另一个用途是本地方法对等体,本地对等体指的是一个本地对象(非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书籍里也有让读者会误解的知识点。
写在最后
又是一篇长文,新的一年里希望能给读者带来更多更有质量的文章。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。