[F-Lab 멘토링 학습]

자바 가비지컬렉션 종류와 동작원리

everydeveloper 2023. 11. 4. 15:32

자바 가비지컬렉션 종류와 동작원리

자바에서는 가비지 컬렉션(Garbage Collection, GC)이라는 메커니즘을 통해 자동으로 더 이상 사용되지 않는 메모리를 회수합니다. 여러 가지 GC 알고리즘이 존재하며, 각각은 특정 상황에서 장단점을 가집니다.

가비지 컬렉션의 주요 영역

  • 영구(Perm) 영역: 클래스와 메서드 정보가 저장됩니다.
  • 힙(Heap) 영역: 객체가 저장되는 공간입니다.
  • 스택(Stack) 영역: 지역 변수나 메서드 호출 정보가 저장됩니다.

주로 힙 영역에 대한 가비지 컬렉션이 이루어집니다.

가비지 컬렉션의 종류

  1. Serial GC: 단일 쓰레드를 사용해 Young 영역과 Old 영역을 차례로 정리합니다. 단순하고 메모리를 효율적으로 정리하지만, GC 작업 동안에는 애플리케이션 작업이 멈춥니다(Stop-the-world).
  2. Parallel GC: 여러 쓰레드를 사용해 Young 영역을 정리합니다. 멀티 쓰레드 환경에서 좋은 성능을 보이지만, Old 영역에 대한 처리는 Serial GC와 유사하게 이루어집니다.
  3. Concurrent Mark-Sweep (CMS) GC: 실시간 시스템에서 사용하기 적합하며, 애플리케이션의 중단 시간을 최소화하려고 설계되었습니다. 다만, 메모리 조각화 문제가 발생할 수 있고 CPU 사용량이 높습니다.
  4. G1 GC: Region-based 방식을 사용해 메모리를 관리합니다. CMS의 문제를 해결하고 더 예측 가능한 중단 시간을 제공하기 위해 설계되었습니다.

동작 원리

  1. Mark: 사용되고 있는 객체를 식별합니다.
  2. Sweep: Mark 단계에서 식별되지 않은 객체를 메모리에서 제거합니다.
  3. Compact: 메모리 공간을 재구성하여 객체들을 연속적인 위치로 이동시킵니다(일부 GC 알고리즘에서만 적용됩니다).

중단 시간(Stop-the-World)

가비지 컬렉션 동작 중 일정 시간 동안은 애플리케이션의 모든 작업이 멈추게 됩니다. 이를 "Stop-the-World" 이벤트라고 하며, 각 GC 알고리즘은 이 시간을 최소화하는 방향으로 설계되고 있습니다.

가비지 컬렉션은 편리하지만 100%의 메모리 관리를 보장하지는 않습니다. 따라서 개발자는 여전히 메모리 누수 등의 문제에 대한 대비가 필요합니다.

많이 쓰이는 가비지 컬렉션의 종류는?

자바 환경에서 가장 널리 사용되는 가비지 컬렉션(GC) 알고리즘은 대체로 다음과 같습니다:

  1. Parallel GC: 서버 환경에서 많이 사용됩니다. 이 알고리즘은 멀티 쓰레드를 활용해 Young 영역을 빠르게 정리할 수 있습니다. 일반적으로 CPU 코어가 많은 환경에서 뛰어난 성능을 보입니다.
  2. Concurrent Mark-Sweep (CMS) GC: 실시간 트랜잭션이 중요한 시스템에서 선호됩니다. CMS는 애플리케이션의 중단 시간을 최소화하려고 설계되었기 때문입니다. 하지만 메모리 조각화와 높은 CPU 사용량이 문제가 될 수 있습니다.
  3. G1 GC: Java 7 이후로 소개되었고, Java 9부터 기본 GC로 설정되었습니다. 이 알고리즘은 높은 힙 메모리와 더 예측 가능한 응답 시간을 필요로 하는 대규모 시스템에서 유용합니다. G1은 여러 개의 작은 영역으로 나뉜 힙을 사용하여 효율적으로 메모리를 관리합니다.

Parallel GC가 전체적으로 빠르고 효율적이긴 하지만, 일부 시나리오에서는 CMS나 G1이 더 적합할 수 있습니다. CMS는 더 낮은 중단 시간을 가지지만, 메모리 조각화가 발생할 수 있습니다. G1은 메모리 관리가 더 복잡하지만, 큰 힙 크기와 낮은 중단 시간을 동시에 달성할 수 있습니다.

