Home  >  Article  >  Java  >  5 Tips to Reduce Java Garbage Collection Overhead

5 Tips to Reduce Java Garbage Collection Overhead

伊谢尔伦
伊谢尔伦Original
2016-12-05 10:59:541248browse

What are some tips for keeping GC overhead low?

With the delayed release of Java 9, the G1 ("Garbage First") garbage collector will become the default garbage collector for the HotSpot virtual machine. From serial garbage collector to CMS collector, JVM has witnessed many GC implementations and G1 will be its next generation garbage collector.

With the development of garbage collectors, each generation of GC has brought huge progress and improvements compared with its previous generation. Compared with serial GC, parallel GC allows the garbage collector to work in a multi-threaded manner, making full use of the computing power of multi-core computers. Compared with parallel GC, the CMS ("Concurrent Mark-Sweep") collector divides the recycling process into multiple stages, so that when the application thread is running, the collection work can be completed concurrently, which greatly improves the frequent execution of "stop- the-world” situation. G1 shows better performance for JVMs with large amounts of heap memory, and has a more predictable and uniform pause process.

Tip #1: Predict collection capacity

All standard Java collections, including custom and extended implementations (such as Trove and Google's Guava), use arrays (either native data types or object-based types) under the hood. Because once an array is allocated, its size is immutable, so adding elements to the collection will in most cases result in the need to re-apply for a new large-capacity array to replace the old array (referring to the array used by the underlying implementation of the collection).

Even if the size of the collection initialization is not provided, most collection implementations try to optimize the processing of reallocating the array and amortize its overhead to a minimum. However, best results can be obtained by providing the size when constructing the collection.

Let us analyze the following code as a simple example:

public static List reverse(List & lt; ? extends T & gt; list) {    List result = new ArrayList();
    for (int i = list.size() - 1; i & gt; = 0; i--) {
        result.add(list.get(i));
    }    return result;
}

This method allocates a new array, then fills it up with items from another list, only in reverse order. Then fill the array with elements from another list, but the numerical order of the elements changes.

This processing method may pay a heavy performance price. The optimization point is the line of code that adds elements to a new list. As each element is added, the list needs to ensure that its underlying array has enough space to accommodate the new element. If there is a free slot, the new element is simply stored in the next free slot. If not, a new underlying array is allocated, the old array contents are copied to the new array, and the new elements are added. This will result in the array being allocated multiple times, and those remaining old arrays will eventually be reclaimed by the GC.

We can avoid these redundant allocations by letting its underlying array know how many elements it will store when constructing the collection

public static List reverse(List & lt; ? extends T & gt; list) {    List result = new ArrayList(list.size());
    for (int i = list.size() - 1; i & gt; = 0; i--) {
        result.add(list.get(i));
    }    return result;
}

The above code specifies a large enough space to store list.size() through the constructor of ArrayList elements, the allocation is completed during initialization, which means that List does not need to allocate memory again during iteration.

Guava’s collection class goes a step further, allowing you to explicitly specify the number of expected elements or specify a predicted value when initializing the collection.

List result = Lists.newArrayListWithCapacity(list.size());List result = Lists.newArrayListWithExpectedSize(list.size());

In the above code, the former is used when we already know exactly how many elements the collection will store, while the latter is allocated in a way that takes into account wrong estimates.

Tip #2: Process data streams directly

When processing data streams, such as reading data from a file or downloading data from the network, the following code is very common:

byte[] fileData = readFileToByteArray(new File("myfile.txt"));

The resulting byte array may The parsed XML document, JSON object or protocol buffered message, as well as some common options.

When dealing with large files or files with unpredictable sizes, the above approach is very unwise, because when the JVM cannot allocate a buffer to process the real file, it will cause OutOfMemoryErrors.

Even if the size of the data is manageable, using the above pattern will still cause huge overhead when it comes to garbage collection, because it allocates a very large area in the heap to store the file data.

A better way is to use a suitable InputStream (such as FileInputStream in this example) to pass directly to the parser, instead of reading the entire file into a byte array at once. All mainstream open source libraries provide corresponding APIs to directly accept an input stream for processing, such as:

FileInputStream fis = new FileInputStream(fileName);
MyProtoBufMessage msg = MyProtoBufMessage.parseFrom(fis);

Tip #3: Use immutable objects

Immutability has too many benefits. I don’t even need to go into details. However, there is an advantage that has an impact on garbage collection and should be looked at.

The attributes of an immutable object cannot be modified after the object is created (the example here uses attributes of reference data types), such as:

public class ObjectPair {
    private final Object first;    
    private final Object second;    
    
    public ObjectPair(Object first, Object second) {        
       this.first = first;        
       this.second = second;
    }    
    public Object getFirst() {        
        return first;
    }    
    public Object getSecond() {        
        return second;
    }
}

After instantiating the above class, an immutable object will be generated —All its attributes are modified with final, and they cannot be changed after the construction is completed.

不可变性意味着所有被一个不可变容器所引用的对象,在容器构造完成前对象就已经被创建。就 GC 而言:这个容器年轻程度至少和其所持有的最年轻的引用一样。这意味着当在年轻代执行垃圾回收的过程中,GC 因为不可变对象处于老年代而跳过它们,直到确定这些不可变对象在老年代中不被任何对象所引用时,才完成对它们的回收。

更少的扫描对象意味着对内存页更少的扫描,越少的扫描内存页就意味着更短的 GC 生命周期,也意味着更短的 GC 暂停和更好的总吞吐量。

Tip #4: 小心字符串拼接

字符串可能是在所有基于 JVM 应用程序中最常用的非原生数据结构。然而,由于其隐式地开销负担和简便的使用,非常容易成为占用大量内存的罪归祸首。

这个问题很明显不在于字符串字面值,而是在运行时分配内存初始化产生的。让我们快速看一下动态构建字符串的例子:

public static String toString(T[] array) {    
   String result = "[";    
   for (int i = 0; i & lt; array.length; i++) {
        result += (array[i] == array ? "this" : array[i]);        
        if (i & lt; array.length - 1) {
            result += ", ";
        }
    }
    result += "]";
    return result;
}

这是个看似不错的方法,接收一个字符数组然后返回一个字符串。但是这对于对象内存分配却是灾难性的。

很难看清这语法糖的背后,但是幕后的实际情况是这样的:

public static String toString(T[] array) {    
     String result = "[";    
     for (int i = 0; i & lt; array.length; i++) {
        StringBuilder sb1 = new StringBuilder(result);
        sb1.append(array[i] == array ? "this" : array[i]);
        result = sb1.toString();        
        
        if (i & lt; array.length - 1) {
            StringBuilder sb2 = new StringBuilder(result);
            sb2.append(", ");
            result = sb2.toString();
        }
    }
    StringBuilder sb3 = new StringBuilder(result);
    sb3.append("]");
    result = sb3.toString();
    return result;
}

字符串是不可变的,这意味着每发生一次拼接时,它们本身不会被修改,而是依次分配新的字符串。此外,编译器使用了标准的 StringBuilder 类来执行这些拼接操作。这就会有问题了,因为每一次迭代,既隐式地分配了一个临时字符串,又隐式分配了一个临时的 StringBuilder 对象来帮助构建最终的结果。

最佳的方式是避免上面的情况,使用 StringBuilder 和直接的追加,以取代本地拼接操作符(“+”)。下面是一个例子:

public static String toString(T[] array) {
    StringBuilder sb = new StringBuilder("[");    
    for (int i = 0; i & lt; array.length; i++) {
        sb.append(array[i] == array ? "this" : array[i]);        
        if (i & lt; array.length - 1) {
            sb.append(", ");
        }
    }
    sb.append("]");    
    return sb.toString();
}

这里,我们只在方法开始的时候分配了唯一的一个 StringBuilder。至此,所有的字符串和 list 中的元素都被追加到单独的一个StringBuilder中。最终使用 toString() 方法一次性将其转成成字符串返回。

Tip #5: 使用特定的原生类型的集合

Java 标准的集合库简单且支持泛型,允许在使用集合时对类型进行半静态地绑定。比如想要创建一个只存放字符串的 Set 或者存储 Map

TIntDoubleMap map = new TIntDoubleHashMap();
map.put(5, 7.0);
map.put(-1, 9.999);...

Trove 的底层实现使用了原生类型的数组,所以当操作集合的时候不会发生元素的装箱(int->Integer)或者拆箱(Integer->int), 没有存储对象,因为底层使用原生数据类型存储。

最后

随着垃圾收集器持续的改进,以及运行时的优化和 JIT 编译器也变得越来越智能。我们作为开发者将会发现越来越少地考虑如何编写 GC 友好的代码。然而,就目前阶段,不论 G1 如何改进,我们仍然有很多可以做的事来帮 JVM 提升性能。


Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn