[Java] JVM Garbage Collection(GC)

clean·2024년 1월 9일
0
post-thumbnail

Garbage Collection(GC)

Garbage Collection(GC)는 자바의 메모리 관리 방법 중 하나로 JVM의 Heap 영역에서 동적으로 할당했던 메모리 중 더이상 필요없는 메모리 객체(garbage)를 모아 주기적으로 제거(메모리 해제)해주는 프로세스이다.

C/C++ 언어에서는 GC가 없어 프로그래머가 수동으로 메모리 할당/해제를 해주어야 했지만, Java는 JVM의 GC가 알아서 메모리 관리를 해주기 때문에 편리하다는 장점이 있다.

다음 코드를 보자.

    for(int i=0; i<100000; i++) {
        Object obj = new Object();
    }

위 코드를 보면 반복문을 돌며 100000개의 새로운 인스턴스를 만드는데, for문이 한번 끝날때 obj 변수도 같이 사라지기 때문에 해당 Object 인스턴스에 접근할 수가 없다. 이렇게 많이 만들어졌지만 쓰이지 않는 객체들이 메모리 공간을 차지하고 있다면 메모리의 낭비가 발생할 수밖에 없다. 이렇게 더이상 사용하지 않는 인스턴스의 메모리 공간을 회수해주는 것이 GC이다.

GC는 알아서 메모리 공간을 관리해주기에 편리하지만 단점도 존재한다.
1. 프로그래머는 GC가 언제 작동하는지 알 수 없다.
2. GC가 실행될 때, GC 관련 스레드를 제외한 다른 스레드들이 모두 멈추기 때문에 성능상의 문제가 생길 수 있다. 이를 Stop-the-World라고 한다.

Full GC가 무엇인지는 아래에서 더 설명해보고자 한다.

Stop The World(STW)
GC가 작동하는 동안 GC 관련 스레드를 제외한 모든 스레드가 실행을 멈추는 현상이다. 이 시간이 길어지면 성능에 문제가 생길 수 있으므로 이 시간을 최소화 시키는 최적화가 필요하다.

STW라는 단점 때문에 GC가 너무 자주 실행되거나 오랫동안 실행되면 성능 문제가 생길 수 있다. 따라서 GC 실행을 최적화하는 작업이 필요한데, 이를 GC 튜닝이라고 한다.

GC의 대상

GC는 메모리 중 힙(Heap) 메모리에 저장된 객체만을 대상으로 한다. 그럼 힙 영역에 저장된 객체 중 어떤 것을 지우는 것일까?

GC는 힙 영역 객체들 중 지울 것을 판단하기 위해 Reachability(도달성)이라는 개념을 사용한다.

자바는 참조 자료형 변수와 인스턴스를 저장할 때, 그림고 같이 인스턴스는 heap 영역에, 그 인스턴스를 참조하는 참조 변수는 stack 영역(메소드 내의 변수는 method area)에 저장한다. 이런 상황에서 stack 영역에 있는 참조 변수가 메모리에서 제거 되어서 그 인스턴스에 더이상 접근 불가능한 경우가 생기는데 이것을 Unreachable(도달 불가능)이라고 한다.

정리하자면

  • Reachable: 스택 영역에 참조 변수가 남아있음. 인스턴스가 참조 되고 있음.
  • Unreachable: 스택 영역에 참조 변수가 없음. 인스턴스가 참조되고 있지 않음.

GC는 Unreachable한 객체들을 메모리에서 제거하는 역할을 한다.

GC의 청소 방식 - Mark & Sweep

GC는 기본적으로 'Mark and Sweep' 방식으로 동작한다.
GC의 청소 대상이 될 객체들을 식별(Mark)하고, 청소(Sweep)하고 메모리의 빈공간을 채우는 과정(Compaction)으로 이뤄져있다.

  1. Mark 과정: Root Space로부터 그래프 순회를 통해 연결된 객체들을 찾아내어 각각 어떤 객체를 참조하고 있는지 찾아서 마킹한다.
  2. Sweep 과정: Unreachable 객체들을 메모리에서 제거한다.
  3. Compaction 과정: Sweep 후에 파편화된 메모리 영역을 Heap의 시작 주소로 모아서 빈공간을 메운다. (GC의 종류에 따라 이 과정이 없을 수도 있다.)