따라서 사용하는 애플리케이션의 특성과 요구 사항에 따라 적절한 GC 알고리즘을 선택해야 합니다.

메소드 영역엔 어떤 것들이 저장되어 있어?

자바의 메소드 영역(Method Area)는 클래스 로더(Class Loader)에 의해 클래스가 최초로 메모리에 로드될 때 생성되는 정보들을 저장합니다. 다음과 같은 종류의 정보가 메소드 영역에 저장됩니다.

  1. 클래스 정보: 클래스의 접근 제어자, 이름 등의 기본 정보가 저장됩니다.
  2. 필드 정보: 클래스에 정의된 필드(멤버 변수)의 이름, 타입, 접근 제어자 등이 저장됩니다.
  3. 메소드 정보: 메소드의 시그니처(이름, 매개변수 타입 등), 반환 타입, 접근 제어자, 바이트코드 등이 저장됩니다.
  4. 상수 풀(Constant Pool): 클래스 파일 내의 모든 리터럴과 상수 값이 저장되는 풀입니다. 이는 메소드의 코드에서 사용되는 문자열과 기본 타입의 상수 값들도 포함합니다.
  5. 인터페이스 정보: 해당 클래스가 구현하는 인터페이스에 대한 정보가 저장됩니다.
  6. 자료형 정보: 배열 등의 자료형과 관련된 정보도 저장될 수 있습니다.
  7. 런타임 상수 풀(Runtime Constant Pool): 실행 도중 필요한 상수를 저장하는 풀로, 상수 풀의 런타임 버전이라고 생각할 수 있습니다.
  8. 코드와 특수 메서드: static initializer나 생성자 정보 등도 이 영역에 저장됩니다.

메소드 영역은 힙(Heap) 영역과는 달리 JVM이 자동으로 가비지 컬렉션을 수행하지 않거나, 아주 드물게 수행합니다. 따라서 메소드 영역에서의 메모리 누수는 일반적으로 발생하지 않으며, 주로 힙 영역에 대한 가비지 컬렉션이 더 중요한 주제입니다.

힙 메모리엔 어떤 것들이 저장이 되어 있어?

힙(Heap) 메모리는 자바 애플리케이션에서 동적으로 할당되는 데이터를 저장하는 공간입니다. 힙 영역은 여러 섹션으로 나뉘며, 각각은 특정 목적을 가집니다. 힙 영역에는 다음과 같은 것들이 저장됩니다:

  1. 객체: 클래스의 인스턴스가 힙 영역에 생성됩니다.
  2. 배열: 기본 데이터 타입이나 객체의 배열도 힙 영역에 할당됩니다.
  3. 인스턴스 변수: 객체의 인스턴스 변수들이 저장되며, 이 변수들은 해당 객체의 생명주기와 연결됩니다.
  4. 문자열 풀: String 객체가 상수로 사용되는 경우 이들은 힙 영역 내의 문자열 풀에 저장됩니다.

힙 영역의 섹션

  1. Young Generation: 새로 생성된 객체들이 먼저 올라가는 영역입니다. Young Generation은 다시 Eden 영역과 두 개의 Survivor 영역(S0, S1)으로 나뉩니다.
  2. Old Generation (Tenured): Young 영역에서 살아남은 객체들이 이동하는 영역입니다. 여기 있는 객체들은 일반적으로 애플리케이션의 생명주기 동안 계속 존재하게 됩니다.
  3. Permanent Generation (PermGen) 또는 Metaspace: JDK 7까지는 클래스 메타데이터가 Permanent Generation에 저장되었으나, JDK 8 이후로는 Native 메모리 영역인 Metaspace로 이동했습니다.

힙 메모리는 가비지 컬렉션(GC)의 대상이며, 더 이상 참조되지 않는 객체는 GC에 의해 자동으로 회수됩니다. 하지만 GC가 모든 메모리 누수 문제를 해결해주는 것은 아니므로, 개발자가 직접 메모리 관리에 신경을 써야 합니다.

Heap영역은 처음 설계될 때 다음의 2가지를 전제 (Weak Generational Hypothesis)로 설계되었다. 라는데 이렇게 생각하고 만드신건가 아니면 이렇게 되도록 만드신건가

