1. 首页
  2. IT资讯

一个JDK线程池BUG引发的GC机制思考

“u003Ch2 class=”pgc-h-arrow-right”u003E问题描述u003Cbru002Fu003Eu003Cu002Fh2u003Eu003Cpu003E前几天,在帮同事排查一个线上偶发的线程池错误u003Cu002Fpu003Eu003Cpu003E逻辑很简单,线程池执行了一个带结果的异步任务。但是最近有偶发的报错:u003Cu002Fpu003Eu003Cpreu003Eu003Ccodeu003Ejava.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]u003Cu002Fcodeu003Eu003Cu002Fpreu003Eu003Cpu003E本文中的模拟代码已经问题都是在HotSpot java8 (1.8.0_221)版本下模拟&出现的u003Cu002Fpu003Eu003Cpu003E下面是模拟代码,通过Executors.newSingleThreadExecutor创建一个单线程的线程池,然后在调用方获取Future的结果u003Cu002Fpu003Eu003Cdiv class=”pgc-img”u003Eu003Cimg src=”http:u002Fu002Fp1.pstatp.comu002Flargeu002Fpgc-imageu002Fb51b33419a434a4d9df21b7664cba665″ img_width=”675″ img_height=”482″ alt=”一个JDK线程池BUG引发的GC机制思考” inline=”0″u003Eu003Cp class=”pgc-img-caption”u003Eu003Cu002Fpu003Eu003Cu002Fdivu003Eu003Cdiv class=”pgc-img”u003Eu003Cimg src=”http:u002Fu002Fp3.pstatp.comu002Flargeu002Fpgc-imageu002F845ed2b9ee2a4c7f9b343525f7ed2d05″ img_width=”677″ img_height=”594″ alt=”一个JDK线程池BUG引发的GC机制思考” inline=”0″u003Eu003Cp class=”pgc-img-caption”u003Eu003Cu002Fpu003Eu003Cu002Fdivu003Eu003Ch2 class=”pgc-h-arrow-right”u003E分析&疑问u003Cu002Fh2u003Eu003Cpu003E第一个思考的问题是:线程池为什么关闭了,代码中并没有手动关闭的地方。看一下 Executors.newSingleThreadExecotor的源码实现:u003Cu002Fpu003Eu003Cdiv class=”pgc-img”u003Eu003Cimg src=”http:u002Fu002Fp3.pstatp.comu002Flargeu002Fpgc-imageu002F9942b882e0a0457cb180c76a0788342e” img_width=”730″ img_height=”142″ alt=”一个JDK线程池BUG引发的GC机制思考” inline=”0″u003Eu003Cp class=”pgc-img-caption”u003Eu003Cu002Fpu003Eu003Cu002Fdivu003Eu003Cpu003E这里创建的实际上是一个 FinalizableDelegatedExecutorService,这个包装类重写了 finalize函数,也就是说这个类会在被GC回收之前,先执行线程池的shutdown方法。u003Cu002Fpu003Eu003Cpu003E问题来了,u003Cstrongu003EGC只会回收不可达(unreachable)的对象u003Cu002Fstrongu003E,在 submit函数的栈帧未执行完出栈之前, executorService应该是可达的才对。u003Cu002Fpu003Eu003Cpu003E对于此问题,先抛出结论:u003Cu002Fpu003Eu003Cpu003Eu003Cstrongu003E当对象仍存在于作用域(stack frame)时, finalize也可能会被执行u003Cu002Fstrongu003Eu003Cu002Fpu003Eu003Cpu003Eoracle jdk文档中有一段关于finalize的介绍:u003Cu002Fpu003Eu003Cpu003Ehttps:u002Fu002Fdocs.oracle.comu002Fjavas…u003Cu002Fpu003Eu003Cblockquoteu003Eu003Cpu003EA reachable object is any object that can be accessed in any potential continuing computation from any live thread.u003Cu002Fpu003Eu003Cpu003EOptimizing transformations of a program can be designed that reduce the number of objects that are reachable to be less than those which would naively be considered reachable. For example, a Java compiler or code generator may choose to set a variable or parameter that will no longer be used to null to cause the storage for such an object to be potentially reclaimable sooner.u003Cu002Fpu003Eu003Cu002Fblockquoteu003Eu003Cpu003Eu003Cstrongu003E大概意思是:可达对象(reachable object)是可以从任何活动线程的任何潜在的持续访问中的任何对象;java编译器或代码生成器可能会对不再访问的对象提前置为null,使得对象可以被提前回收u003Cu002Fstrongu003Eu003Cu002Fpu003Eu003Cpu003E也就是说,在jvm的优化下,可能会出现对象不可达之后被提前置空并回收的情况u003Cu002Fpu003Eu003Cpu003E举个例子来验证一下(摘自https:u002Fu002Fstackoverflow.comu002Fquestionsu002F24376768u002Fcan-java-finalize-an-object-when-it-is-still-in-scope):u003Cu002Fpu003Eu003Cdiv class=”pgc-img”u003Eu003Cimg src=”http:u002Fu002Fp3.pstatp.comu002Flargeu002Fpgc-imageu002Ffae247ac37e3401aa6eb1e244fa24e67″ img_width=”731″ img_height=”434″ alt=”一个JDK线程池BUG引发的GC机制思考” inline=”0″u003Eu003Cp class=”pgc-img-caption”u003Eu003Cu002Fpu003Eu003Cu002Fdivu003Eu003Cpu003E从例子中可以看到,如果a在循环完成后已经不再使用了,则会出现先执行finalize的情况;虽然从对象作用域来说,方法没有执行完,栈帧并没有出栈,但是还是会被提前执行。u003Cu002Fpu003Eu003Cpu003E现在来增加一行代码,在最后一行打印对象a,让编译器u002F代码生成器认为后面有对象a的引用u003Cu002Fpu003Eu003Cdiv class=”pgc-img”u003Eu003Cimg src=”http:u002Fu002Fp3.pstatp.comu002Flargeu002Fpgc-imageu002F6e684f5d90864f50b2f7fdc90dc7bca9″ img_width=”729″ img_height=”161″ alt=”一个JDK线程池BUG引发的GC机制思考” inline=”0″u003Eu003Cp class=”pgc-img-caption”u003Eu003Cu002Fpu003Eu003Cu002Fdivu003Eu003Cpu003E从结果上看,finalize方法都没有执行(因为main方法执行完成后进程直接结束了),更不会出现提前finalize的问题了u003Cu002Fpu003Eu003Cpu003E基于上面的测试结果,再测试一种情况,在循环之前先将对象a置为null,并且在最后打印保持对象a的引用u003Cu002Fpu003Eu003Cdiv class=”pgc-img”u003Eu003Cimg src=”http:u002Fu002Fp1.pstatp.comu002Flargeu002Fpgc-imageu002F0dde02d162ac43849cc20e447619d847″ img_width=”731″ img_height=”330″ alt=”一个JDK线程池BUG引发的GC机制思考” inline=”0″u003Eu003Cp class=”pgc-img-caption”u003Eu003Cu002Fpu003Eu003Cu002Fdivu003Eu003Cpu003E从结果上看,手动置null的话也会导致对象被提前回收,虽然在最后还有引用,但此时引用的也是null了u003Cu002Fpu003Eu003Chru002Fu003Eu003Cpu003E现在再回到上面的线程池问题,根据上面介绍的机制,在分析没有引用之后,对象会被提前finalizeu003Cu002Fpu003Eu003Cpu003E可在上述代码中,return之前明明是有引用的 executorService.execute(futureTask),为什么也会提前finalize呢?u003Cu002Fpu003Eu003Cpu003E猜测可能是由于在execute方法中,会调用threadPoolExecutor,会创建并启动一个新线程,这时会发生一次主动的线程切换,导致在活动线程中对象不可达u003Cu002Fpu003Eu003Cpu003E结合上面Oracle Jdk文档中的描述“可达对象(reachable object)是可以从任何活动线程的任何潜在的持续访问中的任何对象”,可以认为可能是因为一次显示的线程切换,对象被认为不可达了,导致线程池被提前finalize了u003Cu002Fpu003Eu003Cpu003E下面来验证一下猜想:u003Cu002Fpu003Eu003Cdiv class=”pgc-img”u003Eu003Cimg src=”http:u002Fu002Fp3.pstatp.comu002Flargeu002Fpgc-imageu002Fb3a19ce375a34ae3b99f1c9ef272127e” img_width=”675″ img_height=”598″ alt=”一个JDK线程池BUG引发的GC机制思考” inline=”0″u003Eu003Cp class=”pgc-img-caption”u003Eu003Cu002Fpu003Eu003Cu002Fdivu003Eu003Cdiv class=”pgc-img”u003Eu003Cimg src=”http:u002Fu002Fp1.pstatp.comu002Flargeu002Fpgc-imageu002F33931c2fa99b4cb1b88d49482f63b993″ img_width=”676″ img_height=”632″ alt=”一个JDK线程池BUG引发的GC机制思考” inline=”0″u003Eu003Cp class=”pgc-img-caption”u003Eu003Cu002Fpu003Eu003Cu002Fdivu003Eu003Cdiv class=”pgc-img”u003Eu003Cimg src=”http:u002Fu002Fp9.pstatp.comu002Flargeu002Fpgc-imageu002Ffe8641c52f914c728d297c96209ebd5c” img_width=”674″ img_height=”644″ alt=”一个JDK线程池BUG引发的GC机制思考” inline=”0″u003Eu003Cp class=”pgc-img-caption”u003Eu003Cu002Fpu003Eu003Cu002Fdivu003Eu003Cdiv class=”pgc-img”u003Eu003Cimg src=”http:u002Fu002Fp3.pstatp.comu002Flargeu002Fpgc-imageu002F0da83ba5eb2c42a8965bd72a4c704933″ img_width=”582″ img_height=”184″ alt=”一个JDK线程池BUG引发的GC机制思考” inline=”0″u003Eu003Cp class=”pgc-img-caption”u003Eu003Cu002Fpu003Eu003Cu002Fdivu003Eu003Cpu003E执行若干时间后报错:u003Cu002Fpu003Eu003Cpreu003Eu003Ccodeu003EExceptioninthread"Thread-1"java.lang.RuntimeException: reject!!![true]u003Cu002Fcodeu003Eu003Cu002Fpreu003Eu003Cpu003E从错误上来看,“线程池”同样被提前shutdown了,那么一定是由于新建线程导致的吗?u003Cu002Fpu003Eu003Cpu003E下面将新建线程修改为 Thread.sleep测试一下:u003Cu002Fpu003Eu003Cdiv class=”pgc-img”u003Eu003Cimg src=”http:u002Fu002Fp3.pstatp.comu002Flargeu002Fpgc-imageu002Ffb6bac6c3df04b34913a8aa2c3fe5309″ img_width=”674″ img_height=”333″ alt=”一个JDK线程池BUG引发的GC机制思考” inline=”0″u003Eu003Cp class=”pgc-img-caption”u003Eu003Cu002Fpu003Eu003Cu002Fdivu003Eu003Cpu003E执行结果一样是报错u003Cu002Fpu003Eu003Cpreu003Eu003Ccodeu003EExceptioninthread"Thread-3"java.lang.RuntimeException: reject!!![true]u003Cu002Fcodeu003Eu003Cu002Fpreu003Eu003Cpu003Eu003Cstrongu003E由此可得,如果在执行的过程中,发生一次显式的线程切换,则会让编译器u002F代码生成器认为外层包装对象不可达u003Cu002Fstrongu003Eu003Cu002Fpu003Eu003Ch2 class=”pgc-h-arrow-right”u003E总结u003Cu002Fh2u003Eu003Cpu003E虽然GC只会回收不可达GC ROOT的对象,但是在编译器(没有明确指出,也可能是JIT)u002F代码生成器的优化下,可能会出现对象提前置null,或者线程切换导致的“提前对象不可达”的情况。u003Cu002Fpu003Eu003Cpu003E所以如果想在finalize方法里做些事情的话,一定在最后显示的引用一下对象(toStringu002Fhashcode都可以),保持对象的可达性(reachable)u003Cu002Fpu003Eu003Cpu003E上面关于线程切换导致的对象不可达,没有官方文献的支持,只是个人一个测试结果,如有问题欢迎指出u003Cu002Fpu003Eu003Cpu003Eu003Cstrongu003E综上所述,这种回收机制并不是JDK的bug,而算是一个优化策略,提前回收而已;但 Executors.newSingleThreadExecutor的实现里通过finalize来自动关闭线程池的做法是有Bug的,在经过优化后可能会导致线程池的提前shutdown,从而导致异常。u003Cu002Fstrongu003Eu003Cu002Fpu003Eu003Cpu003E线程池的这个问题,在JDK的论坛里也是一个公开但未解决状态的问题https:u002Fu002Fbugs.openjdk.java.netu002Fbrowseu002FJDK-8145304。u003Cu002Fpu003Eu003Cpu003E不过在JDK11下,该问题已经被修复:u003Cu002Fpu003Eu003Cdiv class=”pgc-img”u003Eu003Cimg src=”http:u002Fu002Fp3.pstatp.comu002Flargeu002Fpgc-imageu002Fa7c9b2099a7d44229ba711013e9b84de” img_width=”676″ img_height=”132″ alt=”一个JDK线程池BUG引发的GC机制思考” inline=”0″u003Eu003Cp class=”pgc-img-caption”u003Eu003Cu002Fpu003Eu003Cu002Fdivu003E”

原文始发于:一个JDK线程池BUG引发的GC机制思考

主题测试文章,只做测试使用。发布者:乾奕,转转请注明出处:http://www.cxybcw.com/26268.html

联系我们

13687733322

在线咨询:点击这里给我发消息

邮件:1877088071@qq.com

工作时间:周一至周五,9:30-18:30,节假日休息

QR code