计划的未来会导致内存泄漏吗?

我想我在我的Android动态壁纸中有一个内存泄漏。每当我旋转屏幕时,收集的内存垃圾量都会增加50kb,并且不会回落。我认为这可能是由预定的未来引起的,所以我将提出一个场景,看看情况是否如此。

假设您有一个类(我们称之为Foo),其中包含以下成员。

private ScheduledFuture<?> future;
private final ScheduledExecutorService scheduler = Executors
        .newSingleThreadScheduledExecutor();

private final Runnable runnable = new Runnable() {
    public void run() {
        // Do stuff
    }
};

现在,您设定了一个预定的未来

future = scheduler.scheduleAtFixedRate(runnable, delay, speed,
                TimeUnit.MILLISECONDS);

future 包含对 runnable 的引用,runnable 包含对父 Foo 对象的引用。我不确定是否属于这种情况,但这一事实是否意味着,如果程序中没有任何内容包含对Foo的引用,垃圾回收器仍然无法收集它,因为有一个预定的未来?我不太擅长多线程处理,所以我不知道我显示的代码是否意味着计划任务将比对象存活更长时间,这意味着它最终不会被垃圾回收。

如果这种情况不会导致防止Foo成为垃圾回收,我只需要用一个简单的解释来告诉我。如果它确实可以防止Foo被垃圾回收,那么我该如何修复它?必须这样做吗?该部分是否没有必要?future.cancel(true); future = null;future = null


答案 1

虽然这个问题很久以前就得到了回答,但在阅读本文后,我想发布带有解释的新答案。

计划的未来会导致内存泄漏吗?--- 是的

ScheduledFuture.cancel()或者通常不会通知它已被取消,并且它会一直停留在队列中,直到其执行时间到来。对于简单的期货来说,这没什么大不了的,但对于.它可以在那里停留几秒钟,几分钟,几小时,几天,几周,几年或几乎无限期,这取决于它被安排的延迟。Future.cancel()ExecutorScheduledFutures

下面是一个最坏情况的示例。可运行的内容及其引用的所有内容都将在队列中保留 long.MAX_VALUE 毫秒,即使在其 Future 已被取消后也是如此!

public static void main(String[] args) {
    ScheduledThreadPoolExecutor executor 
        = new ScheduledThreadPoolExecutor(1);

    Runnable task = new Runnable() {
        @Override
        public void run() {
            System.out.println("Hello World!");
        }
    };

    ScheduledFuture future 
        = executor.schedule(task, 
            Long.MAX_VALUE, TimeUnit.MILLISECONDS);

    future.cancel(true);
}

您可以通过使用或调用方法来查看这一点,该方法将返回一个包含一个元素的列表(取消的是Runnable)。ProfilerScheduledThreadPoolExecutor.shutdownNow()

这个问题的解决方案是编写自己的Future实现,或者不时调用该方法。在自定义执行器工厂解决方案的情况下是:purge()

public static ScheduledThreadPoolExecutor createSingleScheduledExecutor() {
    final ScheduledThreadPoolExecutor executor 
        = new ScheduledThreadPoolExecutor(1);

    Runnable task = new Runnable() {
        @Override
        public void run() {
            executor.purge();
        }
    };

    executor.scheduleWithFixedDelay(task, 30L, 30L, TimeUnit.SECONDS);

    return executor;
}

答案 2
  • 您的方法要么依赖于封闭类,因此不能独立存在。在这种情况下,我不明白你怎么能让你的gc'ed并保持你的可运行“活着”由执行者运行runFooFoo
  • 或者您的方法是静态的,因为它不依赖于类的状态,在这种情况下,您可以将其设置为静态,这将防止您遇到的问题。runFoo

你似乎没有处理你的Runnable中的中断。这意味着即使您调用Runnable也将继续运行,正如您确定的那样,这可能是导致泄漏的原因。future.cancel(true)

有几种方法可以使Runnable“中断友好”。要么调用一个抛出 InterruptedException 的方法(如或阻塞 IO 方法),当将来取消时,它将抛出 一个。您可以捕获该异常,并在清理需要清理的内容并恢复中断状态后立即退出 run 方法:Thread.sleep()InterruptedException

public void run() {
    while(true) {
        try {
            someOperationThatCanBeInterrupted();
        } catch (InterruptedException e) {
            cleanup(); //close files, network connections etc.
            Thread.currentThread().interrupt(); //restore interrupted status
        }
    }
}    

如果您不调用任何此类方法,则标准成语为:

public void run() {
    while(!Thread.currentThread().isInterrupted()) {
        doYourStuff();
    }
    cleanup();
}

在这种情况下,您应该尝试确保定期检查while中的条件。

通过这些更改,当您调用 时,将向执行 Runnable 的线程发送一个中断信号,该线程将退出它正在执行的操作,从而使您的 Runnable 和 Foo 实例有资格获得 GC。future.cancel(true)


推荐