[Java]가비지 컬렉터(Garbage Collector)란?

Arthur·2023년 5월 7일
1
post-thumbnail

가비지 컬렉터란?


사용하지 않는 객체의 메모리를 GC(Garbage Collector)가 주기적으로 검사해서 청소해준다.

왜 사용하지 않는 객체의 메모리를 청소할까?

C와 C++ 같은 Unmanaged language는 free()와 같은 함수를 사용해서 메모리를 직접 메모리를 해제해야 한다.

이런 번거로운 일을 GC가 대신 해주고 있는 것이다.

사용하지 않는 객체의 메모리 점유는 결국 메모리 누수로 이어지게 된다.

메모리는 한정된 자원이기 때문에 사용하지 않거나 필요가 없는 부분은 해제를 해주는 것이 맞다.


가비지 컬렉터와 가비지 컬렉션의 차이는?


  • 가비지 컬렉터 : 메모리 관리를 담당하는 시스템 또는 프로그램의 구성 요소이며, 메모리에서 더 이상 사용되지 않는 객체를 찾아 제거하여 메모리를 회수하는 역할을 수행한다.
  • 가비지 컬렉션 : 메모리 관리 기술 중 하나로, 가비지 컬렉터에 의해 수행되는 프로세스를 의미.

가비지 컬렉션은 프로세스 자체를 얘기하고 컬렉터는 실제 역할을 수행하는 주체를 얘기한다.


JVM Heap 메모리 영역은 어떻게 생겼을까?


JVM의 한 종류인 Hotspot JVM의 Heap 영역은 아래와 같이 생겼다고 한다.

가장 크게 비교하면 Young, Old, Permanent Generation으로 나뉘어져 있다.

각각 무엇인지 알아보자.

Young Generation(Young 영역)이란?

  • 새롭게 생성된 객체가 할당(Allocation)되는 영역
  • 대부분 객체가 금방 Unreachable 상태가 되기 때문에, 많은 객체가 Young 영역에 생성되었다가 사라진다.
  • Young 영역에 대한 가비지 컬렉션(Garbage Collection)을 Minor GC라고 부른다.

Old Generation(Old 영역)이란?

  • Young 영역에서 Reachable 상태를 유지하여 살아남은 객체가 복사되는 영역
  • Young 영역보다 크게 할당되며, 영역의 크기가 큰 만큼 가비지는 적게 발생한다.
  • Old 영역에 대한 가비지 컬렉션을 Major GC 또는 Full GC라고 부른다.

Permanent Generation(Perm 영역)이란?

  • JVM에서 클래스 메타데이터(클래스와 메소드, 필드 등의 정보)를 저장하는 곳
  • 클래스 로더는 클래스 파일을 읽어들여서 이 영역에 클래스 메타데이터를 저장한다.
  • 이 정보들은 JVM 실행 도중에 변경되지 않으며, JVM 종료 시까지 유지된다.

Perm 영역이 저장하는 정보들

  1. Class 의 Meta정보

  2. Method의 Meta 정보

  3. Static Object

  4. Class와 관련된 배열 객체 Meta 정보

  5. JVM 내부적인 객체들과 최적화컴파일러(JIT)의 최적화 정보

Young과 Old는 객체의 생명주기와 연결 시켜서 이해하면 도움이 될 것 같다.

Perm 영역은 Hotspot JVM Heap 영역의 사진에 담겨 있어서 적었지만,

자바 8 버전 이후에는 metaspace 영역으로 대체 되었다고 한다.

대체된 이유를 알아보자.


Perm 영역은 왜 사라졌을까?


위 JMV Heap 영역의 사진을 보면 Perm 영역이 없다.

대부분 JVM Heap 영역을 검색하면 위와 같은 사진 아니면 Perm 영역이 포함된 사진이 있을 것이다.

위에도 작성 했듯이 자바 8 버전 이후에는 metaspace 영역으로 대체 되었기 때문이다.

Metasapace 영역이란?

  • Perm 영역에서 저장하던 Class의 Meta 정보들이 이 영역에 저장된다.
  • Native Memory 영역에 위치하며, JVM이 아닌 OS 레벨에서 관리된다.
  • 클래스 메타데이터와 리플렉션을 사용하는 애플리케이션에서 사용하는 일부 메모리를 저장한다.

대체된 이유는?

  • Perm 영역의 메모리 누수, OutOfMemoryError 등과 같은 문제
    • 클래스 로딩 및 언로딩 과정에서 메모리 할당 및 해제의 빈번한 발생으로 인해 이러한 문제가 더 심각해졌다.
  • 클래스 메타데이터를 Native Memory에 저장하면서,
    JVM에서의 OutOfMemoryError 문제가 해결되었다.

Reachable과 Unreachable이란?


자바 GC는 객체가 가비지인지 판별하기 위해서 Reachability라는 개념을 사용한다.

어떤 객체에 유효한 참조가 있으면 ‘Reachable’로, 없으면 ‘unreachable’로 구별한다.

  • Unreachable 객체를 가비지로 간주해서 GC를 수행한다.

