자바 가비지 컬렉션 (Garbage Collection)

Jeongmin Yeo (Ethan)·2021년 2월 1일
4

Java Performance

목록 보기
2/3
post-thumbnail

자바 가비지 컬렉션에 대해 정리합니다.

학습할 내용은 다음과 같습니다.

  • 가비지 컬렉션 개요
  • 제너레이션 간의 가비지 컬렉터들
  • GC 알고리즘
  • GC 알고리즘 선택
  • 기본 GC 튜닝

Reference


1. 가비지 컬렉션 개요

가비지 컬렉션 튜닝은 자바 애플리케이션 성능을 개선하기 위해 할 수 있는 일 중 가장 중요합니다.

현재 JVM에서 사용할 수 있는 주요 가비지 컬렉터는 단일 CPU 머신에서 사용하는 시리얼 컬렉터(serial collector), 처리율 병렬 컬렉터(parallel collector), 동시 병렬 컬렉터(concurrent mark-sweep collector), G1(Garbage First) 컬렉터가 있습니다.

이들의 성능 특성은 매우 다릅니다. 깊이 있는 내용은 다음 챕터에서 다루겠습니다.

먼저 이 글에서는 컬렉터가 동작하는 방법에 대한 기본 개요를 알아보곘습니다.

자바의 가장 매력적인 기능 중 하나는 개발자가 명시적으로 객체의 생명 주기를 관리할 필요가 없다는 점입니다.

객체는 필요할 때 생성되고 더 이상 사용되지 않을 때 JVM이 자동으로 객체를 해체시킵니다.

기본적으로 GC(가비지 컬렉터)는 더 이상 사용되지 않는 객체를 찾아서 그 객체와 관련된 메모리를 해체하도록 구성되어 있습니다.

JVM은 정기적으로 미사용 객체가 있는 힙을 통해서 찾습니다. 미사용 객체를 찾으면 JVM은 그 객체들이 점유하고 있는 메모리를 해제하고 추가 객체를 할당하는 데 사용할 수 있습니다.

하지만 대게 이렇게 단순하게 메모리를 해제하고 사용하는 걸로는 충분하지 않습니다. 왜냐하면 어느 순간 메모리를 할당하고 해제하는 걸 반복하다 보면 메모리 단편화(memory fragmentation)이 일어날 수 있기 때문입니다.

그러므로 GC의 성능은 미사용 객체 찾기, 가용 메모리 생성, 힙 압축과 같은 기본 동작에 따라 좌우됩니다.

GC 컬렉터들은 이 동작에 대해 각기 다른 접근 방식을 취하는데 이와 같은 이유로 각각의 컬렉터는 성능이 다릅니다.

가비저 컬렉터가 수행되는 동안 실행 중인 애플리케이션을 실행하는 스레드가 없다면 훨씬 동작의 수행은 간단하지만 자바 프로그램은 전형적으로 멀티 스레드 기반으로 가비지 컬렉터 자제가 멀티 스레드를 사용합니다.

여기서는 두 개의 논리적인 스레드 그룹을 표현하겠습니다. 하나는 애플리케이션 로직을 수행하는 스레드 그룹, 나머지는 GC를 수행하는 그룹으로 표현하겠습니다.

GC 스레드가 객체를 참조하고 메모리에 객체를 옮기는 작업을 수행한다면 이는 애플리케이션 스레드가 그 객체를 사용하고 있지 않다는 의미입니다.

즉 GC 스레드는 그 객체에 참조할 수 있는 애플리케이션 스레드가 없을 때 작동됩니다.

GC 스레드가 동작하고 모든 애플리케이션 스레드가 중지될 때를 stop the world pause 라고 합니다. 이렇게 중지되면 일반적인 애플리케이션 성능에 가장 큰 영향을 미치므로 GC를 튜닝한다는 건 이 중지가 최소화 되는 걸 의미합니다.


2. 제너레이션 간의 가비지 컬렉터들

