不幸的是,您没有为这个问题分配任何语言标签,所以我假设您的语言是 Java。理解发生了什么的重要事情是回想一下 Java 支持嵌套的也就是内部类,它可以是static
和非static
. 此问题可能仅来自非static
内部类。同样重要的是,Java 中的所有匿名内部类都是 non- 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);
}
});
}
}
现在这在技术上不是内存泄漏,因为大约 24 小时后,scheduler
它将释放一个匿名实例,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 等工具和其他开发人员更清楚这确实是经过深思熟虑的,而不是偶然的。