JVM에서 GC 종류별 작동방식

이세민·2024년 12월 2일

JVM 메모리 구조

GC가 작동하는 방식을 알아보기전, JVM의 메모리 구조가 어떻게 되는지 알아보자.

  1. Method Area
    모든 스레드가 공유하는 메모리 영역이다.
    클래스, 인터페이스, 메소드, 필드, static 변수등에 대한 정보를 저장한다.
    Method Area는 JVM 명세에서 정의된 추상적인 개념이고, 실제로는 Java 버전에 따라 두가지 방식으로 구현된다.
    Java 7이전까지 Permanent로 구현되었고, Java 8이후로는 Metaspace를 통해 구현되었다.
    Permanent의 경우 jvm heap에 포함되어있어 크기에 제한이 있었고, 따라서 static 변수에 많은 양의 정보를 저장하다보면OutOfMemoryError: PermGen space 에러가 발생할 수 있다.
    반면 Metaspace는 jvm heap이 아닌 os의 native 메모리에서 관리되며, 그에 따라 크기를 유동적으로 바꿀 수 있어 OutofMemoryError 발생을 크게 줄일 수 있다.
  2. Heap Area
    모든 스레드가 공유 가능하고 Java에서 생성된 객체들이 저장되는 영역이다. Method Area에 로드된 클래스만 생성할 수 있고 GC가 필요없는 객체들을 제거한다.
  3. Stack Area
    메서드 호출마다 스택 프레임을 생성하고, 그곳에 메서드안에서 사용되는 변수, 매개변수, 리턴값 등을 저장한다. 메서드의 수행이 끝나면 해당하는 프레임을 삭제한다.
  4. PC Register
    Program Counter Register의 약자로, 현재 실행중인 바이트코드 명령의 주소를 저장한다.
  5. Native Method Stack
    네이티브 코드(메서드)가 실행될 때의 정보를 저장하는 Stack area 역할을 한다.

GC 종류별 작동방식

기본 개념 정리.

  1. stop the world
    GC를 수행하는 동안 JVM의 코드 실행이 멈추는 시간, 상태
  2. mark
    살아있는 객체를 표시하는 것
  3. sweep
    mark되지 않은 객체를 메모리에서 삭제하는 것
  4. 메모리 단편화
    sweep후 남은 메모리 공간은 많으나, 중간중간 객체가 저장되어 메모리가 작게 잘려있는 상태
    큰 크기의 객체를 저장할 수 없는 문제를 일으킨다.
  5. compaction
    sweep후 단편화된 메모리를 정리하여 압축하는 것
    메모리 단편화를 해결하는 방법이다.

0. Mark and Sweep

jdk 1.1 이전에 사용하던 오래된 gc

사용중인 객체를 mark 하고 mark되지 않은 객체를 sweep을 통해 제거한다.
그림에서도 보이듯 sweep한후 메모리는 실제로 11칸의 여유 공간이 있지만 실질적으로 연속되게 저장 가능한 최대 공간은 3칸으로 그 이상의 객체가 생성되면 에러를 뱉게 된다. 이 문제를 메모리 단편화라고 한다.

1. Serial GC

가장 기본적인 방식의 GC
jdk 1.2에 추가 되었다.

Heap에서 아직 사용하는 객체를 Mark한 후 Mark되지 않은 객체를 Sweep하여 삭제한다.
그 후 Compaction을 통해 메모리 단편화를 해결한다.

Generational GC

Serial GC에서는 Generational GC라는 개념이 도입되어 효율적인 GC 처리가 가능하게 되었고, Generational GC는 현재까지 사용되는 GC 수행 개념이다.
Generational GC는 대부분의 객체가 많이 사용되기전 버려진다는 점을 활용해 적게 사용된 객체, 많이 사용되는 객체를 분류하여 관리하는 방식이다. 이를 통해 일반적인 상황에서는 Minor GC로 stop the world 시간을 최소화 한다.

