Having

[JAVA] JVM의 GC 동작 원리 본문

JAVA

[JAVA] JVM의 GC 동작 원리

GHM 2022. 12. 7. 23:22
  • GC 정의
  • GC가 삭제할 객체를 식별하는 방법
  • GC 동작 과정 (Mark And Sweep)
  • GC 단점 및 STW
  • Heap의 구조
  • GC는 언제 발생할까?

 

GC란?

가비지 컬렉션(Garbage Collection)을 줄여서 GC라고 부른다. GC는 JVM의 Heap 영역에서 참조가 유효하지 않은 객체를 주기적으로 삭제하는 기능이고 GC를 동작시키는 주체, 즉 프로그램을 Garbage Collector라고 한다. JVM 안에 탑재된 또 하나의 작은 메모리 관리 프로그램(S/W)인 셈이다. 이는 메모리 공간을 확보하고 메모리 누수(memory leak)을 막는 역할을 한다. C, C++에서는 이러한 가비지 컬렉션이 없어 개발자가 메모리 할당과 해제를 직접해줘야 하지만, Java는 JVM에 탑재되어 있는 가비지 컬렉터가 메모리를 자동으로 관리해주기 때문에, 개발자는 개발에만 집중할 수 있다는 장점이 있다.


GC는 어떤 기준으로 삭제할 객체를 식별할까? Reachability !

Root Set = Roots = Root-space

JVM의 GC는 GC Root에서부터 참조하고 있는 객체들을 찾아간다. 이렇게 Root에서부터 참조되고 있는 객체들과 그렇지 않은 객체를 분류하는 것이 객체를 제거하기 위한 사전 작업이다. 참조가 유효한 객체를 Reachable Object, 그렇지 않은 객체를 Unreachable Object라고 한다. Unreachable한 상태의 객체는 GC의 대상되어 Heap에서 삭제된다. GC의 대상이 되는 객체, 즉 참조가 유효하지 않은 객체를 Garbage라고 한다.

GC의 Root-space

  1. Stack의 로컬변수
  2. Method의 static 변수
  3. Native Method Stack의 JNI 참조

 


GC 동작 과정 (Mark And Sweep)

Mark And Sweep 알고리즘

1. Mark : GC는 GC Root부터 연결된 객체들을 찾아 marking한다. (reachable/unreachable한 객체를 식별하는 과정)
2. Sweep : GC는 marking 되지 않은 객체, 즉 unreachable objects들을 Heap에서 제거한다.
3. Compaction : GC는 Sweep 이후 분산된 객체들을 Heap의 시작 주소로 모아 채운다. 객체의 이동으로 객체의 메모리 주소값이 변경되고, 메모리의 단편화(파편화)를 막아준다. Mark And Sweep에서 필수가 아니기 때문에, 가비지 컬렉터 종류에 따라 compaction 과정이 없는 경우도 있다.

 


가비지 컬렉션의 단점

  1. GC가 언제 발생하는지 알 수 없다.
  2. GC가 발생하는 동안, Java application은 실행을 멈춘다.

GC를 실행하기 위해, JVM이 프로그램 실행을 중단시키는 것을 stop-the-world(STW)라고 한다. GC를 수행하는 thread를 제외한 모든 thread가 작업을 중단하고, GC 작업을 완료한 이후에 작업을 재개한다. 개발자는 JVM에 탑재된 default 가비지 컬렉터를 JVM options으로 변경할 수 있지만, 어떤 가비지 컬렉터(GC 알고리즘)을 사용하더라도 STW는 발생하기 때문에, 작업환경과 각 GC의 특성을 고려해서 선택하는 것이 중요하다.

GC를 수행하려면, 왜 STW는 불가피한 것일까? 왜 프로그램이 중단돼야만 하는 것일까?

먼저 두 가지를 알아야 한다.

  1. 위에서 언급한 Compaction 작업 중 객체의 이동으로 객체의 메모리 주소값이 변경된다.
  2. GC의 대상인 Heap 영역은 모든 스레드가 공유하는 JVM 메모리 영역이다.


compation 과정 중에, 즉 살아남은 객체가 이동하는 중에,

