内部 Class 反向引用保留的对象是什么意思

What does it mean Objects Retained by Inner Class Back References

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 内部 class。同样重要的是,Java 中的所有匿名内部 class 都是非 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));
    }
}

所以现在匿名子实例class ScheduledJob 捕获 IntegrationModulethis。这意味着即使没有 IntegrationModule 的直接引用,全局 Scheduler 对象保留对 ScheduledJob_IntegrationModule_Nested 实例的引用这一事实意味着 IntegrationModule 与所有它的字段也将永远有效保留。这是纯粹的内存泄漏。

请注意,如果 ScheduledJob_IntegrationModule_Nested 是非匿名但非 static 嵌套 IntegrationModule 的 class,则情况相同。只有 static 嵌套的 classes 不会隐式捕获其 "owner" class.

的实例

如果您想象这是一个处理 HTTP 请求并且处理程序是有状态的 Web 应用程序,则示例会稍微复杂一些。所以有一些 "dispatcher" 分析传入的 HTTP 请求,然后创建一个适当的处理程序的实例并将工作委托给它。这实际上是许多网络框架中的典型方法。

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,包括 HttpRequestHttpResponse 等内容,即使从技术上讲,在主处理完成后不需要它们完成了。

对于 C#,情况类似,只是通常您会有一个 delegate 来捕获其父项而不是嵌套的 class.


更新:要做什么?

这是一个较少的硬事实区域,更多的是基于意见的区域。

什么是内存泄漏?

这里的第一个问题是什么是"memory leak"?我认为有两个不同但相互关联的方面:

  1. 内存泄漏是一种程序行为,表现为内存消耗稳定且可能无限增长。这是一件坏事,因为这会降低性能并可能最终导致内存不足的崩溃。

  2. 当某些内存区域(OOP 世界中的对象)保留的时间比开发人员预期的要长得多时,内存泄漏是程序的一种行为。

定义#1 中描述的不良行为通常是#2 中定义错误的结果。

怎么办内心是class邪恶的?

我认为 YourKit 警告您这些事情的原因是因为这种行为也可能是有意为之的,对于程序员来说通常是不明显的,因为反向引用是隐式生成的,您很容易忘记这一点。此外,Java 编译器不够智能,无法自行做出正确的决定,需要程序员通过明确指定(或避免)static 关键字来做出决定。由于没有地方可以为匿名内部 class 放置 static,因此即使他们真的不需要,它们也会捕获它们的父级。

要回答问题 "what to do about it?",您首先应该了解为什么编译器会生成 "back reference"。回到 IntegrationModule 示例,可能有两种不同的行为:

  1. 我们想从配置中读取 delay 一次并永远使用它(直到应用程序重新启动)
  2. 我们希望 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
        }
    }
}

所以你让你的匿名 class 命名和 static 并显式地传递里面的所有依赖项。

在第二种情况下,我们实际上想从 getDelay 调用 readDelayConfiguration。这就是为什么匿名对象需要外部 class 的 this,所以编译器为我们提供了它。您仍然可以将匿名 class 转换为命名 static 并显式传递读取配置所需的所有依赖项,但是新的 class 无论如何都必须保留所有依赖项,因此有没什么好处。

生命周期

另一个重点是不同对象的生命周期。如果非静态内部 classes 的生命周期完全位于其父对象的生命周期内,或者至多只是稍微晚一点,那么它们是绝对可以的。所以问题实际上是关于生命周期的差异。如果父对象具有无限生命周期(即自身全局保留),则一切正常。仅当这样的内部对象被短时间对象 "leaked" 进入具有延长或可能无限生命周期的 "outer world" 时,例如在 Scheduler 示例中,才会出现问题。我会说在这种情况下,当这是预期的行为时,您应该使用命名 static 内部 classes 并显式传递外部 this 甚至可能写一些注释来实现它对于像 YouKit 这样的工具和其他开发人员来说,这确实是经过深思熟虑的,而不是偶然的。