GC의 Root Space
Heap 메모리 영역을 참조하는 Method Area, Stack, Native Method Stack 등이 있다.

GC의 동작 과정

JVM 메모리의 힙 영역

GC가 어떻게 동작하는 것인지 알기 위해서는 JVM의 힙 메모리가 어떤 구조로 이뤄져 있는지를 알아야한다.

Heap 영역은 'Weak Generational Hypothesis'라는 가정을 전제로 설계가 되었다.

Weak Generational Hypothesis
1. 대부분의 객체는 금방 Unreachable 상태가 된다.
2. 오래된 객체에서 새로운 객체로의 참조는 아주 드물게 발생한다.

위 가정을 요약하자면, 대부분의 객체는 일회성으로 사용이 끝난다는 것이다. 즉 오랫동안 남아있는 객체는 드물것이라는 말이된다. 이러한 특성에 따라, Heap 영역은 생긴지 얼마 안된 객체가 저장되는 Young Generation, 오래된 객체가 저장되어 있는 Old Generation으로 분리하여 설계되었다.

Young Generation

  • 새롭게 생성된 객체가 할당된다.
  • 대부분의 객체가 금방 unreachable 상태가 되기 때문에 많은 객체가 Young 영역에 생성되었다가 사라진다.
  • Young 영역에 대한 GC를 Minor GC라고 부른다.

Old Generation

  • Young 영역에서 Reachable 상태를 유지하여 살아남은 객체가 복사되는 영역이다.
  • Young 영역보다 크게 할당되며, 공간이 큰 만큼 GC는 더 적은 빈도로 발생한다.
  • Old 영역에 대한 GC를 Full GC, Major GC라고 부른다.

Young 영역보다 Old 영역이 더 크게 할당된 이유는 Young 영역의 수명이 짧은 객체들은 그렇게 큰 공간을 차지하지 않으며, 크기가 큰 객체들은 Young 영역이 아닌 바로 Old 영역으로 할당되기 때문이다.

참고: Metaspace 영역
Java 7까지는 힙 영역에 'Permanant'라는 영역이 존재했다. Permanant란 영구적인 세대라는 의미로 생성된 객체들의 정보의 주소값이 저장되는 영역이다. Permanant는 Java 7까지 힙 영역에 존재하다가 Java 8부터는 'Metaspace'라는 이름으로 Native method stack에 편입되었다.

Young Generation 영역은 다시 세 부분으로 나눠진다.

Eden 영역

  • new를 통해서 새로 생성된 객체가 위치한다.
  • GC가 실행된 후 살아남은 객체들은 Survival 영역으로 이동한다.

Survival 영역

  • GC가 최소 1번 수행되는 동안 살아남은 객체들이 위치한다.
  • Survival 0, Survival 1로 나눠져있다.
  • s0, s1 중 적어도 한 공간은 비어져 있어야하는 규칙이 있다.

Minor GC의 동작

객체는 처음 생성될 때 Young Generation에서 생성된다.
Young Generation에서 동작하는 GC를 Minor GC라고한다.
Young Generation이 Old Generation보다 메모리의 크기가 작기 때문에, Minor GC의 수행시간이 Major GC의 수행시간보다 더 짧다.

Minor GC의 동작은 아래와 같다.

  1. 객체들은 처음 생성될 때 Eden 영역에 할당된다.

  2. 객체가 계속 생성되어서 Eden 영역이 꽉 차게 되면 Minor GC가 발생한다.

  3. Mark 과정을 통해 Reachable 객체들을 탐색한다.

  4. Reachable 객체들은 s0으로 이동한다. (s0, s1 중 하나는 비어있어야 하므로, s0이 아닌 s1으로 이동할 수도 있다.)

  5. Eden 영역에 남아있는 객체들을 제거한다(Sweep)

  6. 살아남은 객체들의 age값이 1씩 증가한다.