두 가지의 구분에 관련해서 Naver D2에 상당히 정리가 잘 되어있다.

아래의 그림과 설명을 통해 이해가 쉽게 되었다.

위 사진은 오라클 HotSpot VM 기준에 런타임 데이터 영역이다.

(스레드가 차지하는 영역, 객체를 생성 및 보관하는 큰 힙, 클래스 정보가 차지하는 메서드 영역)

Object는 객체이고, 화살표는 참조를 나타낸다.

여기서 보면 알 수 있듯이 객체들의 참조는 사슬처럼 이루어져 있다.

이런 상황에서 유효한 참조 여부를 파악하려면 항상 최초의 참조가 있어야 하는데,

이를 객체 참조의 root set이라고 한다.

힙에 있는 객체들에 대한 참조는 다음 4가지 종류 중 하나이다.

  • 힙 내의 다른 객체에 의한 참조
  • Java 스택, 즉 Java 메서드 실행 시에 사용하는 지역 변수와 파라미터들에 의한 참조
  • 네이티브 스택, 즉 JNI(Java Native Interface)에 의해 생성된 객체에 대한 참조
  • 메서드 영역의 정적 변수에 의한 참조

이들 중 ‘힙 내의 다른 객체에 의한 참조’를 제외한 3 가지는 root set으로 reachability를 판가름하는 기준이 된다.

  • root set으로부터 시작한 참조 사슬에 속한 객체들은 Reachable 객체이다.
  • 참조 사슬과 무관한 객체들이 Unreachable 객체로 GC 대상이다.

위 사진을 보고 트리 자료구조가 떠올랐다.

순환참조와 Unreachable 객체의 연관성이 있지 않을까?

Object a = new Object();
Object b = new Object();
a.setReference(b);
b.setReference(a);

순환 참조의 예시로 위와 같이 코드를 작성했다.

순환 참조란 두 개 이상의 객체가 서로를 참조하는 상황을 말한다.

위와 같은 상황의 문제점은 순환 참조된 객체들은 root set이 불분명해지고,
메모리 누수를 야기하는 원인이 된다.

스프링에서의 두 객체의 서로 순환 참조 문제와 비슷한 개념 같다.

GC의 root set에 이들 객체가 계속 유지되어 메모리 누수(Memory Leak)를 야기한다.

GC가 가비지를 판별하는 기준을 알게 되었다.

이제는 가비지 컬렉션 과정에 대해서 알아보자.


Stop the world란?


GC를 실행하기 위해 JVM 애플리케이션 실행을 멈추는 것을 말한다.

  • Stop the world가 발생하면 GC를 실행하는 스레드를 제외한 나머지 스레드는 모두 작업을 멈춘다.
  • GC 작업을 완료한 이후에야 중단했던 작업을 다시 시작한다.
  • GC 튜닝은 이런 stop the world 시간을 줄이는 것이다.

GC가 가비지의 메모리를 해제하는 행동의 코스트가 생각보다 높다는 것을 알게되었다.

그러면 stop the world는 왜 발생하는 걸까?

stop the world가 발생하는 이유

GC가 실행되는 동안에는 모든 객체의 참조 관계를 추적하고,
유효한 객체들과 그렇지 않은 객체들을 식별하여 메모리를 회수해야 하기 때문이다.

객체의 참조 관계는 실행 중에도 추적할 수 있지만, 이 작업이 매우 느리기 떄문에

stop the world를 통해 일시적으로 애플리케이션을 멈추고 추적 작업을 수행한다.


가비지 컬렉션 과정


설명 보다는 사진이 더 기억에 잘 남아서 다시 사진을 찾아봤다.

실행 범위와 영향 범위를 보게 되면 가장 눈에 띄는 것은 Minor GC와 Major GC이다.

위 2개의 개념을 이해하면 왜 이렇게 나눠 놨는지 더 이해하기 쉬울 것 같다.

  • Minor GC : Young 영역에서 수행되는 GC
  • Major GC : Old 영역에서 수행되는 GC

그렇다면 Minor GC와 Major GC의 동작 과정으로 나눠서 봐보자.

Minor GC

객체가 새롭게 생성되면 Young 영역 중에서도 Eden 영역에 할당(Allocation)이 된다.

그리고 Eden 영역이 꽉 차면 Minor GC가 발생하게 된다.

  • 사용되지 않는 메모리는 해제되고 Eden 영역에 존재하는 객체는 (사용중인) Survivor 영역으로 옮겨지게 된다.
  • Survivor 영역은 총 2개이지만 반드시 1 개의 영역에만 데이터가 존재해야 한다.

