14 нояб. 2012 г.

java: garbage less

За время написания gflogger и борьбой за low latency в java накопился некоторый опыт о том как меньше плодить мусора и тем самым меньше нагружать сборщик мусора, и как следствие, меньше влиять на производительность самого приложения.

Такому тонкому троллиподходу придумали специальный термин garbage less design.

Впрочем, существую и некоторые другие вариации как заставить garbage collector меньше мешать нам жить.

Что следует избегать

autoboxing

Тут скорее авторы java пошли на поводу у миллионов леммингов, орущих дайте мне сахара, да побольше, побольше!

Чем плох autoboxing ? Самое очевидное, тем, что он создаёт overhead по памяти: так если long занимает 8 байт (причём на стеке, если это локальная переменная или аргумент метода), тогда как экземпляр java.lang.Long занимает уже 16 байт и всегда (~ почти всегда) на куче.

Теперь, о не очевидном: можно смело отбросить то, что будут вызываться методы для обращения к подлежащему примитиву (н-р метод longValue() у java.lang.Long) поскольку такие методы отлично inline'ятся.

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

Самое интересное во всём этом, то, что не только смерть молодых останавливает время, но и рождение может спровоцировать нечто подобное.
На это тонко и деликатно намекает зубр рок java Doug Lea (смотрите java.util.concurrent.Exchanger.createSlot()): когда заканчивается память, которая нитка резервирует под молодое поколение (TLAB), приходится идти в большую кучу и брать ещё немного этих вкусных кусочков памяти, что, сделать без блокировок, CAS'ов не удастся.

Впрочем, что побочные эффекты при рождении и смерти объекта гарантированы каждому объекту и это касается далеко не только autoboxing, просто используя autoboxing вы это делаете неявно и в больших объёмах.

Но не стоит впадать в солипсизм - придерживайтесь здравого смысла - если использование autoboxing незначительное или вы используете значения из кэша значений -128 .. 127, то не стоит бросаться с топором и изничтожать его.

Если необходимы коллекции (в т.ч. и ассоциативные массивы) с использованием примитивов - Trove Collections - отличное решение, но стоит учитывать, что это не thread safe коллекции. Даже если вам не нужны коллекции с примитивами, всё равно стоит взглянуть на данный framework и некоторые детали реализации - open addressing, hashing strategies, особенно кто не слушал курсов по cs.

wrappers или объекты-обёртки

Пожалуй я даже почти забыл об этом, но давеча кандидат на интервью буквально расковырял мою старую фобию - wrapperовофию!
Крайне нелепо плодить фантики-обёртки (и выкидывать) на критически важном (с точки зрения производительности) пути выполнения.

Словом, используя шаблоны проектирования включайте голову и ощущайте горький вкус.

String.format или MessageFormat.format


StringBuilder/StringBuffer в методах append использует default visibility методы на уровне пакета java.lang, такие как Integer.getChars(int value, int posAtBuf, char[] buf), которые дают возможность прямого добавления строкового представления числа в char[].

Для наглядности,

final String s = String.format("value is %d", ( 100000L + v) );
что творится внутри в деталях пообъектно:
allocated the object 
 of type java/nio/HeapCharBuffer whose size is 48

an array of java.lang.Object[1]
allocated the object 100009 of type java/lang/Long whose size is 16
an array of char[16]
allocated the object  of type java/lang/StringBuilder whose size is 16
an array of char[18]
allocated the object RU of type java/lang/StringBuffer whose size is 16
an array of char[3]
allocated the string 'RUB'
allocated the object java.text.DecimalFormatSymbols@117fc of type java/text/DecimalFormatSymbols whose size is 64
allocated the object  of type java/util/Formatter whose size is 24
an array of java.lang.Object[10]
allocated the object [] of type java/util/ArrayList whose size is 24
allocated the string 'value is '
allocated the string 'value is '
allocated the object value is  of type java/util/Formatter$FixedString whose size is 16
an array of java.lang.String[6]
allocated the string ''
allocated the string 'd'
an array of char[0]
allocated the object  of type java/util/Formatter$Flags whose size is 16
an array of java.util.Formatter$Flags[1]
allocated the object d of type java/util/Formatter$FormatSpecifier whose size is 40
an array of java.util.Formatter$FormatString[0]
an array of char[32]
allocated the string 'java/util/Formatter$FormatString'
an array of java.util.Formatter$FormatString[2]
an array of char[16]
allocated the object  of type java/lang/StringBuilder whose size is 16
an array of char[6]
allocated the string '100009'
an array of char[6]
an array of char[6]
allocated the string '100009'
an array of char[15]
allocated the string 'value is 100009'
String.format count:35 size:1016

