Подтвердить что ты не робот

Может ли запланированное будущее вызвать утечку памяти?

Я думаю, что у меня есть утечка памяти на моих живых обоях Android. Всякий раз, когда я поворачиваю экран, количество собранного мусора памяти увеличивается на 50 кбайт и не возвращается. Я думаю, что это может быть вызвано запланированным будущим, поэтому я собираюсь представить сценарий, чтобы убедиться, что это дело.

Скажем, у вас есть класс (пусть его называют 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);

Будущее содержит ссылку на runnable, а runnable содержит ссылку на родительский объект Foo. Я не уверен, так ли это, но мог ли этот факт означать, что если в программе нет ссылки на Foo, сборщик мусора все равно не сможет его собрать, потому что запланированное будущее? Я не слишком хорошо разбираюсь в многопоточности, поэтому не знаю, показывал ли код, который я показал, запланированное задание будет жить дольше, чем объект, а это значит, что он не будет собирать мусор.

Если этот сценарий не приведет к тому, что Foo не станет сборкой мусора, мне просто нужно сказать, что с простым объяснением. Если это мешает Foo собирать мусор, то как его исправить? Нужно делать future.cancel(true); future = null;? Не нужна ли часть future = null?

4b9b3361

Ответ 1

  • Либо ваш метод run полагается на класс Foo и поэтому не может жить независимо. В этом случае я не вижу, как вы могли бы использовать ваш Foo gc'ed и сохранить ваш runnable "alive", который будет выполняться исполнителем
  • или ваш метод run статичен в том смысле, что он не зависит от состояния вашего класса Foo, и в этом случае вы можете сделать его статическим, и это предотвратит возникшую проблему.

Кажется, вы не справляетесь с перерывами в Runnable. Это означает, что даже если вы вызываете future.cancel(true), ваш Runnable будет продолжать работать, что, как вы определили, может стать причиной утечки.

Существует несколько способов сделать Runnable "прерывистым". Либо вы вызываете метод, который выдает InterruptedException (например, 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();
}

В этом случае вы должны убедиться, что условие в то время проверено регулярно.

С этими изменениями, когда вы вызываете future.cancel(true), сигнал прерывания будет отправлен в поток, выполняющий ваш Runnable, который выйдет из того, что он делает, делая ваш Runnable и ваш экземпляр Foo подходящим для GC.

Ответ 2

Хотя на этот вопрос ответили долго, но после прочтения этой статьи, я подумал о публикации нового ответа с объяснением.

Может ли запланированное будущее вызвать утечку памяти? --- ДА

ScheduledFuture.cancel() или Future.cancel() вообще не уведомляет свой Executor о том, что он был отменен, и он остается в очереди, пока не наступило время его выполнения. Это не очень важно для простых фьючерсов, но может быть большой проблемой для ScheduledFutures. Он может оставаться там в течение нескольких секунд, минут, часов, дней, недель, лет или почти неограниченно в зависимости от задержек, которые он запланировал.

Вот пример с наихудшим сценарием. Управляемый и все, что он ссылается, останется в очереди для Long.MAX_VALUE миллисекунд даже после того, как его будущее будет отменено!

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);
}

Вы можете увидеть это с помощью Profiler или вызова метода ScheduledThreadPoolExecutor.shutdownNow(), который вернет список с одним элементом в нем (это Runnable, которое было отменено).

Решение этой проблемы состоит в том, чтобы либо написать свою собственную реализацию Future, либо время от времени вызывать метод purge(). В случае пользовательского Executor factory решение:

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;
}

Ответ 3

Будущее содержит ссылку на runnable, а runnable содержит ссылку на родительский объект Foo. Я не уверен, что так, но может ли этот факт означать, что если ничто в программе не содержит ссылка на Foo, сборщик мусора все еще не может его собрать потому что запланированное будущее?

Неплохая идея сделать Foo своего рода переходный объект, который вы создаете часто, потому что вы должны закрыть ScheduledExecutorService scheduler, когда ваше приложение закрывается. Таким образом, вы должны сделать Foo псевдо-одиночным.

Сборщик мусора понимает циклы, поэтому, как только вы завершите работу службы Foo, у вас скорее всего не будет проблем с памятью.