힙 영역의 설계에서 주로 고려되는 "Weak Generational Hypothesis"는 다음 두 가지 전제를 기반으로 합니다:

  1. 대부분의 객체는 금방 접근 불가능 상태가 된다: 즉, 많은 객체들이 빠르게 생성된 후 더 이상 사용되지 않게 되고, 가비지 컬렉션의 대상이 된다는 것입니다.
  2. 오래된 객체에서 젊은 객체로의 참조가 적다: 이는 젊은 객체(Young Generation)와 노화된 객체(Old Generation) 사이의 상호 작용이 상대적으로 적다는 것을 의미합니다.

이러한 전제들은 경험적 관찰과 연구를 통해 얻어진 인사이트를 기반으로 합니다. 따라서 이것은 "이렇게 생각하고 만든 것"이라기보다는 "대부분의 애플리케이션에서 이렇게 동작한다는 경험적 증거가 있으므로 이렇게 설계되었다"고 보는 것이 더 정확할 것입니다. 이러한 전제들은 가비지 컬렉션의 효율을 높이기 위한 방법으로 활용되며, 실제로 많은 자바 애플리케이션에서 이러한 가정이 유효함이 증명되고 있습니다.

이런 전제조건들을 기반으로 힙 영역은 Young Generation과 Old Generation으로 나뉘며, 각각의 섹션은 특정한 방식으로 가비지 컬렉션을 수행합니다. 이 설계 방식은 가비지 컬렉션의 오버헤드를 줄이고, 성능을 향상시키는 데 도움을 줍니다.

[ Java 8 에서의 Permanent ]

Permanent는 직역하면 영구적인 세대의 의미로서, 생성된 객체들의 정보의 주소값이 저장된 공간이다. 클래스 로더에 의해 load되는 Class, Method 등에 대한 Meta 정보가 저장되는 영역이고 JVM에 의해 사용된다. Java 7 까지는 힙 영역에 존재했지만 Java 8 버전 이후에는 Native Method Stack에 편입되게 된다. 이렇게 바뀐 이유와 목적은?

Java 8 이후로 Permanent Generation(PermGen) 영역이 제거되고, 그 대신 Metaspace라는 새로운 영역이 Native 메모리에 도입된 이유와 목적은 여러 가지가 있습니다:

메모리 제한 해소

  1. 메모리 제한 해소: PermGen 영역은 힙 메모리의 일부였기 때문에 그 크기는 정적으로 할당되어 있었습니다. 이로 인해 메타데이터의 크기가 PermGen의 크기를 초과하면 OutOfMemoryError가 발생했습니다. Metaspace는 네이티브 메모리를 사용하므로 이러한 제약을 훨씬 더 유연하게 다룰 수 있습니다.

가비지 컬렉션 효율성

  1. 가비지 컬렉션 효율성: PermGen 영역이 힙의 일부였기 때문에, 가비지 컬렉션(GC)의 대상이 될 수 있었습니다. 이는 GC의 복잡성을 높였습니다. Metaspace 도입으로 이러한 문제가 개선되었습니다.

성능 개선

  1. 성능 개선: Metaspace는 클래스 메타데이터를 저장하기 위한 특별한 영역이기 때문에, PermGen이 가지고 있던 일반적인 객체와는 다른 수명 주기를 가집니다. 이로 인해 더 효율적인 메모리 관리와 성능 개선이 이루어질 수 있습니다.

유연성과 확장성

  1. 유연성과 확장성: 네이티브 메모리를 사용함으로써, JVM이 더 유연하고 확장 가능한 메모리 관리를 할 수 있게 되었습니다. 이는 특히 메타데이터의 동적 로딩과 언로딩이 빈번한 애플리케이션에서 유용합니다.

이러한 이유로, Java 8에서는 PermGen을 Metaspace로 대체하여 여러 가지 문제를 해결하고 성능을 향상시켰습니다.

마이너 GC가 일어나는 중 survivor 0이나 1이 만약 꽉차게 된다면?

