image
Все java-разработчики, рано или поздно, встречаются с пресловутой ошибкой OutOfMemoryError. 
После этой встречи мы начинаем более бережно относится к используемой памяти, экономить ее. Начиная с версии 1.2 в Java появился пакет java.lang.ref.* с классами SoftReference, WeakReference, PhantomReference. Далее я расскажу вам о том, как помогут эти классы в борьбе с OutOfMemoryError. И что более интересно, приведу реальные примеры их использования. Начнем.

Общее Описание


Для начала немного общей теории. Вспомним, в общих чертах, как работает Garbage Collector (далее GC). Если не вдаваться в детали, то алгоритм прост: при запуске сборщика виртуальная машина рекурсивно находит, для всех потоков, все доступные объекты в памяти и помечает их неким образом. А на следующем шаге GC удаляет из памяти все непомеченные объекты. Таким образом, после чистки, в памяти будут находиться только те объекты, которые могут быть полезны программе. Идем дальше.
В Java есть несколько видов ссылок. Есть StrongReference — это самые обычные ссылки которые мы создаем каждый день.
	StringBuilder builder = new StringBuilder();
builder это и есть strong-ссылка на объект StringBuilder.
И есть 3 «особых» типа ссылок — SoftReference, WeakReference, PhantomReference. По сути, различие между всеми типами ссылок только одно — поведение GC с объектами, на которые они ссылаются. Мы более детально обсудим особенности каждого типа ссылок позже, а пока достаточно будет следующих знаний:
  1. SoftReference — если GC видит что объект доступен только через цепочку soft-ссылок, то он удалит его из памяти. Потом. Наверно.
  2. WeakReference — если GC видит что объект доступен только через цепочку weak-ссылок, то он удалит его из памяти.
  3. PhantomReference — если GC видит что объект доступен только через цепочку phantom-ссылок, то он его удалит из памяти. После нескольких запусков GC.
Если пока не понятно в чем же разница, то не переживайте, скоро все станет на свои места. Мелочи в деталях, а детали будут дальше.
Эти 3 типа ссылок наследуются от одного родителя — Reference, у которого они собственно и берут все свои public методы и конструкторы.

StringBuilder builder = new StringBuilder();
SoftReference<StringBuilder> softBuilder = new SoftReference(builder);

После выполнения этих двух строчек у нас будет 2 типа ссылок на 1 объект StringBuilder:
  • builder — strong-ссылка
  • softBuilder — soft-ссылка (формально это strong-ссылка на soft-ссылку, но для простоты я буду писать soft-ссылка)
И если во время выполнения программы, переменная builder станет недоступной, но при этом ссылка на объект, на который ссылается softBuilder, будет еще доступна И запустится GC -> то объект StringBuilder будет помечен как доступный только через цепочку soft-ссылок.
Рассмотрим доступные методы:
softBuilder.get() — вернет strong-ссылку на объект StringBuilder в случае если GC не удалил этот объект из памяти. В другом случае вернется null.
softBuilder.clear() — удалит ссылку на объект StringBuilder (то есть soft-ссылки на этот объект больше нет)
Все то же самое работает и для WeakReference и для PhantomReference. Правда, PhantomReference.get() всегда будет возвращать null, но об этом позже.
Есть еще такой класс – ReferenceQueue. Он позволяет отслеживать момент, когда GC определит что объект более не нужен и его можно удалить. Именно сюда попадает Reference объект после того как объект на который он ссылается удален из памяти. При создании Reference мы можем передать в конструктор ReferenceQueue, в который будут помещаться ссылки после удаления.

Детали SoftReference


