[Java] JVM Garbage Collection

박성우·2023년 7월 19일
0

Java

목록 보기
5/6

Java에서 객체를 생성하면 JVM 내의 메모리 영역 중 Heap 영역이라는 곳에 메모리를 동적으로 할당해준다.

동적으로 할당한다는 것은 프로그램이 실행 중에 필요한 경우마다 생성된 객체를 위한 메모리를 마련해준다는 뜻이고, 이 메모리를 마련해주는 영역이 Heap 영역이다.

문제는 메모리는 한정적이기 때문에, 메모리를 계속 할당만 하게되면 언젠가는 사용 중인 메모리와 필요없는 메모리 구분 없이 가득 차게 될 것이다.

그래서 할당한 메모리를 해제해주는 과정이 필요한데, C언어와 같은 경우에는 개발자가 할당한 메모리를 직접 해제해줘야 한다.

이렇게 개발자가 수동으로 메모리를 관리하는 것은 메모리 누수가 발생할 확률이 높고, 다양한 휴먼 에러가 발생할 가능성이 크다.

그래서 Java에서는 Garbage Collection라고 하는 동적 할당된 메모리를 관리해주는 일종의 자동화 시스템을 채택한 것이다.

Garbage Collection

JVM(Java Virtual Machine)의 Heap 영역에서 사용하지 않는 객체를 주기적으로 제거하는 프로세스

우선 사용하지 않는 객체를 판단하려면 기준이 필요하다.

Garbage Collection는 그 기준을 Reachability로 판단을 하는데,

쉽게 말해 어딘가에서 참조하고 있어서 닿을 수 있는(Reachable) 객체와 어디에서도 참조하지 않아서 닿을 수 없는(Unreachable)로 구분하는 것이고 Unreachable 객체들이 제거 대상이 된다.

여기서 참조가 처음으로 일어나는 객체 및 데이터를 Root라고 칭하고 Root가 될 수 있는 경우는 세 가지가 존재한다.

  1. Stack 영역의 데이터
  2. Method 영역의 데이터
  3. JNI(Java Native Interface)에 의해 생성된 객체들 (Native Method Stack)

❓그래서 제거하고자 하는 객체의 기준은 알았는데, 객체들이 서로가 서로를 참조하고 있는 마당에 어떻게 Unreachable 객체들을 찾아서 제거할 수 있는 것일까

Garbage Collection는 이를 위한 방법론으로 Mark and Sweep이라는 알고리즘을 내부적으로 채택하고 있다.

말그대로 Reachable한 객체들을 지정(Mark)하고 다 찾았으면 그 외의 Unreachable 객체들은 전부 쓸어(Sweep)버리겠다는 것이다.

또한 Garbage Collector에 따라서 Compact 라는 과정이 추가로 수행되는 경우도 있는데, Sweep을 한 이후에 분산된 객체들을 Heap의 시작 주소로 모아서 메모리 단편화(Fragmentation)를 막아주는 작업이다.

이렇게 Mark and Sweep 알고리즘이 적용되서 객체들을 관리하는 Heap 영역의 구조는 다음과 같다.

크게 두 영역으로 나뉘는데,

Young Generation

  • 새롭게 생성된 객체들이 할당되는 영역
  • Minor GC가 실행되는 영역

Old Generation

  • Young Generation에서 제거되지 않은 객체들이 존재하는 영역
  • Major GC가 실행되는 영역

이렇게 나뉘어진 이유에는 다음과 같은 전제가 있다.
1. 대부분의 객체는 금방 접근 불가능(Unreachable)한 객체가 된다.
-> 금방 Garbage가 된다.
2. 오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다.

따라서, 모든 Heap 영역에 대한 Garbage Collection를 진행하기보다, Minor와 Major로 나눠서 효율적으로 관리한다.

대부분의 객체가 제거되는 Young Generation 영역은 크기가 작아서 Minor GC가 짧은 주기와 실행 시간으로 일어나고, 크기가 큰 Old Generation 영역은 10배 이상의 실행 시간을 사용하며 비교적 주기가 길다.

그렇다면 Young Generation 영역의 젊은 객체, Old Generation 영역의 오래된 객체를 나누는 기준은 무엇일까

객체에 age 값을 부여하고 이 age 수치를 기준으로 객체를 나눈다.

이 수치가 언제 증가하는지 알기 위해서는 Young Generation의 구조를 먼저 알아야한다.

Young Generation 영역은 Eden, Survivor 0, Survivor 1 영역으로 나뉜다.

  1. 처음 생성된 객체는 Eden 영역에 위치
  2. 객체가 계속 생성되어 Eden 영역이 꽉차면 Minor GC 실행
  3. Mark 동작을 통해 Reachable 객체를 탐색
  4. Eden 영역에서 살아남은 객체는 Survivor 0 영역으로 이동
  5. Sweep 동작을 통해 Eden 영역에서 Unreachable 객체의 메모리를 해제
  6. Survivor 0으로 이동한 살아남은 모든 객체들은 age값이 1씩 증가
  7. 다시 Eden 영역이 가득참