Generational GC에서는 Heap을 Young Genetion, Old Generation으로 나눈다. Young Generation에서는 생성된지 얼마 되지 않은 객체들이 저장되며 Old Generation에서는 오랬동안 생성된 객체들이 저장된다.
Young Generation은 다시 Eden과 두개의 Survivor 공간으로 나뉜다.
Survivor가 두개인 이유는 Compaction 작업을 쉽게 하기 하도록 위함이다.
Survivor는 항상 하나는 비어있는 상태, 다른 하나는 메모리 단편화 없이 객체들이 저장된 상태를 유지한다.
1. Eden에서는 처음 생성된 객체가 저장된다.

  1. Minor GC가 한번 실행되면 Survivor중 하나로 이동한다.

  2. GC가 실행될때 비어있지 않은 Survivor 한쪽에서 Mark, Sweep을 한후 남은 객체들을 비어있는 Survivor로 Copy 함으로써 Compaction을 수행한다.

  3. 이렇게 Minor GC가 실행될 때마다 객체들은 survivor들을 왔다갔다하며 15번 Minor GC를 겪은 후 Old Generation으로 이동된다.

Old Generation에서 수행되는 GC를 Major GC라고 하는데, 대부분의 객체는 Young Generation에서 사라지기 때문에 Major GC는 Old Generation의 메모리 공간이 부족할 때만 발생한다. Major GC는 모든 객체를 Scan, Mark, Sweep, Compaction 한다. Survivor 처럼 Copy를 통한 Compaction이 불가능 하기 때문에 Compatcion이 복잡하다. 이러한 방식을 Full GC라고 부르며, 과정에서 보이듯 Full GC는 느리고 시간이 오래 걸린다.

2. Parallel GC

Parallel GC는 Serial GC에 멀티 스레딩을 추가한 GC이다.
멀티스레딩을 통해 GC의 수행시간을 줄였고, 그에 따라 stop the world의 시간이 줄어들었다.

3. CMS GC

stop the world의 시간을 최소화 하기 위해 고안된 방법이다.
Minor GC는 Parallel GC와 같이 Eden, Survivor를 사용하여 실행된다.
CMS의 Major GC에서는 Concurrent Mark, Concurrent Sweep을 사용하는데, 애플리케이션의 작업을 멈추지 않고 GC 작업과 병렬로 수행 할 수 있다.
물론 stop the world가 아예 사라진 것은 아니다.
CMS에서 마킹은 Initial Mark - Concurrent Mark - Final Mark 순서대로 실행되는데, Inital Mark에서는 GC에서 애플리케이션에서 직접적으로 참조하는 Root 객체들을 마킹한다. Concurrent Mark는 애플리케이션이 실행되는 동시에 Root 객체들이 참조중인 모든 객체들을 Mark한다. Final Mark에서는 Concurrent Mark에서 놓친 살아있는 객체들을 Mark한다.
이름에서 처럼 Concurrent Mark에서는 stop the world가 발생하지 않지만 Initial Mark와 Final Mark에서는 짧은 시간의 stop the world가 발생한다.
CMS GC에서는 sweep 후에 compaction을 진행하지 않아 stop the world가 inital mark, final mark에서만 발생하는 것이 특징이다. 하지만 메모리 단편화로 인해 old generation에 객체를 추가할 수 없는 상태가 되면 serial GC, parallel GC에서 쓰이던 Full GC를 실행하여 이를 해결해야한다. 이때 stop the world가 비교적 긴시간 발생하게 된다.

4. G1 GC

현재 Java의 기본 GC로 채택되어 있는 GC이다.
기존처럼 heap을 eden, survivor, old로 잘라서 관리하지 않고, heap 메모리 전체를 Region이라는 단위로 관리한다. 2048개의 Region을 가질 수 있고, 각각의 Region의 크기는 1MB~32MB 사이이다. 각각의 Region에 역할을 부여하여 기존 Generational GC를 구현한다.