Особенности GC
Так всё же, как ведет себя GC когда видит что объект доступен только по цепочке soft-ссылок? Давайте рассмотрим работу GC более детально:
И так, GC начал свою работу и проходит по всем объектам в куче. В случае, если объект в куче это Reference, то GC помещает этот объект в специальную очередь в которой лежат все Reference объекты. После прохождения по всем объектам GC берет очередь Reference объектов и по каждому из них решает удалять его из памяти или нет. Как именно принимается решение об удалении объекта — зависит от JVM. Но общий контракт звучит следующим образом: GC гарантировано удалит с кучи все объекты, доступные только по soft-ссылке, перед тем как бросит OutOfMemoryError.
SoftReference это наш механизм кэширования объектов в памяти, но в критической ситуации, когда закончится доступная память, GC удалит не использующиеся объекты из памяти и тем самым попробует спасти JVM от завершения работы. Это ли не чудно?
Вот как Hotspot принимает решение об удалении SoftReference: если посмотреть на реализацию SoftReference, то видно, что в классе есть 2 переменные — private static long clock и private long timestamp. Каждый раз при запуске GC, он сетит текущее время в переменную clock. Каждый раз при создании SoftReference, в timestamp записывается текущее значение clock. timestamp обновляется каждый раз при вызове метода get() (каждый раз, когда мы создаем strong-ссылку на объект). Это позволяет вычислить, сколько времени существует soft-ссылка после последнего обращения к ней. Обозначим этот интервал буквой I. Буквой F обозначим количество свободного места в куче в MB(мегабайтах). Константой MSPerMB обозначим количество миллисекунд, сколько будет существовать soft-ссылка для каждого свободного мегабайта в куче.
Дальше все просто, если I <= F * MSPerMB, то не удаляем объект. Если больше то удаляем.
Для изменения MSPerMB используем ключ -XX:SoftRefLRUPolicyMSPerMB. Дефалтовое значение — 1000 ms, а это означает что soft-ссылка будет существовать (после того как strong-ссылка была удалена) 1 секунду за каждый мегабайт свободной памяти в куче. Главное не забыть что это все примерные расчеты, так как фактически soft-ссылка удалится только после запуска GC.
Обратите внимание на то, что для удаления объекта, I должно быть строго больше чем F * MSPerMB. Из этого следует что созданная SoftReference прожи��ет минимум 1 запуск GC. (*если не понятно почему, то это останется вам домашним заданием).
В случае VM от IBM, привязка срока жизни soft-ссылки идет не к времени, а к количеству переживших запусков GC.

Применение
Главная плюшка SoftReference в том что JVM сама следит за тем нужно удалять из памяти объект или нет. И если осталось мало памяти, то объект будет удален. Это именно то, что нам нужно при кэшировании. Кэширование с использованием SoftReference может пригодится в системах чувствительных к объему доступной памяти. Например, обработка изображений. Первый пример применения будет немного выдуманным, зато показательным:
Наша система занимается обработкой изображений. Допустим, у нас есть громадное изображение, которое находиться где-то в файловой системе и это изображение всегда статично. Иногда пользователь хочет соединить это изображение с другим изображением. Вот наша первая реализация такой конкатенации:

public class ImageProcessor {
	private static final String IMAGE_NAME = "bigImage.jpg";
	public InputStream concatenateImegeWithDefaultVersion(InputStream userImageAsStream) {
		InputStream defaultImage = this.getClass().getResourceAsStream(IMAGE_NAME);                
		// calculate and return concatenated image
	}            
}

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

public class CachedImageProcessor {
	private static final String IMAGE_NAME = "bigImage.jpg";
	private InputStream defaultImage;           
	
	public InputStream concatenateImegeWithDefaultVersion(InputStream userImageAsStream) {
		if (defaultImage == null) {
			defaultImage = this.getClass().getResourceAsStream(IMAGE_NAME);
		}                
		// calculate and return concatenated image
	}            
}

Этот вариант уже лучше, но проблема все ровно есть. Изображение большое и забирает много памяти. Наше приложение работает со многими изображениями и при очередной попытке пользователя обработать изображение, легко может свалиться OutOfMemoryError. И что с этим можно сделать? Получается, что нам нужно выбирать, либо быстродействие либо стабильность. Но мы то знаем о существовании SoftReference. Это поможет нам продолжать использовать кеширование, но при этом в критических ситуациях выгружать их из кэша для освобождения памяти. Да еще и при этом нам не нужно беспокоиться о детектировании критической ситуации. Вот так будет выглядеть наша третья реализация:

public class SoftCachedImageProcessor {
	private static final String IMAGE_NAME = "bigImage.jpg";
	private SoftReference<InputStream> defaultImageRef = new SoftReference(loadImage());