Young 영역의 동작 순서

  1. 새로 생성된 객체가 Eden 영역에 할당된다.
  2. 객체가 계속 생성되어 Eden 영역이 꽉차게 되고 Minor GC가 실행된다.
    1. Eden 영역에서 사용되지 않는 객체의 메모리가 해제된다.
    2. Eden 영역에서 살아남은 객체는 1개의 Survivor 영역으로 이동된다.
  3. 1~2번의 과정이 반복되다가 Survivor 영역이 가득 차게 되면 Survivor 영역의 살아남은 객체를 다른 Survivor 영역으로 이동시킨다.(1개의 Survivor 영역은 반드시 빈 상태가 된다.)
  4. 이러한 과정을 반복하여 계속해서 살아남은 객체는 Old 영역으로 이동(Promotion)된다.

여기서 객체의 생존 횟수를 카운트하기 위해 Minor GC에서 객체가 살아남은 횟수를 의미하는 Age를

Object Header에 기록한다.

Major GC

Young 영역에서 오래 살아남은 객체는 Old 영역으로 Promotion되는 것을 확인했다.

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

위 글은 Generational GC의 과정을 나타내고 있다.

Old 영역에 데이터가 가득 차면 사용하는 GC 방식이 있다고 한다.

Old 영역에 대한 GC

  • Serial GC
  • Parallel GC
  • Parallel Old GC(Parallel Compacting GC)
  • Concurrent Mark & Sweep GC(이하 CMS)
  • G1(Garbage First) GC

위의 알고라즘에 대한 자세한 내용은 아래 3 개의 링크를 참고해서 공부해볼 수 있다.

  • [Java] 다양한 종류의 Garbage Collection(가비지 컬렉션) 알고리즘 (2/2) ⇒ 링크
  • Naver D2 - Java Garbage Collection ⇒ 링크
  • [Java] Java의 Garbage Collection - Generational GC, G1 GC ⇒ 링크

여기서 우리는 Mark and Sweep과 mark-sweep-compact 알고리즘을 볼 것이다.

  • Mark and Sweep 알고리즘은 Young 영역에서 사용된다.
  • mark-sweep-compact 알고리즘은 Old 영역에서 일반적으로 사용된다.

위와 같이 큰 틀로 구분이 되기 때문에 두 개를 알아보기로 했다.

우선 마크 앤 스윕부터 알아보도록 하자.


마크 앤 스윕(Mark and Sweep)


Mark라는 단어의 뜻은 표시하다 라는 뜻이 있다.

Sweep은 쓸어내리다, 소멸의 뜻이 있다고 한다.

GC Root는 실행중인 스레드, 정적 변수, 로컬 변수, JNI 레퍼런스와 같은 것들이 될 수 있다.

위에서 나왔던 root set과 동일한 용어이다.

GC는 객체에 Mark를 하고 Mark가 되지 않은 객체의 메모리를 해제하게 된다.

결국에 Mark가 안된 객체는 Unreachable 객체이다.

이미 Reachability에서 다뤘던 내용이기 때문에 쉽게 이해할 수 있었다.

더 자세한 내용이 궁금하면 아래의 블로그를 참고해보자

  • 가비지 컬렉터(Garbage Collector)와 Mark & Sweep ⇒ 링크

마크-스윕-컴팩트(mark-sweep-compact)


Old 영역에는 대부분 큰 객체들이 저장되는데 Mark and Sweep 알고리즘을 사용하면

큰 객체들을 처리할 때 많은 시간과 메모리가 소비된다고 한다.

대신 Old 영역에서는 mark-sweep-compact 알고리즘이 사용된다.

  • 객체를 메모리에서 삭제하는 대신 객체들을 메모리의 한쪽으로 몰아서 메모리 공간을 최적하하는 “compact” 작업을 수행한다.
  • 이를 통해 메모리 단편화를 최소화하고 성능을 향상 시킬 수 있다.

사진으로 보니까 훨씬 이해가 편하고 마음이 편안해진다.

예전에 하드디스크 성능을 위해 디스크 조각 모음을 했던 것과 비슷한 것 같다.


다이어그램으로 정리해보기

알아본 내용들을 다이어그램으로 표시해 봤다.
각 공부한 키워드들의 연관성을 좀 더 가시적으로 보기 위해 정리해봤다.
내용을 전부 머릿속에 기억 하는 것은 한계가 있을 수 있으니,
키워드들만 봐도 서로의 연관성과 개념이 떠오르도록 나의 방식대로 정리해봤다.


🔗참고자료


  • [Java] Garbage Collection(가비지 컬렉션)의 개념 및 동작 원리 (1/2) ⇒ 링크
  • [Java] 다양한 종류의 Garbage Collection(가비지 컬렉션) 알고리즘 (2/2) ⇒ 링크
  • JAVA 8에서 perm 영역이 사라지고 metaspace 영역으로 대체된 이유? ⇒ 링크
  • Naver D2 - Java Reference와 GC ⇒ 링크
  • Naver D2 - Java Garbage Collection ⇒ 링크
  • [JVM] Garbage Collection Algorithms ⇒ 링크
  • 가비지 컬렉터(Garbage Collector)와 Mark & Sweep ⇒ 링크
  • chatGPT
profile
기술에 대한 고민과 배운 것을 회고하는 게임 서버 개발자의 블로그입니다.

0개의 댓글