세부 사항은 다소 다를 순 있겠지만 모든 가비지 컬렉터는 힙을 별도의 제너레이션으로 나눠서 작업합니다.

이건 올드 제너레이션, 영 제너레이션이라고 불립니다. 때로 에덴 영역은 전체 영 제너레이션을 포함하는 부정확하게 사용되기도 합니다.

영 제너레이션 영역은 에덴(eden)과 서바이버 스페이스(survivor space)로 알려진 구간으로 더 나뉩니다.

이렇게 제너레이션을 나누는 근거는 대부분의 객체는 아주 단기간 동안 사용되다가 폐기된다는 철학에서 나옵니다.

많은 자바 코드에서 객체들은 매우 빠르게 생성되고 폐기됩니다. 이런 형태의 동작을 기반으로 가비지 컬렉터는 많은 객체들이 일시적으로 사용된다는 사실을 이용해 설계되었습니다.

객체는 먼저 전체 힙의 일부인 에덴 영역에 할당됩니다. 에덴 영역이 가득 찼다면 가비지 컬렉터는 애플리케이션 스레드를 모두 멈추고 에덴 영역을 서바이서 스페이스 영역 또는 올드 제너레이션 영역으로 옮기는 작업을 합니다.

이 동작을 마이너(minor) GC라고 합니다.

이 설계에는 성능상의 이점이 두 가지가 있습니다.

먼저 영 제너레이션은 전체 힙의 일부분일 뿐이므로 이와 같이 처리하면 전체 힙을 처리하는 것보다 더 빠르게 처리할 수 있습니다. 물론 더 자주 멈춘다는 특징이 있습니다.

이 트레이드 오프에 대해서는 다음에 더 심도있게 다루겠습니다. 하지만 더 자주 중지되더라도 대체로 짧게 처리되는 편이 더 이익입니다.

두번째 장점은 객체가 영 제너레이션에서 할당되는 방식에서 비롯됩니다. 마이너 GC 이후에 살아있는 객체는 모두 서바이버 스페이스나 올드 제너레이션 으로 이동합니다. 즉 영 제너레이션은 자동으로 압축됩니다.

모든 GC 알고리즘은 영 제너레이션에서 수집되는 동안 모든 애플리케이션 스레드를 중지시킵니다.

객체가 올드 제너레이션으로 이동되고 가득 차게 된다면 JVM은 올드 제너레이션 내에서 더 이상 사용되지 않는 객체를 찾아서 폐기시킬 필요가 있을 것입니다.

여기서 GC 알고리즘은 가장 큰 차이점을 갖게 됩니다.

간단한 방법은 모든 애플리케이션 스레드를 중지시키고 사용되지 않는 객체를 찾아서 메모리를 해제하고 힙을 압축시킵니다. 이 과정을 풀(full) GC라고 불리고 일반적으로 애플리케이션 스레드가 오래 중지됩니다.

이 방법 말고 처리가 복잡한 다른 방법도 있습니다.

CMS와 G1 가비지 컬렉터 모두 애플리케이션 스레드가 실행되는 동안 미사용 객체를 찾을 수 있습니다.

이와 같은 이유로 CMS와 G1은 동시 병렬 컬렉터(concurrent collector)라고 불립니다. 모든 애플리케이션 스레드를 멈출 필요를 최소화 하기 때문에 이들은 저중단(low pause) 컬렉터라고 불립니다.

물론 이 둘의 동시 병렬 컬렉터도 올드 제너레이션을 압축하기 위해 다른 접근법을 취합니다.

CMS나 G1 컬렉터를 이용하면 애플리케이션은 일반적으로 더 적게 중지됩니다. 이에대한 트레이드 오프는 애플리케이션이 전반적으로 CPU를 더 사용한다는 사실입니다.

그렇다고 해서 CMS와 G1 모두 긴 풀 GC를 하지 않는다는 말은 아닙니다. 풀 GC 횟수를 줄일 수 있지 상황이 여의치 않다면 풀 GC는 발생할 수 있습니다. 이는 GC 알고리즘에서 설명하겠습니다.