G1 GC에서 새로 추가된 Humongous는 Region크기의 절반이 넘는 크기를 가진 객체를 말하며 young generation을 거치지 않고 Old 구역에 저장된다. 경우에 따라 여러개의 Old 구역에 걸쳐 저장될 수 있다.
Available/Unused는 비어있는, 사용가능한 Region이다.
+편의상 Empty로 지칭하겠습니다..

G1 GC에서는 효율적인 처리를 위해 Minor GC, Full GC 뿐만 아니라 young generation을 old generation과 같이 처리하는 Mixed GC도 사용된다.

IHOP(Initial Heap Occupancy Percent)은 Old 구역의 메모리 사용률에 제한을 나타낸다. 만약 Old 구역의 사용률이 IHOP 보다 높다면 Minor GC 대신 Mixed GC가 실행된다.

Mixed GC는 MaxGCPauseMillis를 넘지 않는 선에서 Young generation과 일부 Old 구역을 포함하여 실행된다. Old 구역중 Mixed GC를 수행할 Old 구역들은 구역의 Garbage 비율에 따라 선정되며 Garbage 비율을 알기 위해서 마킹 과정엔 Old 구역 전체가 포함된다. Garbage 비율이 일정 값 이상이면 순차적으로 MaxGCPauseMillis(gc로 인한 stop the world의 최대 시간)을 넘지 않는 선에서 Mixed GC 대상에 포함시킨다.
+ 이렇게 Mixed GC에서 GC를 수행할 대상들을 담은 Set을 CSet(Collection Set)이라고 합니다.

Mixed GC는 Old 구역의 사용률이 IHOP를 넘으면 Concurrent Mark Cycle을 통해 살아있는 객체를 마크한다. Concurrent Mark Cycle이 끝나면 위 설명처럼 일부 Old 구역을 CSet에 추가한다. CSet이 완성되면 마크된 객체를 빈 region으로 copy하고, 원래 region은 비운다.

G1 GC에서는 Mixed GC에도 불구하고 Heap 공간이 부족해지면 Full GC가 실행된다. Full GC는 싱글스레드로 동작하기 때문에 stop the world 시간이 길어진다. Adaptive IHOP 옵션이 켜져있다면 jvm에서는 자동으로 IHOP의 값을 조정하여 Full GC의 발생 빈도를 줄인다.

5. Shenandoah GC

stop the world의 시간을 최소화 하기 위해 고안된 방식이다.
다른 GC들과 다르게 Heap을 young & old으로 구분짓지 않고 하나로 관리한다. Region은 여전히 존재하지만 64GB의 heap을 지원했던 G1과 달리 최대 2TB의 Heap을 지원한다.
각각의 Region은 독립적으로 GC가 작동되며 작동 방식은 다음과 같다

  1. initial-concurrent-final mark를 통해 참조되는 객체를 마킹한다.
    이때 initial, final 단계에서 짧은 stop the world 가 발생한다.
  2. 마킹된 객체들을 다른 region으로 복사한다. 이 작업역시 application과 동시에 진행되는데, 동시성 문제를 해결하기 위해 brooks pointer라는 추가 포인터를 사용하여 이동과 동시에 포인터를 리디렉션한다.

이렇게 대부분의 GC 과정을 애플리케이션과 함께 진행하여 stop the world의 시간을 최소화 하는것이 Shenandoah GC의 특징이다. 그럼에도 불구하고 cpu 자원을 많이 사용하여 아직 기본 GC로 채택되진 못했다.

6. Epsilon GC

개발용으로 사용되는 GC 옵션중 하나로, 메모리 할당은 진행하지만
Garbage 수집은 하지 않는다.
당연히 실행 속도는 빨라지겠지만, 금방 jvm에서 메모리가 부족하여 멈추게 될것이다.

마무리


지금까지 설명했던 각 GC별 특징이 위 사진 하나로 정리된다.

JVM 메모리 구조와 GC 작동 방식에 대해 알아보았는데, 사실 제대로 이해했는지는 아직 의문이다. 나중에 다시 글을 고쳐가며 알아가야 할 것 같다.

profile
gsm 8기 고등학생

0개의 댓글