age란?
Survival 영역에서, 객체가 살아남은 횟수를 의미하며 Object header에 기록된다. age 값이 임계값에 다다르면 Old 영역으로 이동(Promotion)된다. 참고로 HotSpot JVM의 경우, Object Header에 age를 기록하는 부분이 6 bit로 돼있기 때문에 임계값이 31이다.

  1. 다시 Eden 영역이 꽉차게 되면 Minor GC가 발생한다. (Eden과 s0를 탐색하며 Mark)

  2. 마킹된 객체는 s1으로 이동하며, unreachable 객체들은 제거된다.

  3. 살아남아서 s1으로 이동한 객체들은 age가 1씩 증가한다.

Major GC의 동작

Old Generation에서 발생하는 GC를 Major GC(Full GC)라고 한다. Old Generationd은 오래 살아남은 객체들이 위치하는 곳으로, Young 영역에서 age가 임계값에 도달해 Promotion 된 객체들이 존재한다.

Major GC는 객체들이 계속 Promotion 되어서 Old 영역의 메모리가 부족해지면 발생한다.

Major GC의 과정은 아래와 같다.

  1. Survival 영역에 있는 객체들 중 age가 임계값에 도달한 객체들이 발생한다. (이 그림에서는 임계값=8)

  2. 이 객체들은 Old Generation으로 이동한다. (Promotion)

  3. Old Generation의 메모리가 부족해지면 Major GC(Full GC)가 발생한다. Major GC는 Old Generation 전체를 검사하여 Unreachable Object를 제거한다.

Old 영역은 Young 영역에 비해 메모리의 크기가 크기 때문에 Major GC의 실행시간이 길다.
Minor, Major GC 모두 STW(Stop-the-World)가 발생하지만, Minor GC는 수행 시간이 보통 0.5~1초 정도이기 때문에 크게 문제되지 않는다.
하지만 Major GC는 Minor GC의 수행시간의 10배 이상의 시간을 소요하기 때문에, cpu에 많은 부하를 주고 긴 STW 시간 동안 애플리케이션이 버벅거리는 문제가 생긴다.

따라서 개발자들은 GC의 수행 횟수와 수행 시간을 줄이기 위해 여러 종류의 GC를 개발해왔다.


GC 알고리즘의 종류

GC가 자동으로 메모리를 관리해주는 것은 큰 장점이지만, STW 현상으로 애플리케이션의 성능에 문제가 될 수 있다는 단점이 있다. 이를 보완하고 효율적으로 GC를 사용하기 위해 개발자들은 많은 GC 알고리즘을 개발하였다. 아래에서 소개하는 여러 종류의 GC들은 Java에서 설절을 통해 적용이 가능하며, 상황에 따라 가장 효율적인 GC를 선택하여 사용할 수 있다.

Serial GC

  • 서버의 CPU 코어가 1개 일때 사용하기 위해 개발된 가장 간단한 GC
  • GC를 처리하는 스레드가 1개이기 때문에 STW 시간이 가장 길다.
  • Minor GC에서는 [Mark & Sweep]을, Major GC에서는 [Mark & Sweep & Compact]으로 동작한다.
  • 실무에서 사용하는 경우는 거의 없다. (cpu 코어가 1개인 경우에만 사용)

자바 프로그램을 실행할 때 '-XX:+UseSerialGC' 옵션으로 serial GC를 설정할 수 있다.

java -XX:+UseSerialGC -jar Application.java

Parallel GC

  • Java 8의 기본 GC이다.
  • Serial GC와 기본적인 알고리즘은 동일하지만 Minor GC는 멀티 스레드, Major GC는 싱글 스레드로 실행한다.
  • Serial GC에 비해 STW 시간이 감소한다.

[-XX:+UseParallelGC] 옵션을 통해 적용 가능하다. parallel GC에서 GC 스레드는 기본적으로 cpu 개수만큼 할당된다. 설정을 통해 그 값을 바꿀 수 있다.

Parallel Old GC(Parallel Compacting Collector)

  • parallel GC를 개선한 버전이다.
  • Young 영역 뿐만 아니라 Old 영역에서도 멀티스레드로 동작한다.
  • Minor, Major GC 모두 새로운 방식인 'Mark-Summary-Compact' 방식으로 동작한다.

