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)
的ExampleObject
,Worker<T>
和实施Task<T>
。为了保持异步设计,我尝试Worker<T>
在它的实例中传递实例(可能是个坏主意,但我还是尝试了),调用destroy(T instance)
并设置ExampleObject
为null
. 这清理了Task<T>
实现和ExampleObject
,但不是Worker<T>
。
我尝试的另一个测试是在MessageListenerImpl
(即删除Worker<T>
和使用Task<T>
)内进行同步设计作为后备工作,调用destroy(T instance)
清理。这仍然留下了泄漏,这让我相信它一定是与@ApplicationScoped
和 的冲突Instance<T>
。
如果有一种方法可以在实现异步设计的同时不发生内存泄漏,请告诉我。非常感谢反馈。谢谢!
确实这是一个弱点Instance
,它可能会泄漏。这篇文章有很好的解释。(正如下面来自 Siliarus 的评论中所强调的,这不是 的内在错误Instance
,而是错误的使用/设计。)
您Worker
没有声明范围,因此它是有@Dependent
范围的。这意味着每次注射都会重新创建它。Instance.get()
本质上是一个注入,因此每次调用 时都会创建一个新的依赖范围的对象get()
。
规范说当它们的“父”(意味着它们被注入的对象)被销毁时,依赖范围的对象被销毁;但是应用程序范围的 bean 与应用程序的生命周期一样长,使它们创建的所有依赖范围的 bean 保持活动状态。这就是内存泄漏。
要减轻链接文章中的描述,请执行以下操作:
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 会小心地重新创建它,因此不会对应用程序造成功能损害。仍然打算仅实例化一次的资源将不断被销毁/重新创建,这可能具有非功能性(性能)影响。所以,在我看来,这个解决方案会导致客户端和实现之间不必要的耦合,我宁愿不去做。
如果您只使用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)为您的 提供范围Worker
,以便其父级不再负责其生命周期,而是相关范围。