마이너 GC(Minor Garbage Collection)가 발생할 때, Survivor 영역(S0 또는 S1)이 꽉 차게 된다면, 여러가지 처리 방법이 있습니다:

  1. Old Generation으로 이동: Survivor 영역이 꽉 찼을 때, 그곳에 있던 객체들 중 아직 살아있는 객체들은 Old Generation 영역으로 프로모션될 수 있습니다. 이렇게 하면 Survivor 영역의 공간을 확보할 수 있습니다.
  2. 크기 조정: 일부 JVM 구현체에서는 Survivor 영역의 크기를 동적으로 조정할 수 있습니다. 그러나 이는 상황에 따라 다르며, 일반적으로는 Old Generation으로의 프로모션을 통해 문제를 해결합니다.
  3. Full GC 발생: 이러한 상황이 계속되면, 마지막으로 Full GC가 발생할 수 있습니다. Full GC는 Young Generation뿐만 아니라 Old Generation 영역까지 모두 청소합니다. 이로 인해 시스템 전체의 성능에 부담을 주지만, 메모리를 효과적으로 정리할 수 있습니다.
  4. OutOfMemoryError: 위의 방법들로도 문제를 해결할 수 없다면, 결국 OutOfMemoryError가 발생할 수 있습니다.

보통은 Old Generation으로의 프로모션을 통해 이 문제를 해결하려고 시도합니다. 그러나 이렇게 해도 Old Generation 영역이 과도하게 증가한다면, 전체적인 힙 메모리 크기를 조정해야 할 필요가 있을 수 있습니다. 이는 JVM 구동 옵션을 통해 설정할 수 있습니다.

보통의 힙 메모리는 용량 얼마 할당해? 2gb? 4gb?

힙 메모리의 할당량은 애플리케이션의 요구사항, 실행 환경, 그리고 가용한 시스템 리소스에 따라 달라집니다. 일반적인 설정에서는 JVM이 자동으로 힙 크기를 결정합니다. 그러나 명시적으로 설정할 수도 있고, 서버나 높은 트래픽을 다루는 애플리케이션에서는 더 큰 힙 메모리를 할당하는 경우도 있습니다.

  • 작은 애플리케이션: 일반적으로 수백 MB 정도가 할당됩니다.
  • 일반적인 엔터프라이즈 애플리케이션: 보통 2GB~4GB의 힙 메모리를 사용하는 경우가 많습니다.
  • 데이터 집약적인, 높은 트래픽의 애플리케이션: 8GB, 16GB, 32GB 또는 그 이상의 힙 메모리를 할당하는 경우도 있습니다.

JVM의 -Xmx와 -Xms 옵션을 통해 힙 메모리의 최대치와 시작 시 할당량을 설정할 수 있습니다. 예를 들어,

  • Xmx4g은 최대 힙 크기를 4GB로 설정합니다.
  • Xms2g은 초기 힙 크기를 2GB로 설정합니다.

이러한 설정은 애플리케이션의 성능과 가용 메모리에 따라 튜닝할 수 있습니다.

g1 gc의 상세 알고리즘 설명 부탁

G1(Garbage First) GC는 Java의 대용량 힙을 빠르고 효율적으로 관리하기 위한 가비지 컬렉션 알고리즘 중 하나입니다. G1 GC는 다음과 같은 특징과 알고리즘을 가집니다.

특징

  1. 예측 가능한 지연 시간: G1 GC는 짧고 예측 가능한 휴지(pause) 시간을 목표로 합니다.
  2. 분할 영역: 힙을 여러 개의 동일한 크기의 작은 영역(region)으로 나눕니다.
  3. 병렬성과 병목 최소화: 멀티 프로세서와 멀티 쓰레드 환경에서도 효율적으로 동작합니다.

알고리즘 동작 과정

  1. Initial Mark(초기 표시): G1 GC는 루트에서 시작해 각 영역에서 살아 있는 객체를 찾고 표시합니다. 이 단계는 STW(Stop-The-World) 이벤트입니다.
  2. Concurrent Mark(동시 표시): 이 단계에서는 살아 있는 객체와 그 참조를 다시 표시합니다. 이 작업은 애플리케이션 쓰레드가 동시에 실행되는 동안 수행됩니다.
  3. Final Marking(최종 표시): 참조 변경이나 새로 할당된 객체 등을 반영해 다시 한 번 표시 작업을 수행합니다. 이 단계도 STW 이벤트입니다.
  4. Cleanup(정리): 쓰레기가 되는 객체를 실제로 삭제하거나 재사용을 위해 메모리를 정리합니다.
  5. Evacuation(대피): 살아 있는 객체를 한 영역에서 다른 영역으로 이동합니다. 이는 살아 있는 객체의 메모리를 연속적으로 만들어, 이후 객체 할당을 더 효율적으로 만듭니다.

