很遗憾,您没有为这个问题分配任何语言标签,所以我假设您的语言是 Java。了解发生了什么的重要一点是要记住 Java 支持nested aka inner classes,它可以是static 和非static。此问题可能仅来自非static 内部类。同样重要的是,Java 中的所有匿名内部类都不是static,即使它们在技术上不需要。
考虑一些具有全局Scheduler 服务的大型应用程序,该服务可以延迟或重复运行ScheduledJobs。像这样的:
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 的实例捕获IntegrationModule 的this。这意味着即使没有对IntegrationModule 的直接引用,全局Scheduler 对象保留对ScheduledJob_IntegrationModule_Nested 实例的引用这一事实意味着IntegrationModule 及其所有字段也将永久有效地保留。这是纯粹的内存泄漏。
请注意,如果ScheduledJob_IntegrationModule_Nested 是IntegrationModule 的非匿名但非static 嵌套类,则情况相同。只有 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 并显式传递其中的所有依赖项。
在第二种情况下,我们实际上想从getDelay 调用readDelayConfiguration。这就是为什么匿名对象需要外部类的this,所以编译器为我们提供了它。您仍然可以将您的匿名类转换为名为 static 的类,并显式传递读取配置所需的所有依赖项,但该新类无论如何都必须保存所有这些依赖项,因此没有太大的好处。
生命周期
另一个重点是不同对象的生命周期。如果非静态内部类的生命周期完全位于其父对象的生命周期中,或者最多只是未来一点点,那么它们是绝对可以的。所以问题实际上是关于生命周期的差异。如果父对象具有无限生命周期(即自身全局保留),则一切正常。仅当这样的内部对象被短时间对象“泄漏”到具有扩展或可能无限生命周期的“外部世界”时,例如Scheduler 示例中,才会出现问题。我会说,在这种情况下,当这是预期的行为时,您应该使用命名的 static 内部类并显式传递外部 this 甚至可能编写一些 cmets 以使 YouKit 和其他工具更清楚开发人员认为这确实是经过深思熟虑的,而不是偶然的。