각각의 상황에 맞는 가비지 컬렉터를 선택할 때는 전반적인 성능의 목표에 대해 생각해보면 됩니다. 모든 상황에는 트레이드 오프가 있습니다.

애플리케이션에서 개별적인 요청에 대한 응답 시간을 측정한다고 가정해봅시다.

  • 개별적인 요청은 GC에 의한 중지시간 즉 풀 GC에 의해 영향을 받을 수 있습니다. 성능의 목표가 중지 시간이 응답 시간에 미치는 영향을 최소로 하고 싶다면 동시 병렬 컬렉터가 더 적합할 것입니다.

  • 평균 응답 시간이 특이 요소(GC에 의해 중지 시간의 영향으로 응답 시간이 늦춰지는 경우)보다 더 중요하다면 보통 처리율 컬렉터가 더 나은 결과를 낼 수 있습니다.

  • 동시 병렬 컬렉터로 긴 중지 시간을 피하는 이점으로 CPU 사용률에 대한 추가 비용이 들어갑니다.

CPU를 충분히 사용할 수 있는 경우 동시 병렬 컬렉터를 사용한다면 풀 GC를 좀 더 피할 수 있으므로 작업이 더 빨리 끝날 것입니다.

CPU가 제한되어 있다면 동시 병렬 컬렉터는 CPU를 추가적으로 소모하는 오버헤드가 있기 때문에 더 많은 시간이 걸릴 것입니다.


3. GC 알고리즘

JVM이 GC를 수행하는 알고리즘 4개를 소개하겠습니다.

시리얼 가비지 컬렉터

시리얼 가비지 컬렉터(The serial garbage collector)는 네 개 중에서 가장 단순합니다.

애플리케이션이 클라이언트 클래스 머신(단일 프로세서 머신이나 윈도우 32bit JVM)에서 수행되고 있다면 디폴트 컬렉터 입니다.

시리얼 컬렉터는 힙을 처리하기 위해 단일 스레드를 사용합니다. 마이너나 풀 GC를 사용한다면 애플리케이션 스레드는 모두 중지됩니다.

풀 GC가 일어나는 동안에는 올드 제너레이션은 완전히 압출될 것입니다.

시리얼 컬렉터는 -XX:+UseSerialGC 플래그를 통해 사용할 수 있습니다. 대부분의 JVM 플래그와 달리 시리얼 컬렉터는 + 부호를 - 부호로 바꾼다면 비활성화 됩니다.

또는 시리얼 컬렉터가 다폴트인 시스템에서 다른 GC 알고리즘을 명시한다면 비활성화 됩니다.

처리율 컬렉터

서버 클래스 머신(다중 CPU 유닉스 머신과 64bit JVM)에서 디폴트로 사용되는 컬렉터입니다.

처리율 컬렉터(throughput collector)는 영 제너레이션을 수집할 때 스레드를 여러개 사용합니다. 이로 인해 시리얼 컬렉터를 사용하는 것보다 마이너 GC가 더 빠릅니다.

그리고 올드 제너레이션을 처리할 때도 여러개의 스레드를 사용하므로 보다 더 빠릅니다.

여러개의 스레드를 사용하기 때문에 처리율 컬렉터는 흔히 병렬 컬렉터(parallel collector)라고 불립니다.

처리율 컬렉터는 마이너든 풀이든 GC가 일어나는 경우 모든 애플리케이션 스레드를 멈춥니다. 풀 GC에서는 완전히 올드 제너레이션 영역을 압축합니다.

대부분의 상황에서 처리율 컬렉터는 디폴트이므로 명시적으로 활성화시킬 필요는 없습니다.

필요할 때 사용하려면 -XX:+UseParallelGC -XX:+UseParallelOldGC 플래그를 통해 사용 가능합니다.

CMS 컬렉터

CMS 컬렉터는 처리율과 시리얼 컬렉터에서 풀 GC에서 생기는 애플리케이션 스레드의 긴 중지 현상을 해결할려고 설계되었습니다.