가비지 수집 순서

G1 GC는 이름에서도 알 수 있듯이, 가비지가 가장 먼저 수집되는 영역부터 처리합니다. 이는 여러 영역에서 가장 쓰레기가 많은 영역을 먼저 청소하므로, 가장 효율적인 청소가 가능하게 합니다.

G1 GC는 이러한 알고리즘과 동작 과정을 통해 높은 힙 메모리와 높은 처리량을 필요로 하는 현대의 대규모 애플리케이션에 적합한 가비지 컬렉션 성능을 제공합니다.

GC 튜닝

GC(Garbage Collection) 튜닝은 애플리케이션의 성능과 응답 시간을 개선하기 위한 과정입니다. 각 가비지 컬렉터(GC)에 따라 다양한 튜닝 옵션과 접근 방법이 있습니다. 여기에는 일반적인 몇 가지 방법을 소개합니다.

기본 전략

  1. 힙 크기 조절: JVM의 Xmx와 Xms 옵션을 사용하여 힙 메모리의 최대 크기와 시작 크기를 설정할 수 있습니다. 힙 크기를 크게 할당하면 GC 발생 빈도가 줄어들지만, 메모리 사용량이 늘어납니다.
  2. 영역 비율 설정: Young Generation과 Old Generation의 비율을 조절하여 GC 동작을 튜닝할 수 있습니다. XX:NewRatio 옵션을 사용합니다.
  3. Survivor 영역 크기 조절: XX:SurvivorRatio 옵션을 사용하여 Survivor 영역의 크기를 조절할 수 있습니다.

GC 알고리즘 선택

  • Serial GC: 단일 쓰레드 환경에서 사용. XX:+UseSerialGC
  • Parallel GC: 병렬 처리에 적합. XX:+UseParallelGC 또는 XX:+UseParallelOldGC
  • CMS(Concurrent Mark-Sweep) GC: 낮은 휴지 시간을 요구할 때. XX:+UseConcMarkSweepGC
  • G1 GC: 높은 힙 메모리와 빠른 응답 시간이 필요할 때. XX:+UseG1GC

세부 튜닝

  1. Thread 수 조절: XX:ParallelGCThreads=n 옵션으로 GC를 수행하는 쓰레드의 수를 조절할 수 있습니다.
  2. GC 로깅 활성화: GC 로깅을 활성화하여 (XX:+PrintGCDetails, XX:+PrintGCDateStamps 등의 옵션 사용) 성능을 모니터링하고 분석할 수 있습니다.
  3. 휴지 시간 목표 설정: G1 GC의 경우, XX:MaxGCPauseMillis=n 옵션을 사용하여 GC 휴지 시간의 최대 값을 설정할 수 있습니다.
  4. 올드 영역의 점유율을 모니터링하여 GC 발생 조건 설정: XX:CMSInitiatingOccupancyFraction=n 옵션을 사용하여 CMS GC가 발생할 Old 영역의 점유율을 설정할 수 있습니다.

GC 튜닝은 애플리케이션의 성격, 메모리 사용 패턴, SLA(Service Level Agreement) 등 여러 요인을 고려해야 합니다. 튜닝 작업 전후로 성능을 반드시 테스트하여, 변경 사항이 성능에 미치는 영향을 평가하는 것이 중요합니다.

Shenandoah GC Z Garbage Collector 자세히

특징:

  1. 저지연(Low Latency): Shenandoah는 주로 응답 시간을 최적화하는 데 중점을 둡니다.
  2. 독립적인 힙 분할: 힙을 크게 두 부분으로 분할하지 않고, 모든 곳에서 가비지 컬렉션을 수행합니다.
  3. Concurrent Compaction: 메모리 정리(compaction)를 애플리케이션 쓰레드가 실행 중인 동안 병렬로 수행합니다.