против

final String s = "value is " + (100000L + v);
и его детали пообъектно:
an array of char[25]
allocated the object value is  of type java/lang/StringBuilder whose size is 16
an array of char[15]
allocated the string 'value is 100009'
StringBuilder count:4 size:152

Словом, чего только нет в String.format - и уж сильно сомневаюсь, что escape анализ тут как-то поможет, в то же время как в случае неявного StringBuilder он может помочь избавив от одной аллокации массива (и копирования).

immutability


При всех своих плюсах immutability (нет необходимости в явных барьерах памяти, защищённость объекта и т.п.) не совместим с garbage less настолько, что добавить больше нечего.

Использовать с опаской


LinkedList


Стоит определится, что данная структура имеет скорее больше минусов, чем плюсов (по сравнению с ArrayList) - сложность поиска элемента по индексу O(N), дополнительные расходы по памяти и кроме того больше живых нод для gc, и как следствие, замедление его работы.

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

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

Улучшения


CopyOnWriteArrayList

При использовании шаблона observer, для многопоточного приложения, как правило, хранят список подписчиков в java.util.concurrent.CopyOnWriteArrayList, при разумном условии, что изменение списка происходит существенно реже, чем уведомление о событии.

При обходе списка будет создан java.util.Iterator, который за свою короткую жизнь, успевает немного, но подгадить, особенно, если с учётом объёма потока сообщений.

Избежать это можно, если пойти на компромисс - нарушение безопасности в обмен на чистоту (garbage free):

Используемый нами CopyOnWriteArrayList отличается от базового java.util.concurrent.CopyOnWriteArrayList наличием метода
/**
 * read-only access method
 */
public V[] getArray(){
    return array;
}

Т.о. мы понимаем, что получаем ссылку потокобезопасно на массив (и его элементы) и могли бы творить всё, что угодно, но контракт приписывает использовать его только для чтения.

Сторонники Escape Анализа могут поспорить, но какие ваши доказательства ?.

Тем же, у кого руки в крови кто аргументирует коротким временем жизни итератора - простая модификация с хорошим профитом.

ConcurrentLongObjectHashMap


Вполне очевидно, что одного Trove Collections для жизни мало, особенно, когда идёт речь о конкурентной среде.

Писать свой целый thread safe вариант для всех типов и видов коллекций не было
ни цели, ни желания, ни тем более времени.

В итоге на базе java.util.concurrent.ConcurrentHashMap появился ассоциативный список с long'ами в качестве ключей.

Оставим это упражнение для самостоятельного изучения читателю.

blobs


Существует не малый ряд задач, когда надо хранить большие объёмы (GBs) информации , которые надо просто отправлять в поток (stream) т.е. по-сути блобы. Соображение простое - объёмы данных таковы, что можно целиком (или почти целиком) загрузить в оперативную память, тем более, что она всё более и более доступна.

Типичные примеры - фото в социальных сетях, реплики данных в кластерах (hazelcast, terracota) и т.п.

Очевидно, что хранить каждое фото в виде отдельного объекта, пусть даже массива байт, накладно, когда таких объектов становится несколько миллионов прежде всего потому, как обход такого графа gc может занять существенное время, что приводит к отказу сервиса на некоторое время.

Тот самый случай, когда большой размер кучи враждует с garbage collector'ом.

Blob хранилище, как правило, располагают в off-heap - т.е один большой кусок памяти, за спиной у gc, над которым производят ручное управление памятью (custom memory mngmt).

Anything else ?

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

Denis Orlov комментирует...

"Но прежде всего это создание нового узла для сборщика мусора - даже если экземпляр обёртки будет жить быстро и умрёт молодым, сборщику придётся немного потрудится"

Можешь пояснить? Вроде, для copying collector никаких дополнительных расходов

Vladimir Dolzhenko комментирует...

@Denis Orlov

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

Denis Orlov комментирует...

Так, насколько я понимаю, если узел к моменту обхода copy collectorа будет уже мертвый, то в обходе он участвовать и не будет.

Или ты имеешь в виду, что если меньще мусорить, то сборки (обходы поколения) будут случаться реже?

ps Как здесь подписаться на ответы к комментариям?

Vladimir Dolzhenko комментирует...

@Denis Orlov

если узел мёртв - он будет пропущен, но обход его всё равно будет, хотя и быстрый

и соот-но меньше молодого мусора -> реже minor сборки