참고: Mark-Summary-Compact
Mark 단계: Old 영역을 region 별로 나눈다. region별로 참조되는 객체들을 mark한다. 이때 여러 스레드가 각각의 region을 병렬적으로 검사한다.
Summary 단계: Mark 단계에서 알아본 region 별 정보로 살아남은 객체들의 밀도가 높은 부분이 어디까지인지 dense prefix를 정한다. 오랜 기간 참조된 객체는 앞으로 사용할 확률이 높다는 가정 하에 dense prefix를 기준으로 Compact 영역을 줄인다.
Compact 단계: compact 영역을 destination과 source로 나누어, 살아남은 객체는 destination으로 이동하고 살아남지 않은 객체는 제거한다.

[-XX:+UseParallelOldGC] 옵션으로 Parallel Old GC를 적용 가능하다.

CMS GC(Concurrent Mark & Sweep)

  • GC 스레드와 어플리케이션 스레드를 동시에 실행해서 STW 시간을 최대한 줄이기 위해 고안된 GC이다.
  • GC 대상을 파악하는 과정이 복잡한 여러 단계로 수행되기 때문에 다른 GC에 비해 cpu 사용량이 높다.
  • 메모리 파편화 문제가 생긴다.
  • Java 9에서 deprecated 되었고, Java 14에서 사용이 중지되었다.

G1 GC

  • CMS를 대체하기 위해 jdk7에서 처음 릴리즈된 GC이다.
  • Java 9+ 버전의 디폴트 GC이다.
  • 힙 메모리가 4GB이상, STW 시간이 0.5초 정도 필요한 상황에서 사용이 권장된다. (힙 메모리가 너무 작은 경우 권장하지 않음)
  • 기존 GC 알고리즘들은 물리적으로 Old, Young 공간을 나누어 놓았었지만, G1 GC는 region이라고 하는 다른 개념을 적용
  • 전체 힙 영역을 체스판처럼 여러 Region으로 나눈 후, 상황에서 따라 Eden, Survivor, Old 영역 역할을 동적으로 부여하는 것이다.
  • Garbage로 가득찬 영역을 빠르게 회수하여 빈 공간을 확보하므로 GC 발생 빈도를 줄일 수 있다.

G1 GC가 효율적인 이유는 G1 GC는 이전 GC들처럼 일일히 메모리를 탐색해서 객체들을 제거하지 않는다. 대신 메모리가 많이 차있는 리젼을 인식해서 그 리젼에 우선적으로 GC를 실행한다. 즉, 힙 전체에 GC가 발생하는 것이 아닌, 영역(region, 리젼)별로 GC가 발생한다.

G1 GC는 [-XX:+UseG1GC] 옵션으로 적용 가능하다.

Shenandoah GC

  • 레드 햇이 개발한 java 12에 릴리즈된 GC이다.
  • 기존 CMS가 가진 단편화와 G1의 pause 이슈를 해결하였다.
  • Heap 사이즈에 영향을 받지 않고 일정한 pause 시간이 소요된다.

[-XX:+UseShenandoahGC] 옵션으로 적용가능하다.

ZGC (Z Garbage Collector)

  • Java 15에 릴리즈된 GC이다.
  • 대량의 메모리(8MB ~ 16TB)를 low-latency로 잘 처리하기 위해 디자인 된 GC이다.
  • G1의 Region 처럼 ZGC는 ZPage라는 영역을 사용하며, G1의 Region은 크기가 고정적이지만 ZPage는 2mb의 지수 승으로 동적으로 운영된다 (큰 객체가 들어오면 2^ 로 영역을 구성해서 처리한다.)
  • 힙의 크기가 커지더라도 STW 시간이 10ms를 넘기지 않는다는 장점이 있다.

[-XX:+UnlockExperimentalVMOptions -XX:+UseZGC] 옵션으로 적용가능하다.


Reference

https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EA%B0%80%EB%B9%84%EC%A7%80-%EC%BB%AC%EB%A0%89%EC%85%98GC-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%F0%9F%92%AF-%EC%B4%9D%EC%A0%95%EB%A6%AC

https://imasoftwareengineer.tistory.com/103

https://velog.io/@devnoong/JAVA-Stack-%EA%B3%BC-Heap%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C

https://jaemunbro.medium.com/java-metaspace%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90-ac363816d35e

https://velog.io/@guswlsapdlf/Java-GC-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98

https://blog.leaphop.co.kr/blogs/42

profile
블로그 이전하려고 합니다! 👉 https://onfonf.tistory.com 🍀

0개의 댓글