동작 방식:

  1. Initial Mark: 처음으로 Root에서 시작해서 참조되는 객체를 마킹합니다. (STW)
  2. Concurrent Mark: Root에서 찾은 객체들을 기준으로 참조되는 객체를 찾습니다. (Concurrent)
  3. Concurrent Evacuation: 애플리케이션과 동시에 라이브 객체를 새로운 위치로 이동합니다.
  4. Concurrent Update Reference: 객체의 참조를 업데이트합니다. (Concurrent)
  5. Final Update and Reclamation: 마지막으로 참조를 업데이트하고 불필요한 객체를 삭제합니다. (Concurrent)

Z Garbage Collector(ZGC)

특징:

  1. 저지연: 목표 휴지 시간은 10ms 미만입니다.
  2. 병렬 처리: GC 작업을 병렬로 수행합니다.
  3. 큰 힙 지원: 테라바이트 수준의 힙 메모리도 효율적으로 관리할 수 있습니다.

동작 방식:

  1. Mark: 루트 세트에서 시작하여 살아있는 객체를 표시합니다. (STW)
  2. Relocate: 살아있는 객체를 새로운 위치로 이동하고, 참조를 업데이트합니다. (Concurrent)
  3. Remap: 모든 포인터 필드를 새로운 주소로 업데이트합니다. (STW)
  4. Compact: 더 이상 사용되지 않는 메모리를 반환합니다.

사용 방법:

  • Shenandoah GC를 사용하려면 JVM 옵션에 XX:+UseShenandoahGC를 추가합니다.
  • ZGC를 사용하려면 JVM 옵션에 XX:+UseZGC를 추가합니다.

두 GC 모두 저지연을 목표로 하지만, Shenandoah는 더 적은 메모리 오버헤드와 함께 더 낮은 휴지를 제공하는 반면, ZGC는 큰 힙 메모리를 효율적으로 관리하는 데 더 특화되어 있습니다. 사용 사례와 요구 사항에 따라 적절한 GC 알고리즘을 선택하는 것이 중요합니다.

구세대 GC도 요즘에도 쓰나? 최신일 수록 더 성능이 좋은 것 아냐?

구세대(Genesis) GC 알고리즘은 많이 사용되지 않고 있으며 대부분의 현대 시스템에서는 더 진보된 GC 알고리즘을 사용하고 있습니다. 그러나 구세대 GC가 전혀 사용되지 않는 것은 아닙니다. 이유는 다음과 같습니다.

  1. 간단하고 안정적: 구세대 GC 알고리즘은 단순하므로 이해하거나 디버깅하기가 더 쉽습니다.
  2. 특정 사용 사례에 적합: 특정 사용 사례에서는 구세대 GC가 더 효과적일 수 있습니다.
  3. 레거시 시스템: 이미 구세대 GC를 사용하고 있던 시스템이 있을 수 있으며, 이런 경우 변경하기가 어렵거나 리스크가 있을 수 있습니다.

최신 GC 알고리즘은 일반적으로 더 뛰어난 성능과 기능을 제공하나, 그것이 모든 상황에서 더 나은 것은 아닙니다. 가비지 컬렉션은 애플리케이션의 특성(응답 시간, 메모리 사용량, CPU 사용 등)에 따라 크게 영향을 받으므로, 애플리케이션의 요구 사항에 가장 적합한 GC 알고리즘을 선택하는 것이 중요합니다.

스프링 부트 최신에서 기본 GC는 JDK 버전에 따라 다르겟네

네, 맞습니다. 스프링 부트 자체는 자바 가비지 컬렉터(GC)를 직접 제어하지 않습니다. 대신, 스프링 부트 애플리케이션을 실행하는 JDK 버전에 내장된 GC를 사용하게 됩니다.

예를 들어, JDK 8을 사용한다면 기본적으로 Parallel GC가 사용됩니다. JDK 9 이상에서는 G1 GC가 기본으로 설정되어 있습니다.

JDK 버전별로 기본 GC가 다를 수 있으므로, 특정 GC를 사용하려면 JVM 실행 옵션을 통해 지정해야 할 수 있습니다. 예를 들어, G1 GC를 사용하려면 다음과 같은 JVM 옵션을 설정할 수 있습니다.

-XX:+UseG1GC

애플리케이션의 성능과 요구 사항에 따라 적절한 GC를 선택하고, 필요한 경우 JVM 옵션으로 세부 설정을 할 수 있습니다.