newbi使用事件探查器,我正在使用yourkit。
我在检查中看到可能的内存泄漏
Objects Retained by Inner Class Back References
Find objects retained via synthetic back reference of its inner classes.
Problem: Such objects are potential memory leaks.
这是什么意思 ?有人可以举一个这样的对象的好例子吗?为什么这可能被认为是泄漏?
谢谢
最佳答案
不幸的是,您没有为该问题分配任何语言标签,因此我假设您的语言是Java。了解发生的事情的重要事情是回想一下Java支持nested aka inner classes,它可以是static
,也可以是非static
。此问题可能仅来自非static
内部类。同样重要的是,Java中的所有匿名内部类都必须是非static
,即使它们在技术上不需要。
考虑一些具有全局Scheduler
服务的大型应用程序,该服务可以运行延迟的或重复的ScheduledJob
。像这样:
public interface ScheduledJob {
boolean isRepetitive();
long getDelay();
void runJob();
}
class Scheduler {
private final List<ScheduledJob> jobs = new ArrayList<>();
public void registerJob(ScheduledJob job) {
jobs.add(job);
}
public void runScheduler() {
// some logic to run all registered jobs
}
}
现在考虑您有一些插件系统和一个集成模块,该模块应该在每个配置的时间间隔运行一次作业,并且配置存储在数据库中。
public interface Module {
void register(Scheduler scheduler);
}
public class IntegrationModule implements Module {
private java.sql.Connection db;
private long readDelayConfiguration() {
// read data from DB
}
public void register(Scheduler scheduler) {
final long delay = readDelayConfiguration();
scheduler.registerJob(new ScheduledJob() {
@Override
public boolean isRepetitive() {
return true;
}
@Override
public long getDelay() {
return delay;
}
@Override
public void runJob() {
// do some integration stuff
}
});
}
}
这段代码实际上被编译成这样的东西:
class ScheduledJob_IntegrationModule_Nested implements ScheduledJob {
private final IntegrationModule outerThis;
private final long delay;
public ScheduledJob_IntegrationModule_Nested(IntegrationModule outerThis, long delay) {
this.outerThis = outerThis;
this.delay = delay;
}
@Override
public boolean isRepetitive() {
return true;
}
@Override
public long getDelay() {
return delay;
}
@Override
public void runJob() {
// do some integration stuff
}
}
public class IntegrationModule implements Module {
// some other stuff
...
public void register(Scheduler scheduler) {
final long delay = readDelayConfiguration();
scheduler.registerJob(new ScheduledJob_IntegrationModule_Nested(this, delay));
}
}
因此,现在匿名子类
ScheduledJob
的实例捕获了this
的IntegrationModule
。这意味着即使没有IntegrationModule
的直接引用,全局Scheduler
对象仍保留对ScheduledJob_IntegrationModule_Nested
实例的引用这一事实意味着IntegrationModule
及其所有字段也将被永久有效地保留。这是纯粹的内存泄漏。请注意,如果
ScheduledJob_IntegrationModule_Nested
是static
的非匿名但非IntegrationModule
嵌套类,情况将相同。仅static
嵌套类不会隐式捕获其“所有者”类的实例。如果您想象这是一个处理HTTP请求且处理程序是有状态的Web应用程序,则示例会更加复杂。因此,有一些“调度程序”可以分析传入的HTTP请求,然后创建适当处理程序的实例,并将作业委派给该处理程序。实际上,这是许多Web框架中的典型方法。
public abstract class StatefulRequestProcessor {
protected final Scheduler scheduler;
protected final HttpRequest request;
public StatefulRequestProcessor(Scheduler scheduler, HttpRequest request) {
this.scheduler = scheduler;
this.request = request;
}
public abstract void process();
}
现在假设对于某种传入请求,存在一些延迟清除
public class MyStatefulRequestProcessor extends StatefulRequestProcessor {
public MyStatefulRequestProcessor(Scheduler scheduler, HttpRequest request) {
super(scheduler, request);
}
@Override
public void process() {
// do some processing and finally get some stored ID
...
final long id = ...
// register a clean up of that ID
scheduler.registerJob(new ScheduledJob() {
@Override
public boolean isRepetitive() {
return false;
}
@Override
public long getDelay() {
return 24 * 60 * 60 * 1000L; // one day later
}
@Override
public void runJob() {
// do some clean up
cleanUp(id);
}
});
}
}
现在从技术上讲这不是内存泄漏,因为
scheduler
大约24小时后将释放匿名ScheduledJob
的实例,因此MyStatefulRequestProcessor
也将可用于垃圾回收。但是,这意味着在这24小时内,您必须在内存中存储整个MyStatefulRequestProcessor
,包括HttpRequest
,HttpResponse
等内容,即使从技术上讲,在完成主处理后不需要它们。对于C#,情况是类似的,除了通常情况下,您将拥有一个
delegate
来捕获其父级而不是嵌套类。更新:该怎么办?
这不是硬事实领域,而是更多基于意见的领域。
什么是内存泄漏?
这里的第一个问题是什么是“内存泄漏”?我认为有两个不同但相互联系的方面:
内存泄漏是程序的行为,表现为内存消耗稳定且可能无限增长。这是一件坏事,因为这会降低性能并最终导致内存不足崩溃。
当某些内存区域(OOP世界中的对象)的保留时间比开发人员预期的要长得多时,内存泄漏就是程序的行为。
定义#1中描述的不良行为通常是#2中定义的错误的结果。
该做什么,内部阶级是邪恶的吗?
我认为YourKit警告您有关此类事情的原因是,对于程序员来说,这种行为通常也是显而易见的,因为反向引用是隐式生成的,您可以轻松地将其遗忘。而且,Java编译器不够聪明,无法自行做出正确的决定,并且要求程序员通过显式指定(或避免使用)
static
关键字来做出决定。而且由于没有地方可以将static
用作匿名内部类,因此即使它们确实不需要,它们都可以捕获其父级。回答问题“该怎么办?”您首先应该了解为什么编译器会生成该“反向引用”。回到
IntegrationModule
示例,可能有两种不同的行为:我们想从配置中读取
delay
一次并永久使用它(直到应用程序重新启动)我们希望通过编辑配置即时调整
delay
(即无需重新启动)。在第一种情况下,您可以将代码重写为
public class IntegrationModule implements Module {
// some other stuff
...
public void register(Scheduler scheduler) {
final long delay = readDelayConfiguration();
scheduler.registerJob(new ScheduledJob_IntegrationModule_Nested(this, delay));
}
static class IntegrationScheduledJob implements ScheduledJob {
private final long delay;
public IntegrationScheduledJob(long delay) {
this.delay = delay;
}
@Override
public boolean isRepetitive() {
return true;
}
@Override
public long getDelay() {
return delay;
}
@Override
public void runJob() {
// do some integration stuff
}
}
}
因此,您将您的匿名类命名为和
static
,并在其中显式传递所有依赖项。在第二种情况下,我们实际上想从
readDelayConfiguration
调用getDelay
。这就是为什么匿名对象需要外部类的this
的原因,因此编译器为我们提供了它。您仍然可以将匿名类转换为名为static
的类,并显式传递读取配置所需的所有依赖关系,但是新类仍然必须保留所有这些依赖关系,因此没有太多好处。生命周期
另一个要点是不同对象的生命周期。如果非静态内部类的生命周期完全位于其父对象的生命周期之内,或者至多只是将来一点,则绝对可以。因此,问题实际上是生命周期的差异。如果父对象具有无限的生命周期(即全局保留),则一切正常。仅当这样的内部对象被短时对象“泄漏”到具有扩展的或可能的无限生命周期的“外部世界”(例如在
Scheduler
示例中)时,才可能出现问题。我想说,在这种情况下,当这是预期的行为时,您应该使用命名的static
内部类并显式传递外部this
,甚至可能写一些注释以使对诸如YouKit和类似工具的工具更加清楚开发人员认为这确实是经过深思熟虑的,并非偶然。
关于java - 内部类反向引用保留的对象是什么意思,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48172473/