CMS도 마이너 GC 동안 애플리케이션 스레드는 전부 중지시키고 여러 개의 스레드로 동작합니다.

하지만 영 제너레이션을 수행하는 데는 다른 알고리즘을 사용합니다.

CMS는 풀 GC 동안 애플리케이션 스레드를 멈추지 않고 주기적으로 올드 제너레이션을 통해 살피고 미사용 객체를 폐기하는 데 하나 이상의 백그라운드 스레드를 사용합니다.

이로 인해 CMS는 저중지 컬렉터가 됩니다. 애플리케이션 스레드는 마이너 컬렉션이 일어나느 동안에만 중지되고 특정 시점에 백그라운드 스레드가 올드 제너레이션을 살피고 미사용 객체를 폐기합니다.

즉 처리율 컬렉터보다 애플리케이션 스레드가 멈춘 시간은 적습니다.

여기서 트레이드 오프는 CPU 사용량이 더 많다는 점입니다. 애플리케이션 스레드가 수행되고 있음에도 동시에 백그라운드 GC 스레드가 동작하므로 CPU가 충분해야 합니다.

게다가 CMS 컬렉터의 백그라운드 스레드는 올드 제너레이션을 살필 때 압축을 하지 않습니다.

이건 힙이 단편화 될 수 있다는 사실을 말하며 CMS 백그라운드 스레드가 작업을 하는데 충분한 CPU를 받지 못하거나 힙이 객체를 할당하는데 단편화가 너무 많이 되거나 해서 올드 제너레이션 영역이 부족하다고 느낀다면 CMS는 시리얼 컬렉터의 동작을 합니다.

이 경우 단일 스레드를 사용해 올드 제너레이션을 비우고 압축하기 위해 모든 애플리케이션 스레드가 중지됩니다.

CMS는 -XX:+UseConMarkSweepGC -XX:+UseParNewGC 플래그를 통해 사용할 수 있습니다. 디폴트는 false 입니다.

G1 컬렉터

G1(Garbage First) 컬렉터는 최소한으로 중지시키며 약 4GB 이상의 큰 힙을 처리하도록 설계되었습니다.

힙을 여러 개의 영역으로 나누지만 여전히 제너레이션 기반의 컬렉터 입니다.

영 제너레이션은 여전히 모든 애플리케이션 스레드를 멈추고 올드 제너레이션이나 서바이버 스페이스로 살아 있는 객체를 옮기는 식으로 동작합니다.

다른 알고리즘과 마찬가지로 이는 여러 개의 스레드를 이용해서 일어납니다.

G1은 동시 병렬 컬렉터로 대부분의 작업을 수행하는 데 애플리케이션 스레드를 중단시킬 필요가 없고 백그라운드 스레드로 올드 제너레이션을 처리합니다.

다만 차이는 올드 제너레이션은 여러 영역으로 나뉘고 G1은 한 영역에서 다른 데로 복사해서 올드 제너레이션에서 객체를 지울 수 있으므로 단편화 현상을 약간 해결합니다. 즉 약간 압축할 수 있습니다.

CMS와 마찬가지로 풀 GC를 피하기 위한 트레이드 오프는 CPU 시간입니다.

애플리케이션 스레드가 수행되고 있더라도 동시에 여러 개의 백그라운드 스레드가 동작해야 하므로 CPU가 더 필요합니다.

G1은 -XX:+UseG1GC 플래그를 통해 사용할 수 있습니다. 디폴트는 false 입니다.


4. GC 알고리즘 선택

대부분의 프로그램은 처리율 컬렉터와 동시 병렬 컬렉터 중에서 선택할 필요가 있습니다. 그 선택의 기준은 애플리케이션의 성능 목표에 따라 다릅니다.

CPU를 추가로 사용할 수 있는 경우에는 동시 병렬 컬렉터를 사용한다면 애플리케이션의 성능을 높일 수 있습니다.

여기서 핵심은 동시 병렬 GC 스레드의 백그라운드 처리에 있어서 CPU 이용 가능 여부입니다.

