Поддержи Openmeetings

понедельник, 17 мая 2010 г.

Управление памятью в Java: слабые и мягкие ссылки

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

Как известно, сборщик мусора работает следующим образом. Все ссылки, которые находятся на стеке или в статической области памяти образуют корневое множество. Все объекты в «куче», до которых можно добраться по ссылкам из корневого множества, называются живыми. Объекты, до которых добраться нельзя — мертвыми. Во время сборки мусора мертвые объекты удаляются.

Конечно, в реальности всё сложнее: объекты разделяются на поколения, используется механизм «крапления карт» и другие. Но для наших целей достаточно этой принципиальной схемы:

Те ссылки, которые показаны на этой картинке — обычные. Они образуются в момент присваивания объекта некоторой переменной:

MyClass a = new MyClass();

Такие ссылки ещё называются сильными (strong), а объекты, к которым можно составить путь по сильным ссылкам — сильнодостижимыми (strongly reachable).

Для некоторых задач такой подход может быть показаться слишком негибким.

Может быть, иногда имеет смысл сохранить объект, даже если на него осталось ссылок? Или наоборот — убрать, даже если он достижим из корневого множества? Или, может быть, некоторые объекты имеет смысл уничтожать не сразу, а выполнять некоторые действия перед уничтожением?

В Java в таких случаях используются классы из пакета java.lang.ref: SoftRefence (мягкая ссылка), WeakReference (слабая ссылка) и PhantomReference (призрачная ссылка).

Задача 1: исчезающие ящики

На ваш склад прибыла партия ящиков, и вам нужно присвоить каждому ящику номер. Для этого заведена хеш-табличка, в которой ключи — ссылки на ящики, а значения — их номера. Примерно так:

HashMap boxMap = new HashMap();

При поступлении на склад, ящик добавляется в таблицу:

boxMap.put(box, boxid);
При этом возникает проблема: при отгрузке ящика со склада, память, выделенная для него, не освобождается, поскольку хеш-таблица содержит ссылку на ящик, и виртуальная машина считает его «живым».

Вопрос: как можно модифицировать хеш-таблицу так, чтобы память освобождалась после отгрузки ящика?

Одно из возможных решений:

Эта задача — один из вариантов достаточно общей проблемы: есть объект со «средним» временем жизни (т.е. он живет дольше, чем метод, который выделил для него память, но меньше, чем приложение), c этим объектом связаны метаданные, и эта связь хранится в словаре. В итоге, некоторому третьему объекту необходимо в определенный момент почистить словарь от ненужных записей. Если этот третий объект случайно забудет почистить словарь, случится утечка памяти.

Было бы хорошо, если бы записи в этом словаре вообще не учитывались при подсчёте достижимости объекта — то есть, если бы сборщик мусора уничтожал объект box, когда ссылки на него остались только в словаре.

В Java есть способ добиться такого поведения — использовать шаблонный класс WeakReference («слабая ссылка»). Объект называется слабодостижимым («weakly reachable»), если до него можно добраться только по слабым ссылкам из корневого множества.

Слабые ссылки создаются примерно так:

WeakReference<Box> boxRef = new WeakReference<Box>(box);

В том случае, если объект boxRef останется единственной живой ссылкой на объект box, объект box будет удалён сборщиком мусора:

Возвратимся к нашему примеру: если при добавлении записи в хеш-табличку объект класса «ящик» обернуть в слабую ссылку, то это решит проблему утечек памяти. Однако это создаст другую проблему: иметь дело с хеш-табличкой, ключ в записи в которой может неожиданно обратиться в null — занятие не самое приятное. Поэтому наша табличка должна быть достаточно умной, чтобы самой удалять запись, как только удаляется объект, на который ссылается ключ.

Реализация такой таблицы — это очень интересно, но несколько выходит за рамки этой заметки. Мы воспользуемся стандартным классом WeakHashMap. Одну из возможных реализаций можно посмотреть в Apache Harmony, вошедшей в качестве библиотеки классов в Android.

Map<Box,long> boxMap = new WeakHashMap<Box,long>();
boxMap.put(box, boxid);

Замечание: в статьях на эту тему иногда встречается удивительное утверждение о том, что WeakHashMap можно использовать для кэширования. На мой взгляд, это неверно. В качестве кэша этот класс можно использовать только если вы хотите, чтобы объекты внутри кэша были как можно раньше уничтожены сборщиком мусора. Однако это очень странная стратегия кэширования.

Задача 2: верный путь к OutOfMemoryError

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

public class PicModifier {
  private byte[] picData;
  public synchronized int modifyPicture(String fileName) {
    int picSize = getPicSize(fileName);
    if (picData == null || picData.length < picSize) {
       picdata = new byte[pisSize];
       loadPicture(fileName, picSize);
    }
  }
}

Идея предварительно выделить буфер для файла кажется нормальной: выделение памяти всякий раз для каждой новой картинки может несколько замедлить работу приложения. Однако возникает новая проблема: вдруг приложению перестанет хватать памяти? Тогда, наверное, разумно будет пренебречь буфером и вернуть память системе.

Вопрос: каким образом можно модифицировать код так, чтобы приложение сохраняло буфер, если памяти достаточно и освобождало его, если память в системе заканчивается?

Одно из возможных решений

Здесь нам требуется поведение, обратное поведению слабых ссылок. Мы хотим указать сборщику мусора, что определенный тип объектов нужно держать в памяти столько времени, сколько возможно без риска получить OutOfMemoryError.

Этого можно добиться, используя «мягкие ссылки». Они работают следующим образом: если объект достижим через набор мягких ссылок («мягкодостижим», softly reachable, см. рисунок), то виртуальная машина будет стараться сохранять этот объект в памяти как можно дольше.

Всё, что требуется в нашей задаче — это обернуть буфер в мягкую ссылку.

private SoftReference<byte[]> picData;

Таким образом, мягкие ссылки обеспечивают нечто вроде кэширования. Но это, конечно же, не настоящее кэширование. Если Вашему приложению действительно нужен полноценный кэш, то одних только мягких ссылок мало, нужна реализация кэширующих стратегий, по возможности независимых от количества свободной памяти. Поэтому мягкие ссылки иногда называют «кэшированием для бедных».

4 комментария :

Alexei комментирует...

Почему у вас тут
WeakReferenceboxRef = new WeakReference(box);
Имя класса box совпадает с именем объекта box?

Alexei комментирует...

Да, это ошибка в статье. Вы совершенно правильно заметили.

Alexei комментирует...

Максим, я надеялся, что может ты поправишь. :-)

Alexei комментирует...

Мне когда-то вопрос о ссылках на собеседовании задали,но я кроме жёстких ссылок никаких других не знал.Потому как изучал java по книгам Брюса Эккеля,Шилдта,Хорстмана,а там об этом ничего нет..

Отправить комментарий