Статьи

5 хакерских кодов для уменьшения накладных расходов ГХ

В этом посте мы рассмотрим пять способов, которыми мы можем использовать эффективное кодирование, чтобы помочь нашему сборщику мусора тратить меньше процессорного времени на выделение и освобождение памяти и уменьшить накладные расходы GC. Длинные GC часто могут приводить к остановке нашего кода во время восстановления памяти (AKA «остановить мир»). duke_GCPost

Некоторый фон

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

GC делает это, используя так называемое «молодое поколение» — сегмент кучи, где размещаются новые объекты. Каждый объект имеет «возраст» (помещенный в биты заголовка объекта), который определяет, сколько коллекций он «пережил», не будучи восстановленным. По достижении определенного возраста объект копируется в другой раздел в куче, называемый «выжившим» или «старым» поколением.

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

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

1. Избегайте неявных строк

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

Одна из самых важных вещей, которую стоит отметить, это то, что строки являются неизменяемыми . Они не могут быть изменены после распределения. Такие операторы, как «+» для конкатенации, фактически выделяют новую строку, содержащую содержимое строк, к которым присоединяются. Что еще хуже, есть неявный объект StringBuilder, который выделен для фактической работы по их объединению.

Например —

1
a = a + b; // a and b are Strings

Компилятор генерирует сопоставимый код за кулисами:

1
2
3
4
StringBuilder temp = new StringBuilder(a).
temp.append(b);
a = temp.toString(); // a new String is allocated here.
                     // The previous “a” is now garbage.

Но это становится хуже.

Давайте посмотрим на этот пример —

1
2
3
String result = foo() + arg;
result += boo();
System.out.println(“result = “ + result);

В этом примере у нас есть 3 StringBuilders, выделенных в фоновом режиме — по одному для каждой операции плюс и две дополнительные строки — одна для хранения результата второго присваивания, а другая для хранения строки, переданной в метод print. Это 5 дополнительных объектов в том, что в противном случае выглядит довольно тривиальным утверждением.

Подумайте о том, что происходит в реальных сценариях кода, таких как создание веб-страницы, работа с XML или чтение текста из файла. Вложенные в структуры цикла, вы можете просматривать сотни или тысячи объектов, которые неявно размещены. В то время как у виртуальной машины есть механизмы, чтобы справиться с этим, она идет по цене — один оплачивается вашими пользователями.

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

1
2
3
StringBuilder value = new StringBuilder(“result = “);
value.append(foo()).append(arg).append(boo());
System.out.println(value);

Помня о неявном распределении строк и StringBuilders, вы можете существенно сократить объем краткосрочных выделений в крупномасштабных местах кода.

2. План Список мощностей

Динамические коллекции, такие как ArrayLists, являются одними из самых основных структур для хранения данных динамической длины. ArrayLists и другие коллекции, такие как HashMaps и TreeMaps, реализованы с использованием базовых массивов Object []. Как и Strings (сами являются обертками над массивами char []), массивы также являются неизменяемыми. Тогда возникает очевидный вопрос — как мы можем добавлять / помещать элементы в коллекции, если размер их базового массива неизменен? Ответ также очевиден — выделяя больше массивов .

Давайте посмотрим на этот пример —

1
2
3
4
5
6
7
List<Item> items = new ArrayList<Item>();
 
for (int i = 0; i < len; i++)
{
  Item item = readNextItem();
  items.add(item);
}

Значение len определяет конечную длину элементов после завершения цикла. Это значение, однако, неизвестно конструктору ArrayList, который выделяет новый массив Object с размером по умолчанию. При превышении емкости внутреннего массива он заменяется новым массивом достаточной длины, что делает предыдущий массив мусором.

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

Решение: по возможности выделите списки и карты с начальной емкостью, например:

1
List<MyObject> items = new ArrayList<MyObject>(len);

Это гарантирует, что во время выполнения не будут происходить ненужные выделения и освобождения внутренних массивов, поскольку список теперь имеет достаточную емкость для начала. Если вы не знаете точного размера, лучше пойти с оценкой (например, 1024, 4096) среднего размера и добавить некоторый буфер для предотвращения случайных переполнений.

3. Используйте эффективные примитивные коллекции

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