CPU 100%를 소비하는 단일 어플리케이션 스레드가 있는 단일 CPU 머신의 경우 애플리케이션이 처리율 컬렉터와 실행되면 GC가 주기적으로 수행되면서 애플리케이션이 정지됩니다.

이 어플리케이션이 동시 병렬 컬렉터와 실행된다면 운영체제는 때로는 어플리케이션을 실행하고 때로는 백그라운드 GC 스레드를 실행할 것입니다.

이 경우 동시 병렬 컬렉터의 경우 백그라운드 스레드의 오버헤드 때문에 성능상에 더 안좋을 것입니다.

이 원칙은 여러 개의 어플리케이션 스레드와 여러 개의 백그라운드 GC 스레드, 여러 개의 CPU가 있는 경우에도 적용됩니다.

백그라운드 GC 스레드가 애플리케이션 스레드와 같이 실행되면서 CPU를 받을 수 없다면 처리율 컬렉터보다 성능상으로 이점이 없을 것입니다.

다음 표는 이 트레이드 오프가 영향을 주는 방식을 보여줍니다.

표. 다른 GC 알고리즘을 쓰는 배치 처리 시간

GC 알고리즘CPU 4개 (CPU 효율)CPU 1개 (CPU 효율)
CMS78.09 초 (30.7% 효율)120.0 초 (100% 효율)
처리율81.00 초 (27.7% 효율)111.6초 (100% 효율)

위 어플리케이션의 경우 단일 어플리케이션 스레드로 동작한다고 가정합니다.

즉 이 말은 CPU 4개의 CMS 컬렉터의 경우 애플리케이션 스레드만 동작하는데 CPU는 25% 차지합니다.

나머지 5.7%는 백그라운드 GC 스레드가 동작했다고 생각할 수 있습니다.

CPU 4개의 처리율 컬렉터의 경우 애플리케이션 스레드가 동작하다가 풀 GC 타이밍에는 GC 스레드 4개가 동작할 것입니다. 그러므로 그 때 CPU는 100% 차지할 것입니다.

이와 같이 동작하더라도 전체 CPU 시간에는 2.7%만 수행합니다.

즉 CPU 4개를 사용할 수 있을 때 CMS는 처리율 컬렉터보다 3초 더 빠르게 수행할 수 있습니다.

단일 CPU만 사용할 수 있다면 GC 스레드나 애플리케이션 스레드나 둘 중 하나를 계속 수행할 것이므로 CPU는 항상 100% 차지할 것입니다.

이때 CMS 백그라운드 GC 스레드는 추가 오버헤드가 있을 것이므로 처리율 컬렉터보다 9초 더 느리게 끝날 것입니다.

여기서 한 가지 더 생각해볼 점은 CMS 컬렉터의 경우 백그라운드 GC 스레드가 더 많다고 해서 성능상에 이점은 없을 수 있습니다.

어짜피 애플리케이션 스레드는 계속 CPU를 차지하고 있으므로 작업의 수행량은 동일할 것입니다.

즉 CMS에서 풀 GC가 일어나지 않을 정도로 백그라운드 GC 스레드가 처리해주기만 한다면 백그라운드 GC 스레드는 적을수록 유리합니다.

GC 알고리즘과 처리율 테스트

처리율을 측정할 때 GC 알고리즘에서의 기본적인 트레이드 오프를 살펴보겠습니다.

이 테스트는 주식 서블릿을 이용한다고 가정하겠습니다. 이 서블릿은 2GB 힙을 사용하는 글래스 피쉬 인스턴스에서 사용되고

GC 시스템에 더 많은 스트레스를 주기 위해 이전 10개의 요청은 각 사용자의 HTTP 세션 내에 저장됩니다.

다음 표는 처리율 컬렉터와 CMS 컬렉터에서 수행될 때 테스트의 처리율을 보여줍니다. 이 테스트의 경우 CPU는 네 개 달린 머신에서 수행됩니다.

