GC 성능을 높이는 코딩 [JAVA]

GC 성능을 높이는 코딩 [JAVA]

GC 성능 향상

애플리케이션 성능은 가비지 컬렉션의 빈도와 기간에 직접적인 영향을 받는다. 즉, GC 프로세스의 최적화는 이러한 메트릭을 줄임으로써 수행됩니다. 이를 수행하는 두 가지 주요 방법이 있다.

  1. Old 영역과 Young 영역 힙 크기를 적절히 조정
  2. 개체 할당 및 Old 영역으로의 승격 속도를 줄이기

Young 영역과 Old 영역의 힙 크기를 알맞게 조정하는 것 객체의 할당(Allocation)이나 Old 영역으로의 이동(Promotion) 등의 작업을 줄이는 것이다.

힙 크기를 늘리면 GC 빈도가 감소하면서 지연 시간이 증가하고, 힙 크기를 줄이면 빈도가 증가해 적절한 힙 크기를 조정하는 것은 예상만큼 간단하지 않다.

  1. Collection의 크기를 예측하여 설정

모든 Java의 Collections와 그를 확장하여 구현한 구현체들(Trove나 Google의 Guava)은 내부적으로 배열을 사용한다. 배열의 크기는 불변의 값이라 초기에 할당 되면 수정이 불가능하다. 그렇기 때문에 처음에 설정한 크기를 초과하여 계속 item을 담으려고 하면 내부적으로 새로운 크기의 배열을 생성하고 item을 복사하게 된다. 이 과정에서 기존의 배열은 더 이상 사용되지 않는 가비지가 된다.

물론 대부분의 Collection은 이러한 재할당(Re-Allocation) 과정을 최적화하려고 노력하고 있지만 가비지가 생기는 것은 불가피하다. 그렇기 때문에 가능하다면 Collection의 크기를 예측하여, 생성 시에 직접 설정해주도록 하자.

// 크기를 예측하여 직접 설정
List<String> list = new ArrayList(10);
  1. Stream 사용

파일로부터 데이터를 읽거나 네트워크를 통해 파일을 받는 경우, 다음과 같은 코드를 쉽게 찾아볼 수 있다.

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

읽으려는 데이터의 크기가 작다면 상관이 없겠지만, 데이터의 크기가 크거나 예측할 수 없다면 그렇게 좋지 못한 방법이다. 왜냐하면 데이터의 크기가 너무 크다면 JVM이 해당 파일의 내용을 할당할 수 없어 OutOfMemoryErrors가 발생할 수 있으며, 할당이 되었다 하더라도 이후에 상당히 큰 규모의 가비지가 되기 때문이다. 이러한 문제를 예방하는 가장 좋은 방법은 InputStream을 직접 사용하는 것이다.

InputStream은 내부적으로 Buffer를 두고 있어 일정한 크기(Chunk)만큼씩 데이터를 조회한다. 그렇기 때문에 InputStream을 사용하면 Buffer를 재사용함으로써 OutOfMemoryErrors를 방지할 수 있고, 가비지의 생성을 최소화할 수 있다. 실제로 대부분의 Major한 도구들은 Stream을 직접 받아 처리하도록 되어 있다.

FileInputStream fis = new FileInputStream("myfile.txt");
MyProtoBufMessage msg = MyProtoBufMessage.parseFrom(fis);
  1. 불변(immutability) 객체 사용

불변성(Immutability)을 활용하면 GC의 성능을 높일 수 있다. 불변의 객체는 한번 생성된 이후에 수정이 불가능한 객체로, Java에서는 final 키워드를 사용하여 불변의 객체를 생성할 수 있다.

이렇게 객체를 생성하기 위해서는 객체를 가지는 컨테이너도 존재한다는 것인데, 당연히 불변의 객체가 먼저 생성되어야 컨테이너가 이 객체를 참조할 수 있을 것이다. 즉, 컨테이너는 컨테이너가 참조하는 불변객체들보다 더 젊다는 것(늦게 생성되었다는 것)이다.

이러한 점은 GC가 수행될 때, 가비지 컬렉터가 컨테이너 하위의 불변 객체들은 Skip할 수 있도록 도와준다. 왜냐하면 해당 컨테이너가 살아있다는 것은 하위의 불변 객체들 역시 처음에 할당된 그 상태로 참조되고 있다는 것을 의미하기 때문이다

결국 불변의 객체를 활용하면 가비지 컬렉터가 스캔해야 되는 객체의 수가 줄어서 스캔해야 하는 메모리 영역과 빈도수 역시 줄어들 것이고, GC가 수행되어도 지연 시간을 줄일 수 있을 것이다.

  1. String 사용 최적화
  • 중복된 String이 생성되는 경우, JVM 옵션 사용

ava 8u20 업데이트부터는 동일한 문자열에 의해 불필요한 메모리를 사용을 줄이도록 새로운 JVM 파라미터(UseStringDeduplication)를 추가하였다. 해당 옵션을 사용하면 중복되는 String 인스턴스들을 Global Single Char[]로 관리하여 힙 메모리의 사용을 최적화할 수 있다.

java -XX:+UseStringDeduplication -jar Application.java

  • StringBuilder 사용

tring과 StringBuilder 및 StringBuffer의 가장 큰 차이는, String은 immutable(불변)하고, 나머지 두개는 mutable(가변)하다는 점이다.

그렇기 때문에 문자열을 연결하기 위한 “+” 등과 같은 연산은 String의 내용을 변경하는 것이 아니라, 새로운 문자열을 할당하는 것이다. Java에서는 이를 최적화하기 위해 StringBuilder를 제공해주고 있으며, Compiler는 String을 더하는 연산을 내부적으로 StringBuiler를 사용하여 처리하고 있다.

String

예를 들어 다음과 같은 두 문자열의 덧셈이 있다고 하자.

// b 역시 String 객체이다.
String a = a + b;

컴파일러는 이러한 문자열을 더하는 코드를 다음과 같이 변경하여 처리한다.

StringBuilder temp = new StringBuilder(a).
temp.append(b);
a = temp.toString(); // 새로운 String이 할당되고, 기존의 a의 데이터는 가비지가 됨

StringBuilder