Это может иметь некоторые негативные последствия . Java реализует большинство коллекций, используя внутренние массивы. Для каждой записи ключа / значения, добавленной в HashMap, внутренний объект выделяется для хранения обоих значений. Это неизбежное зло при работе с картами, что означает дополнительное выделение и возможное освобождение, производимое каждый раз, когда вы помещаете элемент в карту. Существует также возможный штраф за наращивание емкости и необходимость перераспределения нового внутреннего массива. При работе с большими картами, содержащими тысячи и более записей, эти внутренние ассигнования могут привести к увеличению затрат на ваш GC.

Очень распространенным случаем является хранение карты между примитивным значением (таким как Id) и объектом. Поскольку HashMap в Java построен для хранения типов объектов (вместо примитивов), это означает, что каждая вставка в карту может потенциально выделить еще один объект для хранения значения примитива («упаковав его»).

Стандартный метод Integer.valueOf кэширует значения в диапазоне от 0 до 255, но для каждого числа, превышающего это, будет добавлен новый объект в дополнение к внутреннему объекту ввода ключа / значения. Это может потенциально больше, чем тройные издержки GC для карты. Для тех, кто пришел из C ++, это может быть тревожной новостью, где шаблоны STL решают эту проблему очень эффективно.

К счастью, эта проблема решается в следующих версиях Java. До этого он решался довольно эффективно некоторыми замечательными библиотеками, которые предоставляют примитивные деревья, карты и списки для каждого из примитивных типов Java. Я настоятельно рекомендую Trove , с которым я работал довольно долгое время и обнаружил, что он действительно может снизить издержки GC в крупномасштабном коде.

4. Используйте потоки вместо буферов в памяти

Большая часть данных, которыми мы манипулируем в серверных приложениях, поступает к нам в виде файлов или данных, передаваемых по сети из другого веб-сервиса или из БД. В большинстве случаев входящие данные находятся в сериализованной форме и должны быть десериализованы в объекты Java, прежде чем мы сможем начать работать с ними. Этот этап очень подвержен большим неявным выделениям .

Обычно проще всего считывать данные в память с помощью ByteArrayInputStream, ByteBuffer, а затем передавать это в код десериализации.

Это может быть плохим шагом , так как вам нужно было бы выделить и затем освободить пространство для этих данных во всей их полноте, создавая из них новые объекты. А поскольку размер данных может быть неизвестного размера, вы уже догадались — вам придется выделять и освобождать внутренние массивы byte [] для хранения данных, когда они выходят за пределы емкости исходного буфера.

Решение довольно простое. Большинство постоянных библиотек, таких как собственная сериализация Java, буфер протокола Google и т. Д., Созданы для десериализации данных непосредственно из входящего файла или сетевого потока, без необходимости хранить их в памяти и без выделения новых внутренних байтовых массивов для хранения данные по мере роста. Если возможно, используйте этот подход вместо загрузки данных в память. Ваш GC поблагодарит вас.

5. Сводные списки

Неизменность — прекрасная вещь, но в некоторых масштабных ситуациях она может иметь серьезные недостатки. Один сценарий — при передаче объектов List между методами.

При возврате коллекции из функции обычно желательно создать объект коллекции (например, ArrayList) внутри метода, заполнить его и вернуть в виде неизменяемого интерфейса Collection.

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

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

Пример 1 (неэффективно) —

1
2
3
4
5
6
7
8
List<Item> items = new ArrayList<Item>();
 
for (FileData fileData : fileDatas)
{
  // Each invocation creates a new interim list with possible
  // internal interim arrays
  items.addAll(readFileItem(fileData));
}

Пример 2 —

1
2
3
4
5
6
7
List<Item> items =
  new ArrayList<Item>(fileDatas.size() * avgFileDataSize * 1.5);
 
for (FileData fileData : fileDatas)
{
  readFileItem(fileData, items); // fill items inside
}

Пример 2, хотя и не подчиняется правилам неизменности (которые обычно должны соблюдаться), может сохранить N списков (вместе с любыми временными массивами). В масштабных ситуациях это может быть благом для вашего GC.

Дополнительное чтение

Это сообщение также доступно на палубе спикера

Ссылка: 5 хаков кодирования для уменьшения накладных расходов GC от нашего партнера по JCG Айрис Шор в блоге Takipi .