	public InputStream concatenateImegeWithDefaultVersion(InputStream userImageAsStream) {                
		if (defaultImageRef.get() == null) {        //  1
			defaultImage = this.getClass().getResourceAsStream(IMAGE_NAME);
			defaultImageRef = new SoftReference(defaultImage);
		}        
		
		defaultImage = defaultImageRef.get();        //  2        
		// calculate and return concatenated image
	}            
}

Эта версия не идеальна, но она показывает как просто мы можем контролировать размер занимаемый кэшем, а точнее возложить контроль на виртуальную машину. Опасность данной реализации заключается в следующем. В строчке №1 мы делаем проверку на null, фактически мы хотим проверить, удалил GC данные с памяти или нет. Допустим, что не удалил. Но перед выполнением строки №2 может начать работу GC и удалить данные. В таком случае результатом выполнения строчки №2 будет defaultImage = null. Для безопасной проверки существования объекта в памяти, нам нужно создать strong-ссылку, defaultImage = defaultImageRef.get(); Вот как будет выглядеть финальная реализация:

public class SoftCachedImageProcessor {
	private static final String IMAGE_NAME = "bigImage.jpg";
	private SoftReference<InputStream> defaultImageRef = new SoftReference(loadImage());;

	public InputStream concatenateImegeWithDefaultVersion(InputStream userImageAsStream) {
		defaultImage = defaultImageRef.get();
		if (defaultImage == null) {
			defaultImage = this.getClass().getResourceAsStream(IMAGE_NAME);
			defaultImageRef = new SoftReference(defaultImage);
		}                
		// calculate and return concatenated image
	}            
}

Пойдем дальше. java.lang.Class тоже использует SoftReference для кэширования. Он кэширует данные о конструкторах, методах и полях класса. Интересно посмотреть, что именно они кешируют. После того как решено использовать SoftReference для кеширования, нужно решить что именно кеширова��ь. Допустим нам нужно кешировать List. Мы можем использовать как List<SoftReference> так и SoftReference<List>. Второй вариант более приемлемый. Нужно помнить, что GC применяет специфическую логику при обработке Reference объектов, да и освобождение памяти будет происходить быстрее если у нас будет 1 SoftReference а не их список. Это мы и видим в реализации Class — разработчики создали soft-ссылку на массив конструкторов, полей и методов. Если говорить про производительность, то стоить отметить что часто, ошибочно, люди используют WeakReference для построения кэша там где стоит использовать SoftReference. Это приводит к низкой производительности кэша. На практике weak-ссылки быстро будут удалены из памяти, как только исчезнут strong-ссылки на объект. И когда нам реально понадобиться вытянуть объект с кэша, мы увидим что его там уже нет.
Ну и еще один пример использования кэша на основе SoftReference. В Google Guava есть класс MapMaker. Он поможет нам построить ConcurrentMap в которой будут следующая особенность — ключи и значения в Map могут заворачиваться в WeakReference или SoftReference. Допустим в нашем приложении есть данные, которые может запросить пользователь и эти данные достаются с базы данных очень сложным запросом. Например, это будет список покупок пользователя за прошлый год. Мы можем создать кэш в котором значения (список покупок) будут храниться с помощью soft-ссылок. А если в кэше не будет значения то нужно вытянуть его с БД. Ключом будет ID пользователя. Вот как может выглядеть реализация:

ConcurrentMap<Long, List<Product>> oldProductsCache = new MapMaker().softValues().
           .makeComputingMap(new Function<User, List<Product>>() {
                   @Override
                   public List<Product> apply(User user) {
                     return loadProductsFromDb(user);
                   }
             });

WeakReference

Особенности GC
Теперь рассмотрим более детально, что же собой представляет WeakReference. Когда GC определяет, что объект доступен только через weak-ссылки, то этот объект «сразу» удаляется с памяти. Тут стоить вспомнить про ReferenceQueue и проследить за порядком удаления объекта с памяти. Напомню что для WeakReference и SoftReference алгоритм попадания в ReferenceQueue одинаковый. Итак, запустился GC и определил что объект доступен только через weak-ссылки. Этот объект был создан так:
StrIngBuilder AAA = new StringBuilder();
ReferenceQueue queue = new ReferenceQueue();
WeakReference weakRef = new WeakReference(AAA, queue);