다른 application 쓰레드가 해당 객체를 사용할 수 있는 경우를 막기 위해, 모든 쓰레드를 중단하는 것.



GC가 발생하는 Heap의 구조

Heap : Young + Old

Heap은 Young, Old generation 두 물리적인 영역으로 나눈다.

이렇게, Heap 영역을 Generation으로 나눈 이유는 무엇일까?

Heap의 모든 객체를 Garbage 인지 검사하는 방식의 GC는 비효율적이고 비용이 많이 드는 작업이다.

JVM GC 설계자들은 경험적으로 대부분의 객체는 금방 unreachable 상태가 되어 Garbage가 되고, 오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다는 것을 알고 있었다.

따라서, 매번 모든 객체의 GC root 참조 상태를 검사하지 않고, 일부만 검사할 수 있도록 generational한 구조를 고안해냈다.

 

 

Young generation : 최근에 생성된 객체들이 저장되는 곳이고 Eden, Survivor0, Survival1 세 영역으로 한번 더 나눈다.
Old generation : Young generation에서 오랫동안 살아남은 객체들이 저장되는 곳이다.

 


GC는 언제 어떻게 발생할까?

객체 생성

Eden

new를 통해 새롭게 생성된 객체들이 Eden에 age-bit 0으로 할당된다.
age-bit는 Young에서 Old로 copy되는 threshold(임계점, 시점)의 역할을 한다. (GC에서 살아남은 객체는 age-bit 1씩 증가)

Minor GC

Minor GC

새로운 객체를 계속 생성하다보면 Eden 영역이 가득 차게 된다. 이 때 발생하는 GC를 Minor GC라고 하며, Young GC라고도 불린다. Mark and Sweep 과정을 통해 unreachable한 객체들은 Eden에서 제거되고, 살아남은 reachable한 객체들은 빈 Survivor0으로 이동한 뒤 age가 1 증가한다.

이렇게 Eden이 꽉차는 시점에 Minor GC가 반복적으로 발생하고, 살아남은 객체들은 빈 Survivor 영역으로 옮겨가고 age 1 증가하는 과정을 반복한다.

Survivor 0,1 중 한 곳은 반드시 비워져 있어야 한다. 그 이유는 무엇일까?

Mark and Sweep 과정을 통해, 참조가 유효하지 않는 객체들이 메모리에서 삭제되면 중간중간이 비게 되는 파편화(단편화) 현상이 발생한다. 예를 들어, 빈 3이란 공간에 새로운 5 메모리를 사용하려고 할 때, 3칸과 2칸은 멀리 떨어지게 되므로, OS 입장에서 메모리 포인터 주소를 전부 들고 있어야한다. 이런 현상이 쌓이게 되면 데이터를 읽어올 때 긴 시간이 소요된다. 그래서 분산된 객체를 모아 비어있는 공간을 줄이기 위해서 Heap의 시작주소로 객체를 쭉 이어붙이는 compaction 작업을 한다. compaction 작업을 할 때, 쓰고 있는 영역(해당 Survivor)에서 데이터를 땡겨 모으는 작업보다 새로운 빈 공간(다른 빈 Survivor)에 차례대로 나열하는 것이 더 빠르다. 그래서 survivor 두 영역 중 한 곳은 compaction 작업을 위해 항상 비워두는 것 !

Promotion

Promotion

Minor GC를 반복하다 보면, 특정 객체들의 age-bit가 JVM에서 설정해놓은 age에 도달하게 되는 경우가 발생한다.
이러한 객체들은 오랫동안 쓰일 것으로 판단돼 Old generation으로 이동시킨다. 이 과정을 Promotion이라고 한다.

Major GC

Major GC

Promotion 과정이 반복되다 보면, Old 영역에 객체들이 가득차게 되고 이때 발생하는 GC를 Major GC라고 하며, Full GC라고도 불린다. Major GC는 오랜 시간이 걸리는 작업이고 이때 stop-the-world가 발생한다. Old 영역이 꽉차서 Major GC을 수행하는 것은 메모리 관리에 실패했다는 것을 의미한다.





참고자료:
https://d2.naver.com/helloworld/1329
https://coding-factory.tistory.com/829