Статические ссылки очищаются - действительно ли Android разгружает классы во время выполнения, если они не используются?

У меня вопрос, связанный с тем, как сборка/загрузка мусора работает в Android. Мы несколько раз наткнулись на эту проблему, и, насколько я могу судить, Android ведет себя по-другому от обычной JVM.

Проблема заключается в следующем: мы в настоящее время пытаемся сократить классы singleton в приложении в пользу единственного root factory singleton, единственной целью которого является управление другими классами менеджера. Менеджер верхнего уровня, если хотите. Это упрощает замену реализаций в тестах без выбора полного решения DI, поскольку все действия и службы имеют одну и ту же ссылку на этот корень factory.

Вот как это выглядит:

public class RootFactory {

    private static volatile RootFactory instance;

    @SuppressWarnings("unused")
    private Context context; // I'd like to keep this for now

    private volatile LanguageSupport languageSupport;
    private volatile Preferences preferences;
    private volatile LoginManager loginManager;
    private volatile TaskManager taskManager;
    private volatile PositionProvider positionManager;
    private volatile SimpleDataStorage simpleDataStorage;

    public static RootFactory initialize(Context context) {
        instance = new RootFactory(context);
        return instance;
    }

    private RootFactory(Context context) {
        this.context = context;
    }

    public static RootFactory getInstance() {
        return instance;
    }

    public LanguageSupport getLanguageSupport() {
        return languageSupport;
    }

    public void setLanguageSupport(LanguageSupport languageSupport) {
        this.languageSupport = languageSupport;
    }

    // ...
}

initialize вызывается один раз, в Application.onCreate, т.е. до того, как запущена какая-либо деятельность или служба. Теперь вот проблема: метод getInstance иногда возвращается как null - даже при вызове в том же потоке! Похоже, что это не проблема видимости; вместо этого статическая опорная ссылка одиночного элемента на уровне класса кажется фактически очищена сборщиком мусора. Может быть, я перехожу к выводам здесь, но может быть, это связано с тем, что сборщик мусора Android или механизм загрузки классов могут фактически выгружать классы, когда память становится недостаточной, и в этом случае единственная ссылка на экземпляр singleton исчезнет? Я не очень глубоко разбираюсь в модели памяти Java, но я полагаю, что этого не должно произойти, иначе этот общий способ реализации синглетонов не будет работать на любом JVM правильно?

Любая идея, почему это происходит именно?

PS: можно обойти это, сохранив вместо этого "глобальные" ссылки на экземпляр одного приложения. Это оказалось надежным, когда нужно держать объект в течение всего жизненного цикла приложения.

UPDATE

По-видимому, мое использование изменчивости вызвало некоторую путаницу. Мое намерение состояло в том, чтобы убедиться, что текущее состояние статической ссылки всегда видно все потокам доступа к его. Я должен сделать это, потому что я как пишу, так и читая эту ссылку из более чем одного потока: в обычном приложении запускается только в основном потоке приложения, но в прогоне контрольно-измерительного теста, где объекты заменяются на mocks, я пишу его из измерительной нитью и прочитать ее в потоке пользовательского интерфейса. Я мог бы также синхронизировать вызов с getInstance, но это более дорого, так как требует потребовать блокировку объекта. См. Что представляет собой эффективный способ реализации одноэлементного шаблона в Java? для более подробного обсуждения этого вопроса.

Ответ 1

И вы (@Matthias), и Марк Мерфи (@CommonsWare) правы в том, что вы говорите, но суть кажется утраченной. (Использование volatile корректно и классы не выгружаются.)

Суть вопроса в том, откуда вызывается initialize.

Вот что я думаю происходит:

  • Вы вызываете инициализацию из Activity *
  • Android требует больше памяти, убивает весь Process
  • Android перезапускает Application и верхнюю Activity
  • Вы вызываете getInstance который будет возвращать null, так как initialize не была вызвана

Поправьте меня если я ошибаюсь.


Обновить:
Мое предположение - что initialize вызывается из Activity * - в этом случае, похоже, было неверно. Тем не менее, я оставлю этот ответ, потому что этот сценарий является распространенным источником ошибок.

Ответ 2

Я никогда в жизни не видел статического члена данных, объявленного volatile. Я даже не знаю, что это значит.

Элементы статических данных будут существовать до тех пор, пока процесс не завершится или пока вы не избавитесь от них (например, null из статической ссылки). Процесс может быть прекращен после того, как все действия и службы будут активно закрыты пользователем (например, кнопка BACK) и ваш код (например, stopService()). Процесс может быть прекращен даже с живыми компонентами, если Android отчаянно отстает от ОЗУ, но это довольно необычно. Процесс может быть прекращен с помощью живой услуги, если Android считает, что ваша служба слишком длинна, хотя она может перезапустить эту услугу в зависимости от вашего возвращаемого значения от onStartCommand().

Классы не разгружаются, период, заканчивающийся завершением процесса.

Чтобы обратиться к другой точке @sergui, действия могут быть уничтожены, причем состояние экземпляра сохраняется (хотя и в ОЗУ, а не "фиксированное хранилище" ), чтобы освободить оперативную память. Android будет делать это до завершения активных процессов, хотя, если он уничтожит последнее действие для процесса и нет запущенных сервисов, этот процесс станет основным кандидатом на завершение.

Единственное, что очень странно в вашей реализации, это использование volatile.

Ответ 3

Статические ссылки очищаются всякий раз, когда система чувствует себя так, и ваше приложение не является верхним уровнем (пользователь не запускает его явно). Всякий раз, когда ваше приложение сведено к минимуму, и ОС хочет получить больше памяти, оно либо убьет ваше приложение, либо сериализует его на фиксированном хранилище для последующего использования, но в обоих случаях статические переменные стираются. Кроме того, всякий раз, когда ваше приложение получает ошибку Force Close, все статики также стираются. По моему опыту я видел, что всегда лучше использовать переменные в объекте Application, чем статические переменные.

Ответ 4

Я видел подобное странное поведение с моим собственным кодом, включающим исчезающие статические переменные (я не думаю, что эта проблема имеет какое-то отношение к ключевому слову volatile). В частности, это произошло, когда я инициализировал фреймворк регистрации (например, Crashlytics, log4j), а затем после некоторого периода активности он кажется неинициализированным. Исследование показало, что это происходит после того, как OS вызывает onSaveInstanceState(Bundle b).

Ваши статические переменные хранятся в Classloader, который содержится в вашем приложении. Согласно google:

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

http://developer.android.com/guide/topics/processes/process-lifecycle.html

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

Обходной путь, который я использовал для инициализации моей среды регистрации, - это все мои действия для расширения базового класса, где я переопределяю onCreate и проверяю на инициализацию и при необходимости повторно инициализируют.

Я думаю, официальное решение состоит в том, чтобы использовать обратный вызов onSaveInstanceState(Bundle b), чтобы сохранить все, что необходимо для вашей активности, а затем повторно инициализировать в onCreate(Bundle b), когда b != null.

Google объясняет это лучше всего:

http://developer.android.com/training/basics/activity-lifecycle/recreating.html