Сначала GC очистит weak-ссылку, то есть weakRef.get() – будет возвращать null. Потом weakRef будет добавлен в queue и соответственно queue.poll() вернет ссылку на weakRef. Вот и все что хотелось написать про особенности работы GC с WeakReference. Теперь посмотрим, как это можно использовать.
Применение
Ну конечно WeakHashMap. Это реализация Map<K,V> которая хранит ключ, используя weak-ссылку. И когда GC удаляет ключ с памяти, то удаляется вся запись с Map. Думаю не сложно понять, как это происходит. При добавлении новой пары <ключ, значение>, создается WeakReference для ключа и в конструктор передается ReferenceQueue. Когда GC удаляет ключ с памяти, то ReferenceQueue возвращает соответствующий WeakReference для этого ключа. После этого соответствующий Entry удаляется с Map. Все довольно просто. Но хочется обратить внимание на некоторые детали.
  • WeakHashMap не предназначена для использования в качестве кэша. WeakReference создается для ключа а не для значения. И данные будут удалены только после того как в программе не останется strong-ссылок на ключ а не на значение. В большинстве случаев это не то чего вы хотите достичь кэшированием.
  • Данные с WeakHashMap будут удалены не сразу после того как GC обнаружит что ключ доступен только через weak-ссылки. Фактически очистка произойдет при следующем обращении к WeakHashMap.
  • В первую очередь WeakHashMap предназначен для использования с ключами, у которых метод equals проверяет идентичность объектов (использует оператор ==). Как только доступ к ключу потерян, его уже нельзя создать заново.
Хорошо, тогда в каких случаях удобно использовать WeakHashMap? Допустим нам нужно создать XML документ для пользователя. Конструированием документа будут заниматься несколько сервисов, которые на вход будут получать org.w3c.Node в который будут добавлять необходимые элементы. Так же для сервисов нужно много информации о пользователе с Базы Данных. Эти данные мы будем складировать в классе UserInfo. Класс UserInfo занимает много места в памяти и актуален только для построения конкретного XML документа. Кешировать UserInfo не имеет смысла. Нам нужно только ассоциировать его с документом и желательно удалить из памяти, когда документ более не используется программой. Все что нам нужно сделать:
private static final NODE_TO_USER_MAP = new WeakHashMap<Node, UserInfo>();

Создание XML документа будет выглядеть примерно так:
Node mainDocument = createBaseNode();
NODE_TO_USER_MAP.put(mainDocument, loadUserInfo());

Ну а вот чтение:
UserInfo userInfo = NODE_TO_USER_MAP.get(mainDocument);
If(userInfo != null) {
	// …
}

UserInfo будет находиться в WeakHashMap до тех пор пока GC не заметит, что на mainDocument остались только weak-ссылки.
Другой пример использования WeakHashMap. Многие знают про метод String.intern(). Так вот с помощью WeakReference можно создать нечто подобное. (Давайте не будет обсуждать, в рамках этой статьи, целесообразность этого решения, и примем факт, что у этого решения есть некоторые преимущества по сравнению с intern()). Итак, у нас есть ооочень много строк. Мы знаем что строки повторяются. Для сохранения памяти мы хотим использовать повторно уже существующие объекты, а не создавать новые объекты для одинаковых строк. Вот как в этом нам поможет WeakHashMap:
private static Map<String, WeakReference<String>> stringPool = new WeakHashMap<String, WeakReference<String>>;

public String getFromPool(String value) {
	WeakReference<String> stringRef = stringPool.get(value);
	if (stringRef == null || stringRef.get() == null ) {
		stringRef = new WeakReference<String>(value);
		stringPool.put(value, stringRef);
	}

	return stringRef.get();
}

И на последок добавлю, что WeakReference используется во многих классах – Thread, ThreadLocal, ObjectOutpuStream, Proxy, LogManager. Вы можете посмотреть на их реализацию для того чтоб понять в каких случаях вам может помочь WeakReference.

PhantomReference