표. 다른 GC 알고리즘에서의 처리율

클라이언트 개수처리율 TPS (CPU 사용률)CMS TPS (CPU 사용률)
130.43 (29%)31.98 (31%)
1081.34 (97%)61.20 (85%)

클라이언트 개수가 1개인 경우 CMS는 사용할 수 있는 CPU가 있으므로 충분히 백그라운드 GC 스레드를 돌릴 수 있고 애플리케이션 스레드는 계속 작동할 것이므로 위의 예제와 비슷하게 CMS가 처리율 컬렉터보다 5% 더 높은 TPS를 제공할 것입니다.

클라이언트 개수가 10개인 경우 CMS는 사용할 수 있는 CPU가 제한이 되므로 충분한 백그라운드 GC 스레드를 돌릴 수 없습니다. 그러므로 동시 병렬 모드 실패(concurrent mode failures)가 일어나고 단일 스레드로 풀 GC를 수행할 것입니다. 이와 같은 이유로 CMS의 CPU 사용률은 요청이 많음에도 100%가 아닌 85%를 차지합니다.

GC 알고리즘과 응답 시간 테스트

다음 표는 요청 사이에 사고 시간이 250ms 걸리고 그 결과 29 TPS로 동일하게 처리되는 경우의 테스트를 보여줍니다.

그 다음 성능은 각 요청에서의 평균, 90%대, 99%대 응답 시간에서 측정합니다.

표. 다른 GC 알고리즘에서의 응답 시간

크기처리율CMS
평균90%대99%대CPU평균90%대99%대CPU
세션 크기 10 count0.0920.1710.81341%0.1040.2110.26046%
세션 크기 50 count0.1800.2183.61755%0.1070.2220.31553%

결과를 보면 평균 응답 시간과 90%대 응답 시간의 관점에서는 처리율 컬렉터가 동시 병렬 컬렉터보다 더 빠릅니다. 99%대 응답 시간에서는 CMS가 상당히 유리하다는 사실을 보여줍니다.

이와 같은 이유는 처리율의 경우에 풀 GC는 1%를 더 오래 걸리게 만들기 때문입니다.

CMS는 99%대 결과에서 성능을 향상 시키기 위해 CPU를 약 5%더 많이 사용합니다.

특히 처리율 컬렉터의 경우 아이템 50개의 경우 GC 주기는 더 영향을 많이 받습니다. 이때 처리율 컬렉터의 평균 응답 시간은 CMS 보다 더 느린데 이는 3초 이상 걸린 99%대 응답 시간의 이상점 때문입니다.

흥미롭게도 이 케이스의 경우에 CMS는 풀 GC를 수행하지 않았습니다. 즉 백그라운드 처리가 애플리케이션 요구사항에 적합한 경우입니다.

평균 시간만 고려한다면 처리율 컬렉터가 동시 병렬 컬렉터보다 유리할 수 있습니다.

99%대 응답 시간에 관심이 있다면 CMS 컬렉터가 더 유리할 수 있습니다.

CMS와 G1 중에서 선택

앞의 예시에서 동시 병렬 컬렉터로 G1이 아닌 CMS를 이용했습니다.

경험에 따르면 CMS는 힙 사이즈가 4GB보다 작은 경우에 G1보다 좋은 성능을 낼거라고 예상됩니다.

CMS는 G1보다 더 간단한 알고리즘이므로 힙이 작은 경우에 더 빠를 가능성이 있습니다.

CMS 백그라운드 스레드는 올드 제너레이션 전체를 살펴보면서 해제할 객체를 찾습니다. 그러므로 힙이 작을수록 성능이 좋습니다.

하지만 G1의 경우 올드 제너레이션 영역을 분담해서 처리하기 때문에 큰 힙이라면 G1이 더 빠를 것입니다.


5. 기본 GC 튜닝

비록 GC 알고리즘마다 힙을 처리하는 방식은 다르지만 기본적인 환경 설정 매개변수는 공유합니다.

힙 크기 정하기

