0

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.

这是什么意思 ?有人可以举一个这样的对象的好例子吗?为什么这可能被认为是泄漏?谢谢

4

1 回答 1

4

不幸的是,您没有为这个问题分配任何语言标签,所以我假设您的语言是 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捕获thisIntegrationModule. 这意味着即使没有 的直接引用,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捕获其父类而不是嵌套类的类。


更新:怎么办?

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

什么是内存泄漏?

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

  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并显式传递里面的所有依赖项。

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

生命周期

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

于 2018-01-29T00:38:18.180 回答