垃圾收集似乎关闭了本地执行程序并导致 RejectedExecutionException

Posted

技术标签:

【中文标题】垃圾收集似乎关闭了本地执行程序并导致 RejectedExecutionException【英文标题】:Garbage collection seems to shutdown local executor and cause RejectedExecutionException 【发布时间】:2018-03-09 12:29:26 【问题描述】:

在间歇性头痛方面需要帮助。代码调用com.google.api.client.http.HttpRequest#executeAsync(),基本有如下逻辑,

  @Beta
  public Future<HttpResponse> executeAsync(Executor executor) 
    FutureTask<HttpResponse> future = new FutureTask<HttpResponse>(new Callable<HttpResponse>() 

      public HttpResponse call() throws Exception 
        return execute();
      
    );
    executor.execute(future);
    return future;
  

  @Beta
  public Future<HttpResponse> executeAsync() 
    return executeAsync(Executors.newSingleThreadExecutor());
  

调用有时会遇到java.util.concurrent.RejectedExecutionException,从日志看来,当这种情况发生时,它总是伴随着垃圾收集。以下是发生这种情况时的日志模式示例,

2017-09-26 11:04:56.039186 2017-09-26T11:04:56.012+0000: [GC pause (G1 Evacuation Pause) (young) 213M->50M(300M), 0.0262262 secs]
2017-09-26 11:04:56.048210 java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@71a0a39 rejected from java.util.concurrent.ThreadPoolExecutor@36c306aa[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0]
2017-09-26 11:04:56.048212      at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063) ~[?:1.8.0_141]
2017-09-26 11:04:56.048214      at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830) ~[?:1.8.0_141]
2017-09-26 11:04:56.048216      at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379) ~[?:1.8.0_141]
2017-09-26 11:04:56.048218      at java.util.concurrent.Executors$DelegatedExecutorService.execute(Executors.java:668) ~[?:1.8.0_141]
2017-09-26 11:04:56.048220      at com.google.api.client.http.HttpRequest.executeAsync(HttpRequest.java:1085) ~[google-http-client-1.21.0.jar:1.21.0]
2017-09-26 11:04:56.048222      at com.google.api.client.http.HttpRequest.executeAsync(HttpRequest.java:1099) ~[google-http-client-1.21.0.jar:1.21.0]

根据我的理解,GC 不应该清理这个执行程序,因为它还没有超出范围,但是从错误日志来看,这似乎是行为。

编辑:感谢您的快速回复。我可能需要澄清我也不能随意复制这个,否则我可能会有更多线索。代码通常运行良好,但这种错误很少发生,我们所拥有的只是我为症状粘贴的日志信息。

EDIT2:澄清我粘贴的代码不是我的。这是我正在使用的开源库google-http-java-client。所以我没有办法改变它。我当然可以自己创建一个长期的Executor 并使用第一种方法调用它,但我现在正试图了解问题所在。

【问题讨论】:

是什么关闭了您的Executor?如果您正在向数千个睡眠线程发送垃圾邮件,那么很可能会出现问题。 我无法使用此代码重现您的问题。 在 Executor 上调用了 shutdown() 之后,您是否以某种方式提交了新任务? @SotiriosDelimanolis,我可能是指生命周期,或者对不起,也许我只是不了解官方条款。我认为在这种情况下,一旦executeAsync() 完成,执行程序就可以免费供 GC 使用(在此执行程序的终结器中调用关闭)。但是当异常发生时,executor 仍在使用中,所以 GC 绝不应该触摸它并导致它关闭,对吗? @jwils 请注意这部分不是由我控制的,而是我正在使用的 google http 客户端。作为用户,我只是在我的代码中调用 executeAsync(),而使用的执行器完全在这个 HttpRequest 类中。 【参考方案1】:

Executors#newSingleThreadExecutor() 返回的ExecutorService 恰好是包裹在FinalizableDelegatedExecutorService 中的ThreadPoolExecutor,这是一个在finalization 上关闭包裹的ExecutorService 的实现细节。我们可以从您的日志中得知执行程序已关闭

[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0]

但是,为什么要最终确定对象?你说

根据我的理解,GC 不应该清理这个执行程序,因为它 还没有超出范围,但是从错误日志来看 似乎是这种行为。

这是一个常见的误解。 Scope 是一个编译时特性,它决定了在哪里可以使用名称来引用源代码中的某些实体。它与运行时垃圾收集无关。

垃圾收集(和终结)由对象的可达性控制。 The Java Language Specification states

当一个对象不再被引用时,它可能会被 垃圾收集器。如果一个对象声明了一个终结器,那么终结器是 在对象被回收之前执行以使对象最后 清理原本不会被释放的资源的机会。 当不再需要某个类时,它可能会被卸载。

JVM 不会垃圾收集 reachable 对象

可达对象是可以在任何潜在的持续计算中从任何活动线程访问的任何对象。

正如该问题的已接受答案(由 Oracle 开发人员)中所述

finalize() called on strongly reachable object in Java 8

可达性分析允许

一个对象被最终确定和垃圾收集,即使有 在栈上的局部变量中引用它

您所看到的是 JVM 做出决定,即通过来自活动线程的持续计算并最终确定它不再可以访问 FinalizableDelegatedExecutorService(及其 ThreadPoolExecutor)。该操作会关闭执行程序,并在提交任务时抛出RejectedExecutionException

这是该实施的一个已知问题,bug JDK-8145304 被打开以讨论解决方案(本质上只是更好的文档)。

如果由我来决定,google-http-client 会更改他们的实现以使用未使用FinalizableDelegatedExecutorService 包裹的Executors.newFixedThreadPool(1)。 (我已经打开this issue 讨论图书馆的解决方案。)

我的建议是您创建自己的ExecutorService(可能使用newFixedThreadPool)并使用重载的executeAsync(Executor) 方法。


问题已修复,请参阅here。

【讨论】:

来找答案,得到答案,再加上小课。希望我能给你两个接受,谢谢! 还有this statement in the specification:“可以设计优化程序的转换,将可访问的对象数量减少到比那些天真地认为是可访问的要少。 […] 如果对象字段中的值存储在寄存器中,则会发生这种情况的另一个示例。然后程序可能会访问寄存器而不是对象,并且永远不会再次访问对象。这意味着该对象是垃圾。 […] 顺便说一句,如果我事先知道只有一个工作会被安排,我宁愿使用executeAsync(r -&gt; new Thread(r).start());,这意味着自动清理,没有提前完成的风险...... reachabilityFence 是抵消这些优化的另一种选择

以上是关于垃圾收集似乎关闭了本地执行程序并导致 RejectedExecutionException的主要内容,如果未能解决你的问题,请参考以下文章

是否在堆栈上分配了值类型的本地数组并立即收集垃圾? [复制]

Java垃圾回收机制是啥?

确定JVM垃圾收集器的运行时

垃圾收集器

垃圾收集与几种常用的垃圾收集算法

3.垃圾搜集器以及垃圾收集算法