GC의 첫 번째 기본 튜닝은 애플리케이션 힙 크기 입니다. 이는 힙의 제너레이션의 크기에 영향을 주는 고급 튜닝입니다.

여기서는 전체적인 힙 크기 설정에 다루겠습니다.

대부분의 성능 문제에서 힙 크기를 결정하는건 균형의 문제입니다. 힙이 너무 작다면 프로그램 GC를 수행하는 시간이 너무 길고 애플리케이션 로직을 수행하는데 충분한 시간을 쓸 수 없습니다.

하지만 단순히 매우 큰 힙을 쓰는것도 올바르지 않습니다. GC 중단 시간은 힙의 크기에 좌우되므로 힙의 크기가 늘어난다면 중단 시간도 늘어납니다.

즉 중단은 덜 일어나지만 한번 일어나면 크게 지연될 수 있습니다.

그리고 매우 큰 힙을 사용하는건 또 다른 위험이 있을 수 있습니다. 컴퓨터 운영체제는 머신의 물리 메모리를 관리하기 위해 가상 메모리를 사용합니다.

즉 가상 메모리를 통해 더 많은 가용 메모리가 있는 것처럼 보일 수 있습니다.

이와 같은 경우에 물리 메모리를 다 쓴다면 운영체제는 스와핑을 통해 프로그램의 비활성 영역을 디스크로 쓸 것 입니다. 그리고 이 부분이 다시 필요하다면 운영체제는 디스크에서 RAM으로 가져올 것입니다.

만약 풀 GC가 일어나는 경우에 스와핑이 일어난다고 가정하면 이 중단 시간은 몇배나 더 커질 수 있습니다. 백그라운드 스레드가 힙을 살펴보며 지나갈 때 디스크에서 메인 메모리로 데이터 복사가 늦어지면서 오래 대기하게 된다면 비용이 비싼 풀 GC를 겪습니다.

그러므로 힙 크기 설정에서 첫 번째 규칙은 머신 내의 물리적인 메모리의 크기보다 더 큰 힙을 지정하지 않는 것입니다. JVM이 여러 개 실행되고 있다면 모든 힙의 총합은 물리적인 메모리보다 항상 작아야 합니다.

그리고 다른 애플리케이션을 위한 메모리 뿐 아니라 JVM 자체를 위한 공간 일부를 남겨야 합니다. 일반적으로 OS 프로파일을 위해서 적어도 1GB 메모리 여분은 필요합니다.

힙의 크기는 두 가지 값인 초기 값(-XmsN)과 최대 값(-XmsN)에 의해 제어됩니다. 이에 대한 디폴트는 운영체제나 시스템 RAM의 크기 사용중인 JVM에 따라 다릅니다.

JVM은 가용 시스템 자원을 기반으로 힙에 대한 합리적인 디폴트 초기 값을 찾고 애플리케이션에 메모리가 더 필요하다면 힙 사이즈를 알아서 올립니다.

즉 힙의 초기 크기와 최대 크기가 있으므로 JVM은 GC가 너무 많이 일어난다면 알아서 힙을 지속적으로 늘립니다.

만약 그럼에도 불구하고 GC 시간이 많다면 -Xms 플래그를 통해 힙 크기를 늘릴 필요가 있습니다.

애플리케이션에서 필요로 하는 힙의 크기를 정확하게 알고 있다면 초기 값과 최대 값을 똑같이 하도록 적용한다면 힙 크기의 재조정을 할 필요가 없으므로GC는 약간 더 효율적이 됩니다.

제너레이션 크기 정하기

일단 힙 크기가 결정되면 그 다음 사항은 영 제너레이션에 힙을 얼마나 할당할지 올드 제너레이션에 얼마나 할당할지 결정해야 합니다.

영 제너레이션이 비교적 더 크다면 덜 자주 수집되고 더 적은 객체가 올드 제너레이션으로 갈 것입니다. 그러므로 풀 GC가 더 자주 일어납니다.

여기서도 중요한 점은 균형을 유지하는게 관건입니다.

