【问题标题】:What does it mean Objects Retained by Inner Class Back References内部类反向引用保留的对象是什么意思
【发布时间】:2018-06-18 17:47:00
【问题描述】:

newbi 使用 profiler,我正在使用 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 scala memory-leaks profiling yourkit


    【解决方案1】:

    很遗憾,您没有为这个问题分配任何语言标签,所以我假设您的语言是 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 的实例捕获IntegrationModulethis。这意味着即使没有对IntegrationModule 的直接引用,全局Scheduler 对象保留对ScheduledJob_IntegrationModule_Nested 实例的引用这一事实意味着IntegrationModule 及其所有字段也将永久有效地保留。这是纯粹的内存泄漏。

    请注意,如果ScheduledJob_IntegrationModule_NestedIntegrationModule 的非匿名但非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 存储在您的内存中,包括 HttpRequestHttpResponse 等内容,即使在主要处理完成后技术上不需要它们。

    对于 C#,情况类似,但通常您会使用 delegate 捕获其父类而不是嵌套类。


    更新:怎么办?

    这不是一个硬事实领域,而是一个基于意见的领域。

    什么是内存泄漏?

    这里的第一个问题是什么是“内存泄漏”?我认为有两个不同但相互关联的方面:

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

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

    定义#1 中描述的不良行为通常是由于#2 中定义的错误造成的。

    该怎么做,内部类是邪恶的吗?

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

    要回答“该怎么办?”这个问题。您首先应该了解编译器为什么会生成“反向引用”。回到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
            }
        }
    }
    

    因此,您将匿名类命名为 static 并显式传递其中的所有依赖项。

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

    生命周期

    另一个重点是不同对象的生命周期。如果非静态内部类的生命周期完全位于其父对象的生命周期中,或者最多只是未来一点点,那么它们是绝对可以的。所以问题实际上是关于生命周期的差异。如果父对象具有无限生命周期(即自身全局保留),则一切正常。仅当这样的内部对象被短时间对象“泄漏”到具有扩展或可能无限生命周期的“外部世界”时,例如Scheduler 示例中,才会出现问题。我会说,在这种情况下,当这是预期的行为时,您应该使用命名的 static 内部类并显式传递外部 this 甚至可能编写一些 cmets 以使 YouKit 和其他工具更清楚开发人员认为这确实是经过深思熟虑的,而不是偶然的。

    【讨论】:

    • 非常感谢您的详细回答。我还添加了语言标签。你能补充一下应该如何正确完成吗?它应该以非匿名方式创建 ScheduledJob 吗?创建 ScheduledJob 的问题是对外部类中属性的引用(例如,在第一个示例中对 delay 字段的引用。)
    • @igx,我添加了一个更新来解决这个问题。希望它能让事情更清楚。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-08-15
    • 1970-01-01
    • 2011-06-12
    相关资源
    最近更新 更多