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

标签 java scala memory-leaks profiling yourkit

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的实例捕获了thisIntegrationModule。这意味着即使没有IntegrationModule的直接引用,全局Scheduler对象仍保留对ScheduledJob_IntegrationModule_Nested实例的引用这一事实意味着IntegrationModule及其所有字段也将被永久有效地保留。这是纯粹的内存泄漏。

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

对于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/

相关文章:

Java 将文件值存储到数组中

java - 合并 Firestore 查询时出现重复数据

java - 集合中的元素是如何存储的?

Scala - 欧拉项目 #8

memory-leaks - ANTLR4内存清理

ios - 这个简短方法中的内存泄漏

java - 区 block 链表示

java - 可以使用 Postgres 序列和 Hibernate 映射制作可查询的计数器吗?

Scala 替代 pythons `with`

c++ - 在 C++ 中编写一个检查内存泄漏的测试用例