>  기사  >  Java  >  Java 가비지 수집 오버헤드를 줄이는 5가지 팁

Java 가비지 수집 오버헤드를 줄이는 5가지 팁

伊谢尔伦
伊谢尔伦원래의
2016-12-05 10:59:541247검색

GC 오버헤드를 낮게 유지하기 위한 몇 가지 팁은 무엇입니까?

Java 9의 출시가 지연되면서 G1("Garbage First") 가비지 수집기가 HotSpot 가상 머신의 기본 가비지 수집기가 됩니다. 직렬 가비지 수집기에서 CMS 수집기에 이르기까지 JVM은 많은 GC 구현을 목격했으며 G1은 차세대 가비지 수집기가 될 것입니다.

가비지 컬렉터의 개발로 인해 각 세대의 GC는 이전 세대에 비해 엄청난 발전과 개선을 가져왔습니다. 직렬 GC와 비교하여 병렬 GC를 사용하면 가비지 수집기가 다중 스레드 방식으로 작동하여 다중 코어 컴퓨터의 컴퓨팅 성능을 최대한 활용할 수 있습니다. 병렬 GC에 비해 CMS("Concurrent Mark-Sweep") 수집기는 재활용 프로세스를 여러 단계로 나누어 애플리케이션 스레드가 실행 중일 때 수집 작업을 동시에 완료할 수 있으므로 "stop"의 빈번한 실행이 크게 향상됩니다. - 세계”상황. G1은 대량의 힙 메모리가 있는 JVM에 대해 더 나은 성능을 보여주며 더 예측 가능하고 균일한 일시 중지 프로세스를 제공합니다.

팁 #1: 컬렉션의 용량 예측

사용자 정의 및 확장 구현(예: Trove 및 Google Guava)을 포함한 모든 표준 Java 컬렉션은 배열(기본 데이터 유형)을 사용합니다. 바닥 또는 개체 유형에 따라). 배열이 할당되면 크기가 변경되지 않으므로 컬렉션에 요소를 추가하면 대부분의 경우 이전 배열을 대체하기 위해 새로운 대용량 배열을 다시 적용해야 합니다(에서 사용하는 배열 참조). 컬렉션의 기본 구현).

컬렉션 초기화 크기가 제공되지 않더라도 대부분의 컬렉션 구현에서는 배열 재할당 처리를 최적화하고 오버헤드를 최소화하려고 노력합니다. 그러나 컬렉션을 구성할 때 크기를 제공하면 최상의 결과를 얻을 수 있습니다.

다음 코드를 간단한 예로 분석해 보겠습니다.

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

이 방법은 새 배열을 할당한 다음 역순으로 다른 목록의 항목으로 채웁니다. 새 배열을 할당한 다음 다른 목록의 요소로 배열을 채우면 요소의 숫자 순서만 변경됩니다.

이 처리 방법은 성능 비용이 많이 들 수 있습니다. 최적화 지점은 새 목록에 요소를 추가하는 코드 라인입니다. 각 요소가 추가되면 목록은 기본 배열에 새 요소를 수용할 수 있는 충분한 공간이 있는지 확인해야 합니다. 빈 슬롯이 있으면 새 요소는 다음 빈 슬롯에 저장됩니다. 그렇지 않은 경우 새 기본 배열이 할당되고 이전 배열 내용이 새 배열에 복사되며 새 요소가 추가됩니다. 이로 인해 배열이 여러 번 할당되고 나머지 이전 배열은 결국 GC에 의해 회수됩니다.

컬렉션을 구성할 때 기본 배열에 저장할 요소 수를 알려줌으로써 이러한 중복 할당을 피할 수 있습니다.

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

위 코드는 ArrayList의 생성자를 통해 충분히 지정합니다. list.size() 요소를 저장하면 초기화 중에 할당이 완료됩니다. 이는 List가 반복 중에 메모리를 다시 할당할 필요가 없음을 의미합니다.

Guava의 컬렉션 클래스는 한 단계 더 발전하여 컬렉션을 초기화할 때 예상 요소 수를 명시적으로 지정하거나 예측 값을 지정할 수 있습니다.

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

위 코드에서 전자는 컬렉션이 저장할 요소 수를 이미 정확히 알고 있을 때 사용되는 반면, 후자는 잘못된 추정을 고려하는 방식으로 할당됩니다.

팁 #2: 데이터 스트림을 직접 처리

파일에서 데이터를 읽거나 네트워크에서 데이터를 다운로드하는 등 데이터 스트림을 처리할 때 다음 코드가 매우 일반적입니다.

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

결과 바이트 배열은 XML 문서, JSON 개체 또는 프로토콜 버퍼 메시지뿐만 아니라 몇 가지 일반적인 옵션으로 구문 분석될 수 있습니다.

JVM이 실제 파일을 처리하기 위해 버퍼를 할당할 수 없을 때 OutOfMemoryErrors가 발생하므로 대용량 파일이나 크기를 예측할 수 없는 파일을 처리할 때 위의 접근 방식은 현명하지 않습니다.

데이터 크기가 관리 가능하더라도 위 패턴을 사용하면 파일 데이터를 저장하기 위해 힙에 매우 큰 영역을 할당하기 때문에 가비지 수집 시 엄청난 오버헤드가 발생합니다.

이를 처리하는 더 좋은 방법은 전체 파일을 한 번에 바이트 배열로 읽는 대신 적절한 InputStream(예: 이 예에서는 FileInputStream)을 사용하여 파서에 직접 전달하는 것입니다. 모든 주류 오픈 소스 라이브러리는 다음과 같이 처리를 위해 입력 스트림을 직접 받아들이는 해당 API를 제공합니다.

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

팁 #3: 불변 객체 사용

불변성 이점이 너무 많습니다. 자세히 설명할 필요도 없습니다. 그러나 살펴보아야 할 가비지 수집에 영향을 미치는 한 가지 장점이 있습니다.

불변 객체의 속성은 객체가 생성된 후에 수정할 수 없습니다(여기의 예에서는 참조 데이터 유형의 속성을 사용합니다). 예:

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

위를 변경한 후 클래스 인스턴스화되면 불변 객체가 생성됩니다. 해당 객체의 모든 속성은 final로 수정되며 생성이 완료된 후에는 변경할 수 없습니다.

不可变性意味着所有被一个不可变容器所引用的对象,在容器构造完成前对象就已经被创建。就 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 提升性能。


성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.