여기까지의 과정이 끝나면 위 그림과 같은 상황이 되는 것이고,

이후로는 Minor GC가 일어날 때마다 Mark된 객체들이 Survivor 0과 Survivor 1을 오고가며 age 값이 계속 증가하고 그 외의 Unreachable 객체들은 삭제(Sweep)된다.

이 과정이 반복되다가 age 임계값을 돌파한 객체가 생기면, 해당 객체는 Old Generation 영역으로 옮겨지게(Promotion) 되는 것이다.

마지막으로, Old Generation이 가득차면 Major GC (Mark and Sweep)가 일어난다.


❗Garbage Collector을 사용함으로써 메모리 누수를 막고 편리하고 효율적으로 메모리를 관리할 수 있지만 장점만 있는 것은 아니다.

어떤 객체를 삭제할 지 검사하고 삭제하는 과정 모두 CPU 리소스를 사용하는 것이며, 데이터가 많을 수록 비용은 증가한다.

Stop the World

Garbage Collector를 실행하기 위해 JVM이 프로그램 실행을 멈추는 것

또한, GC는 프로그램이 실행되는 도중에 실행되는 것이 아닌, GC를 실행하는 쓰레드 외의 모든 쓰레드의 작업을 중단(Stop the World)하기 때문에 실시간성을 중요하게 여기는 프로그램에는 적합하지 않을 수 있고, 자주 실행될 수록 프로그램 성능 하락에 큰 영향을 미친다.

이러한 성능 문제를 위해 Garbage Collector에 대한 지속적인 최적화가 이뤄지게 되었고, 이에 따라 다양한 GC가 생기며 발전되어왔다.

  1. Serial GC
  • 싱글 쓰레드로 동작 (Stop the World 시간이 가장 긺)
  • Minor GC : Mark-Sweep을 사용하고, Major GC : Mark-Sweep-Compact를 사용
  1. Parallel GC
  • Java 8의 디폴트 GC
  • Minor GC는 멀티 쓰레드, Major GC는 싱글 쓰레드로 동작
  1. Parallel Old GC
  • Minor, Major GC 전부 멀티 쓰레드로 동작
  • Mark-Summary-Compact 알고리즘 사용
    (Sweep : 싱글 쓰레드 Major GC, Summary : 멀티 쓰레드 Major GC)
  1. CMS GC (Concurrent Mark Sweep)
  • Initial Mark (Root에서 참조하는 객체만 Mark) - Concurrent Mark (이전 단계 객체들이 참조하는 객체 Mark) - Remark (이전 단계 객체를 다시 Mark, 추가되거나 참조가 끊긴 객체 확정) - Concurrent Sweep (최종적으로 Sweep) 과정을 거침
  1. G1 GC (Garbage First) - 현재 가장 일반적인 GC
  • Java 9+의 디폴트 GC이며 CMS GC를 개선

  • Heap 영역을 물리적으로 고정된 Young / Old 영역이 아닌 일정한 크기의 Region으로 분할하고 동적으로 역할을 부여

  • 이전 GC들과 같이 일일히 Heap 영역을 전부 탐색하는 것이 아닌, Region 별로 GC가 실행되고 메모리가 많이 차있는 영역을 인식하여 우선적으로 GC를 실행시킨다.

  • Compact 진행

Humonogous: Region 크기의 50%를 초과하는 큰 객체를 저장하기 위한 공간
Available/Unused: 아직 사용되지 않은 Region

현재 사용중인 OpenJDK 17 버전에서 명령어를 통해 채택하고있는 GC를 확인해보았다.

플래그에 의해 G1 GC가 사용되고 있는 것을 볼 수 있다.

  1. Shenandoah GC
  • CMS의 Fragmentation(단편화), G1 GC의 stop-the-world 의 이슈 개선
  • Heap 사이즈에 영향을 받지 않고 일정한 Pause 시간이 소요

Shenandoah GC는 기존 GC들과 같이 Generation 영역을 나누지 않아서 기존에 Young Generation에서 Old Generation으로 복사될 때 생기는 stop-the-world를 없애고, G1의 Compact 단계의 Pause 또한 Concurrent Compact로 해결하였다.

  1. ZGC (Z Garbage Collector)
  • 대량의 메모리를 짧은 지연 시간과 함께 처리하기 위한 GC
  • G1의 Region과 같이 ZPage를 통해 Heap 영역을 파티션하지만, Region은 크기가 고정인데 비해 Zpage는 동적으로 운영
  • 힙 크기가 증가하더도 stop-the-world의 시간이 절대 10ms를 넘지 않음

Reference

profile
Backend Developer

1개의 댓글

comment-user-thumbnail
2023년 7월 19일

이런 정보를 찾고 있었습니다, 감사합니다.

답글 달기