Статьи

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


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

Немного предыстории



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


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


Этот процесс, хотя и эффективен, все же требует затрат. 

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

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



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


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










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

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

StringBuilder temp = new StringBuilder(a);
temp.append(b);
a = temp.toString(); // a new String is allocated here.
                     // The previous "a" is now garbage.
Но это становится хуже.

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



String result = foo() + arg;
result += boo();
System.out.println("result = " + result);
В этом примере у нас есть 3 StringBuilders, выделенных в фоновом режиме — по одному для каждой операции плюс и две дополнительные строки — одна для хранения результата второго присваивания, а другая для хранения строки, переданной в метод print. Это 5 дополнительных объектов в том, что в противном случае выглядит довольно тривиальным утверждением.


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

она идет по цене
— один оплачивается вашими пользователями.


Решение:

Одним из способов уменьшить это является проактивное выделение StringBuilder. В приведенном ниже примере достигается тот же результат, что и в коде выше, при этом выделяется только один StringBuilder и одна String для хранения окончательного результата вместо пяти исходных объектов.
StringBuilder value = new StringBuilder("result = ");
value.append(foo()).append(arg).append(boo());
System.out.println(value);

By being mindful of the way Strings and StringBuilders are implicitly allocated you can materially reduce the amount of short-term allocations in high-scale code locations.



2. Plan List Capacities


Dynamic collections such as ArrayLists are among the most basic structures to hold dynamic length data. ArrayLists and other collections such as HashMaps and TreeMaps are implemented using underlying Object[] arrays. Like Strings (themselves wrappers over char[] arrays), array size is also immutable. The obvious question then becomes – how can we add/put items in collections if their underlying array’s size is immutable? The answer is obvious as well – by

allocating more arrays
.


Let’s look at this example —

List<Item> items = new ArrayList<Item>();
for (int i = 0; i < len; i++) {
    Item item = readNextItem();
    items.add(item);
}

The value of len determines the ultimate length of items once the loop finishes. This value, however, is unknown to the constructor of the ArrayList which allocates a new Object array with a default size. Whenever the capacity of the internal array is exceeded, it’s replaced with a new array of sufficient length, making the previous array garbage.


If you’re executing the loop thousands of times you may be forcing a new array to be allocated and a previous one to be collected multiple times. For code running in a high-scale environment, these allocations and deallocations are all deducted from your machine’s CPU cycles.


The solution:

Whenever possible, allocate lists and maps with an initial capacity, like so:
List<MyObject> items = new ArrayList<MyObject>(len);

This ensures that no unnecessary allocations and deallocations of internal arrays occur at runtime as the list now has sufficient capacity to begin with. If you don’t know the exact size, it’s better to go with an estimate (e.g. 1024, 4096) of what an average size would be, and add some buffer to prevent accidental overflows.

3. Use Efficient Primitive Collections



Current versions of the Java compiler support arrays or maps with a primitive key or value type through the use of “boxing” – wrapping the primitive value in a standard object which can be allocated and recycled by the GC.


This can have some

negative implications
. Java implements most collections using internal arrays. For each key/value entry added to a HashMap an internal objectis allocated to hold both values. This is a necessary evil when dealing with maps, which means an extra allocation and possible deallocation made every time you put an item into a map. There’s also the possible penalty of outgrowing capacity and having to reallocate a new internal array. When dealing with large maps containing thousands or more entries, these internal allocations can have increasing costs for your GC.


A very common case is to hold a map between a primitive value (such as an Id) and an object. Since Java’s HashMap is built to hold object types (vs. primitives), this means that every insertion into the map can potentially allocate yet another object to hold the primitive value (“boxing” it).


The standard Integer.valueOf method caches the values between -128 and 127, but for each number outside that range, a new object will be allocated in addition to the internal key / value entry object. This can potentially more than

triple GC overhead
for the map. For those coming from a C++ background this can really be troubling news, where STL templates solve this problem very efficiently.


Luckily, this problem is being worked on for next versions of Java. Until then, it’s been dealt with quite efficiently by some great libraries which provide primitive trees, maps and lists for each of Java’s primitive types. I strongly recommend 

Trove
, which I’ve worked with for quite a while and found that can really reduce GC overhead in high-scale code.

4. Use Streams Instead of In-memory Buffers



Most of the data we manipulate in server applications comes to us in the form of files or data streamed over the network from another web service or a DB. In most cases, the incoming data is in serialized form, and needs to be deserialized into Java objects before we can begin operating on it. This stage is very prone to

large implicit allocations
.


The easiest thing to do usually is read the data into memory using a  ByteArrayInputStream, ByteBuffer and then pass that on to the deserialization code.


This can be a

bad move
, as you’d need to allocate and later deallocate room for that data in its entirety while constructing new objects out of it . And since the size of the data can be of unknown size, you guessed it – you’ll have to allocate and deallocate internal byte[] arrays to hold the data as it grows beyond the initial buffer’s capacity.


The solution

is pretty straightforward. Most persistence libraries such as Java’s native serialization, Google’s Protocol Buffers, etc. are built to deserialize data directly from the incoming file or network stream, without ever having to keep it in memory, and without having to allocate new internal byte arrays to hold the data as it grows. If available, go for that approach vs. loading the data into memory. Your GC will thank you.

5. Aggregate Lists



Immutability is a beautiful thing, but in some high-scale situations it can have some serious drawbacks. One scenario is when passing List objects between methods.


When returning a collection from a function, it’s usually advisable to create the collection object (e.g. ArrayList) within the method, fill it and return it in the form of an immutable Collection interface.


There are some cases where this

doesn’t work well
. The most noticeable one is when collections are aggregated from multiple method calls into a final collection. While immutability provides more clarity, in high-scale situations it can also mean massive allocation of interim collections.


The solution in this case would be not to return new collections, but instead aggregate values into a single collection that’s passed into those methods as a parameter.


Example 1 (inefficient) —

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));
}

Example 2 —

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

Example 2, while disobeying the rules of immutability (which should normally be adhered to) can save N list allocations (along with any interim array allocations). In high-scale situations this can be a boon to your GC.

Additional Reading





String interning –

http://plumbr.eu/blog/reducing-memory-usage-with-string-intern

Efficient wrappers –
http://vanillajava.blogspot.co.il/2013/04/low-gc-coding-efficient-listeners.html

Using Trove –
http://java-performance.info/primitive-types-collections-trove-library/

This post originally appeared on the 


Takipi Blog
.



Slides are available on Speaker Deck, and on SlideShare.