GC 알고리즘마다 각자 다른 방식으로 균형을 유지하지만 모든 GC 알고리즘은 제너레이션 크기를 정하는데 공통적인 플래그를 사용합니다.

제너레이션의 크기를 튜닝하기 위한 커맨드 라인 플래그는 모두 영 제너레이션 크기를 조정합니다. 올드 제너레이션은 영 제너레이션을 뺀 나머지로 할당됩니다.

플래그는 다음과 같습니다.

  • -XX:NewRatio=N

    • 올드 제너레이션과 영 제너레이션의 비율 설정
  • -XX:NewSize=N

    • 영 제너레이션의 초기 크기 설정
  • -XX:MaxNewSize=N

    -영 제너레이션의 최대 크기 설정

  • -XmnN

    • NewSize와 MaxNewSize에 동일한 값을 설정

영 제너레이션은 디폴드 값 2인 NewRatio로 설정됩니다. 이를 바탕으로 초기 영 제너레이션의 크기를 정하는 방정식이 사용됩니다.

  • 초기 영 제너레이션 크기 = 초기 힙 크기 / (1 + NewRatio)

영 제너레이션의 크기는 NewSize로 설정할 수 있다고 했습니다. NewSize로 값을 세팅하면 NewRatio 방정식으로 나온 값보다 우선으로 선택됩니다.

병렬성 제어하기

시리얼 컬렉터를 제외한 모든 GC 알고리즘은 여러 개의 스레드를 사용합니다. 이 스레드의 개수는 -XX:ParallelGCThread=N 플래그로 제어됩니다.

이 플래그의 값은 다음 동작에서 사용되는 스레드의 개수에 영향을 미칩니다.

  • -XX:+UseParallelGC를 사용할 때 영 제너레이션 컬렉션

  • -XX:+UseParallelOldGC를 사용할 때 올드 제너레이션 컬렉션

  • -XX:+UseParNewGC를 사용할 때 영 제너레이션 컬렉션

  • -XX:+UseG1GC를 사용할 때 영 제너레이션 컬렉션

  • CMS의 모든 애플리케이션 스레드 중단 단계 (풀 GC 제외)

  • G1의 모든 애플리케이션 스레드 중단 단계 (풀 GC 제외)

이 GC 동작은 애플리케이션 스레드가 실행되는 걸 막습니다. 그러므로 JVM은 중지 시간을 최소화 하기 위해 가능한 많은 CPU 자원을 사용해 이를 극복하려고 합니다.

기본적으로 JVM은 한 머신내의 각 CPU당 한 스레드씩 8개를 기본으로 합니다. 그리고 임계치에 다다르면 JVM은 CPU 한 개마다 5/8씩 신규 스레드를 추가합니다.

이 방정식은 다음과 같습니다.

  • ParallelGCThreads = 8 + ((N - 8) * 5 / 8)

128-CPU에서 GC 스레드는 83개가 될 것입니다. 8개의 CPU가 달린 머신에서 8개의 스레드는 좀 많을 수 있습니다. 오히려 4~6개의 스레드가 더 적합할 수 있습니다.

게다가 머신에 한 개 이상의 JVM이 실행되고 있다면 GC 스레드 개수는 제한하는게 낫습니다.

한 예로 JVM 4개가 실행되고 있는 16개의 CPU에서는 한 JVM당 GC 스레드 개수는 13개가 됩니다. 그러므로 GC 작업할 땐 52개의 GC 스레드가 경쟁적으로 CPU를 얻으려고 할 것입니다.

이와 같은 경우에는 낭비이므로 한 JVM 당 GC 스레드 개수를 4개로 조정하는 것이 낫습니다.

그리고 여기 나온 플래그는 CMS나 G1이 사용하는 백그라운드 스레드 수를 설정하진 않습니다. 여기에 나올 상세 내용은 다음 챕터에서 다루겠습니다.

profile
좋은 습관을 가지고 싶은 평범한 개발자입니다.

0개의 댓글