Особенности GC
Особенностей у этого типа ссылок две. Первая это то, что метод get() всегда возвращает null. Именно из-за этого PhantomReference имеет смысл использовать только вместе с ReferenceQueue. Вторая особенность – в отличие от SoftReference и WeakReference, GC добавит phantom-ссылку в ReferenceQueue послетого как выполниться метод finalize(). Тоесть фактически, в отличии от SoftReference и WeakReference, объект еще есть в памяти.

Практика
На первый взгляд не ясно как можно использовать такой тип ссылок. Для того чтоб объяснить как их использовать, ознакомимся сначала с проблемами возникающими при использовании метода finalize(): переопределение этого метода позволяет нам очистить ресурсы связанные с объектом. Когда GC определяет что объект более недоступный, то перед тем как удалит его из памяти, он выполняет этот метод. Вот какие проблемы с этим связаны:
  1. GC запускается непредсказуемо, мы не можем знать когда будет выполнен метод finalize()
  2. Методы finalize() запускаются в одном потоке, по очереди. И до тех пор, пока не выполниться этот метод, объект не может быть удален с памяти
  3. Нет гарантии, что этот метод будет вызван. JVM может закончить свою работу и при этом объект так и не станет недоступным.
  4. Во время выполнения метода finalize() может быть создана strong-ссылка на объект и он не будет удален, но в следующий раз, когда GC увидит что объект более недоступен, метод finalize() больше не выполнится.
Вернемся к PhantomReference. Этот тип ссылок в комбинации с ReferenceQueue позволяет нам узнать, когда объект более недоступен и на него нет других ссылок. Это позволяет нам сделать очистку ресурсов, используемых объектом, на уровне приложения. В отличии от finalize() мы сами контролируем процесс очистки ресурсов. Помимо этого, мы можем контролировать процесс создания новых объектов. Допустим у нас есть фабрика, которая будет возвращать нам объект HdImage. Мы можем контролировать, сколько таких объектов будет загружено в память:
public HdImageFabric {
	public static final int IMAGE_LIMIT = 10;
	public static int count = 0;
	public static ReferenceQueue<HdImage> queue = new ReferenceQueue<HdImage>();

	public HdImage loadHdImage(String imageName) {
		while (true) {
			if (count < IMAGE_LIMIT) {
				return	wrapImage(loadImage(imageName));	
			} else {
				Reference<HdImage> ref = queue.remove(500);
				if (ref != null) {
					count--;
					System.out.println(“remove old image”);
				}
			}
		}
	}

	private HdImage wrapImage(HdImage image) {
		PhantomReference<HdImage> refImage = new PhantomReference(image, queue);
		count++;
		return refImage ;
	}
}

Этот пример не потокобезопасный и имеет друге недостатки, но зато он показывает, как можно использовать на практике PhantomReference.
Из-за того что метод get() всегда возвращает null, становится непонятным а как все же понять какой именно объект был удален. Для этого нужно создать собственный класс, который будет наследовать PhantomReference, и который содержит некий дескриптор, который в будущем поможет определить какие ресурсы нужно чистить.
Когда вы используете PhantomReference нужно помнить о следующих вещах:
  1. Контракт гарантирует что ссылка появится в очереди после того как GC заметит что объект доступен только по phantom-ссылкам и перед тем как объект будет удален из памяти. Контракт не гарантирует, что эти события произойдут одно за другим. В реальности между этими событиями может пройти сколько угодно времени. Поэтому не стоит опираться на PhantomReference для очистки критически важных ресурсов.
  2. Выполнение метода finalize() и добавление phantom-ссылки в ReferenceQueue выполняется в разных запусках GC. По этому если у объекта переопределен метод finalize() то для его удаления необходимы 3 запуска GC, а если метод не переопределен, то нужно, минимум, 2 запуска GC
.

В качестве вывода хочу сказать что java.lang.ref.* дает нам неплохие возможности для работы с памятью JVM и не стоит игнорировать эти классы, они могут здорово нам помочь. Их ис��ользование связанно с большим количеством ошибок, и нужно быть крайне осторожным для достижения желаемого результата. Но разве эти трудности нас когда-то останавливали? На этом все. Спасибо всем кто дочитал до конца. Постараюсь в комментариях ответить на те вопросы, которые не сумел раскрыть в этой статье.