CDI | 应用程序/从属范围| 内存泄漏 - javax.enterprise.inject.Instance<T> 未收集垃圾

Jam*_*mes 5 garbage-collection memory-leaks cdi apache-tomee jakarta-ee

我在 TomEE Java 应用程序中使用 Instance 作为惰性/动态注入器,并且我注意到我的应用程序中存在内存泄漏。这对我来说是第一次,所以看到 Java EE 库中概述的内存泄漏警告实际上令人惊讶:

package javax.enterprise.inject;

public interface Instance<T> extends Iterable<T>, Provider<T>
{
    /**
     * Destroy the given Contextual Instance.
     * This is especially intended for {@link javax.enterprise.context.Dependent} scoped beans
     * which might otherwise create mem leaks.
     * @param instance
     */
    public void destroy(T instance);
}
Run Code Online (Sandbox Code Playgroud)

现在这很可能是由与@ApplicationScoped和 的冲突引起的Instance<T>。我已经提供了一个示例,说明层在我的类中的位置。注意嵌套的Instance<T>. 这是为了提供任务的动态注入。

外层

@ApplicationScoped
public class MessageListenerImpl implements MessageListener {

    @Resource(name="example.mes")
    private ManagedExecutorService mes;

    @Inject @Any
    private Instance<Worker<ExampleObject>> workerInstance;

    // ...

    @Override
    public void onMessage(Message message) {
        ExampleObject eo = new ExampleObject();
        Worker<ExampleObject> taskWorker = workerInstance.get();
        taskWorker.setObject(eo);
        mes.submit(taskWorker);
    }

    // ...
}
Run Code Online (Sandbox Code Playgroud)

内部类

public class Worker<T> implements Runnable {

    @Inject @Any
    private Instance<Task> taskInstance;

    @Setter
    private T object

    // ...

    @Override
    public void run() {
        Task t = taskInstance.get();
        t.setObject(object);
        t.doTask();
        // Instance destruction, manual cleanup tried here.
    }

    // ...

}
Run Code Online (Sandbox Code Playgroud)

界面

public interface Task<T> {
    void doTask();
    void setObject(T obj);
}
Run Code Online (Sandbox Code Playgroud)

有泄漏,而不调用类destroy(T instance)ExampleObjectWorker<T>和实施Task<T>。为了保持异步设计,我尝试Worker<T>在它的实例中传递实例(可能是个坏主意,但我还是尝试了),调用destroy(T instance)并设置ExampleObjectnull. 这清理了Task<T>实现和ExampleObject,但不是Worker<T>

我尝试的另一个测试是在MessageListenerImpl(即删除Worker<T>和使用Task<T>)内进行同步设计作为后备工作,调用destroy(T instance)清理。这仍然留下了泄漏,这让我相信它一定是与@ApplicationScoped和 的冲突Instance<T>

如果有一种方法可以在实现异步设计的同时不发生内存泄漏,请告诉我。非常感谢反馈。谢谢!

Nik*_*los 8

确实这是一个弱点Instance,它可能会泄漏。这篇文章有很好的解释。(正如下面来自 Siliarus 的评论中所强调的,这不是 的内在错误Instance,而是错误的使用/设计。

Worker没有声明范围,因此它是有@Dependent范围的。这意味着每次注射都会重新创建它。Instance.get()本质上是一个注入,因此每次调用 时都会创建一个新的依赖范围的对象get()

规范说当它们的“父”(意味着它们被注入的对象)被销毁时,依赖范围的对象被销毁;但是应用程序范围的 bean 与应用程序的生命周期一样长,使它们创建的所有依赖范围的 bean 保持活动状态。这就是内存泄漏。

要减轻链接文章中的描述,请执行以下操作:

  1. workerInstance.destroy(taskWorker)不再需要时立即调用taskWorker,最好在finally块内调用:

    @Override
    public void onMessage(Message message) {
        ExampleObject eo = new ExampleObject();
        Worker<ExampleObject> taskWorker;
        try {
            taskWorker = workerInstance.get();
            taskWorker.setObject(eo);
            mes.submit(taskWorker);
        }
        finally {
            workerInstance.destroy(taskWorker);
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    编辑:关于这个选项的一些额外想法:如果随着时间的推移,注入的 bean 的实现从@Dependent变为 eg 会发生@ApplicationScoped什么?如果destroy()没有明确删除调用,这不是毫无戒心的开发人员在正常重构中会做的事情,您最终将破坏“全局”资源。CDI 会小心地重新创建它,因此不会对应用程序造成功能损害。仍然打算仅实例化一次的资源将不断被销毁/重新创建,这可能具有非功能性(性能)影响。所以,在我看来,这个解决方案会导致客户端和实现之间不必要的耦合,我宁愿不去做。

  2. 如果您只使用Instance延迟加载,并且只有一个实例,您可能需要缓存它:

    ...
    private Worker<ExampleObject> worker;
    
    private Worker<ExampleObject> getWorker() {
        if( worker == null ) {
            // guard against multi-threaded access if environment is relevant - not shown here
            worker = workerInstance.get();
        }
        return worker;
    }
    
    ...
    
        Worker<ExampleObject> taskWorker = getWorker();
    
    ...
    
    Run Code Online (Sandbox Code Playgroud)
  3. 为您的 提供范围Worker,以便其父级不再负责其生命周期,而是相关范围。