📜 ⬆️ ⬇️

5 hacks to reduce overhead in garbage collection


This post will look at five ways to improve code efficiency, helping the garbage collector spend less time allocating and freeing memory. A long garbage collection procedure can lead to a phenomenon known as “Stop the world”.

General information


Garbage Collector (GC) exists to handle a large number of memory allocations for short-lived objects (for example, objects selected during the rendering of a web page become obsolete as soon as the page is displayed).

GC in this case uses the so-called "young generation" ("young generation") - a segment of the heap, where new objects are located. Each object has an “age” field (“age”, located in the object header), which determines how many garbage collections it has experienced. As soon as a certain age is reached, the object is copied to another area of ​​the heap, called the “old” generation.
')
The process is still effective, but already becoming noticeable. The ability to reduce the amount of memory allocations for temporary objects will help us increase performance, especially in widely scaled environments or in Android applications where resources are more limited.

Below are five ways that can be used in everyday development to improve the efficiency of working with memory, while not spending much of our time and do not reduce the readability of the code.

1. Avoid implicit use of strings.


Strings are an integral part of almost any data structure. More demanding than other primitive types, they have a greater effect on memory consumption.

Do not forget that the lines are immutable. They are not modified after allocation. Operators such as "+" when concatenating strings actually create a new String object containing string concatenation. In addition, this leads to the implicit creation of a StringBuilder object, which performs the merge operation itself.

Let's give an example:
a = a + b; // a  b -  

But the actual code generated by the compiler behind the scenes:
 StringBuilder temp = new StringBuilder(a). temp.append(b); a = temp.toString(); //    . //  “a”   . 

The reality is still worse .
Consider the following example:
 String result = foo() + arg; result += boo(); System.out.println(“result = “ + result); 

Here we have 3 StringBuilders allocated implicitly - one for each "+" operation and two additional strings - one, as a result of the second assignment, the other is passed to the println method. As a result, we received 5 additional objects in the trivial code.

Think about what happens in real programs, like generating a web page, working with XML, or reading text from a file. Such code inside the loop will result in hundreds or thousands of implicitly allocated objects. VM has mechanisms to deal with this, but everything has a price - and your users will pay it.

Solution: one of the ways can be explicitly creating a StringBuilder. In the example below, the same result is achieved, but the memory is allocated only for one StringBuilder and one string for the final result.
 StringBuilder value = new StringBuilder(“result = “); value.append(foo()).append(arg).append(boo()); System.out.println(value); 

Keeping in mind that in such cases strings and StringBuilders are implicitly distinguished, you can significantly reduce the number of small memory allocations in frequently executed code.

2. Set the initial capacity of the lists.


Dynamically expandable collections, such as an ArrayList, are some of the basic structures designed to hold variable-length data. ArrayList and other collections, for example, HashMap, TreeMap are implemented using the underlying Object [] array. Just like strings (which are add-ons over arrays of characters), the size of the array is unchanged. The obvious question is how do you add elements to the collection, with the immutable size of the underlying array? The answer is no less obvious - the allocation of most of the arrays.

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

The value of the variable len defines the maximum number of elements processed before the end of the loop. Nevertheless, this value is unknown to the ArrayList constructor, which is forced to allocate an array with the default size. Whenever the capacity of the internal array becomes exceeded, it is replaced with a new one of sufficient length, as a result of which the previous one turns into garbage.

If the cycle is executed a thousand times, it can lead to multiple allocation of new arrays and assembly of old ones. For a program running in a highly scalable environment, these (de) allocations are uselessly subtracted from the processor cycles.

Solution: specify the initial capacity wherever possible:
 List<MyObject> items = new ArrayList<MyObject>(len); 

This ensures the absence of excessive memory allocation of internal arrays that occur during execution. If the exact size is not known, then it is worth to indicate approximately or the expected average value, adding a couple of percent on top in case of an unexpected overflow.

3. Use efficient collections of primitive types.


Current versions of Java compilers support ordinary and associative arrays with keys and primitive type values ​​by using “autoboxing” - wrapping a primitive value with a standard object that can be selected and deleted by the GC.

This may have some negative consequences . In Java, most of the collections are implemented using internal arrays. Each key-value pair added to a HashMap causes a selection of an internal object to hold both values. This inevitable evil accompanies the use of associative arrays - every time an element is added to the map, this results in the allocation of a new object and, possibly, an assembly of the old one. There are also costs associated with excess capacity, i.e. re-allocation of resources for a new internal array. When we deal with a large associative array, with thousands or even more objects, these internal allocations can significantly affect the GC.

A common case is to preserve any mapping between primitive types (for example, an identifier) ​​and an object. Since HashMap is intended for storing object types, this means that each insert implies the creation of another object to “package” values ​​of a primitive type.

The standard Integer.valueOf () method caches values ​​between -128 and 127, but any number outside this range will result in a separate object for each key-value pair. This results in a triple GC overhead in each associative array. For those who come from C ++, this can be news - thanks to the patterns in STL, this problem is solved quite effectively.

Fortunately, they are working on this in new versions of Java. In the meantime, try to somehow improve efficiency with the help of wonderful third-party libraries that provide trees of primitive types, associative arrays and lists. I highly recommend Trove , which I worked with for quite some time and I can confirm the real reduction in overhead costs for garbage collection in the critical code.

4. Use Streaming instead of buffers in memory.


Most of the data we use in server applications come to us as files or data streams from network connections or databases. In most cases, the incoming data comes in serialized form and requires deserialization into objects before performing operations on them. This stage is subject to implicit memory consumption, often in considerable amounts.

Usually, data is read into memory using ByteArrayInputStream, ByteBuffe, then the result is transferred to deserialization.

This can be a bad approach, because you must first allocate, and then free up space for the data, but only after the completion of the construction of objects from them. But as a rule, the size of the data is not known, which will lead, you guessed it, to a permanent re-allocation of memory for byte [] arrays, which will grow when the buffer capacity is exceeded.

The solution is extremely simple. Many libraries, such as the native Java serializer, Protocol Buffers, etc. able to build deserialized objects using data directly from the network stream, i.e. do not require data storage in memory and internal arrays. If possible, use this approach - GC will say thank you.

5. Immunity is not always good


Immunity is an excellent thing, but in the case of high-performance computing it can be a serious disadvantage. Consider a transfer scenario between methods of a list object.

In the case of returning a collection from a function, it is usually recommended to create a collection object (for example, ArrayList) inside the method, fill it in and return it in the form of an immutable Collection interface.
But in some cases this is unacceptable . For example, in the case where collections returned from methods are collected in the final collection. Although immunity provides transparency, in a high-load service situation, this can mean massive memory allocation for intermediate collections.

The solution in this case is to avoid returning new collections from the methods; instead, pass the object of the resulting collection as a parameter to the method.

Example 1. (Ineffective)
 List<Item> items = new ArrayList<Item>(); for (FileData fileData : fileDatas) { //       // , , -   items.addAll(readFileItem(fileData)); } 

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

Example 2 neglects the immunity rules (which in typical situations is recommended to be observed), but we were able to avoid a lot of side allocations, which in the case of intensive calculations would have a very positive effect on GC.

What else to read?


1) About string interning

2) Pro effective wrappers

3) Trove Pro

4) Pro Trove on Habré

Source: https://habr.com/ru/post/187602/


All Articles