기록하기

Java의 GC 진행 방법 본문

language/java

Java의 GC 진행 방법

jjungdev 2023. 8. 18. 22:45

자바의 아키텍처를 보면 JDK 와 JRE 로 나뉘어져 있는데, JRE 는 자바를 실행할 수 있는 환경의 집합이라고 볼 수 있고 자바에서 제공하는 여러 라이브러리로 구성되어 있다. 이 중에서 GC 는 자바의 성능과도 관련이 있는데, 자동화된 가비지 컬렉터는 낮은 우선순위의 쓰레드로 동작하기 때문에 자바가 높은 성능을 낼 수 있도록 해준다.

 

GC 란 무엇인가

먼저, JVM 이라는 용어를 이해해야 한다.

JVM(Java Virtual Machine), 즉 자바 가상 머신은 자바 프로그램이 수행되는 프로세스를 의미한다. java 라는 명령어로 애플리케이션이 수행되면 JVM 위에서 애플리케이션이 동작하는 것이다.

 

여기서 JVM Heap 메모리와 관련하여 한 가지 주의해야할 것이 있는데, 가급적이면 maxHeapSize, minHeapSize 를 지정해주는 것이 좋다는 점이다.

 

디폴트로 설정된 Heap 메모리 사이즈는 아래 명령어를 통해 알 수 있는데 필자의 경우 32GB 노트북이라서 아래와 같이 정보가 나오게 된다.

 

java -XX:+PrintFlagsFinal -version | grep -iE 'heapsize|permsize|threadstacksize'

 

여기서 InititialHeapSize 가 0.5GB 정도, MaxHeapSize 가 8GB 인 것을 알 수 있는데 머신에 따라 다르겠지만 physical memory 의 1/4 정도까지 Heap 메모리로 사용하는 것을 알 수 있다.

따라서 배치 애플리케이션과 같이 갑자기 대용량 데이터 처리를 할 경우 메모리가 늘어날 위험이 있기 때문에 가급적이면 사이즈 지정을 해주는 것이 좋다고 한다.

 

 

그리고 JVM 에서 스스로 메모리 관리를 하는데 이것이 바로 GC, 가비지 컬렉터이다.

G1(Garbage First) 을 제외한 나머지 JVM 은 Heap 공간에 객체들을 관리하는데 자바의 Heap 영역을 그려보면 다음과 같다.

  • Young : 젊은 객체들이 존재
    • Eden : 객체를 생성하자마자 저장되는 저장소
    • 2개의 Survivor Space
  • Old : 늙은 객체들이 존재
  • Perm : 클래스나 메소드가 존재

즉, 객체가 생성되자마자 Eden 영역에 생성이 된다. Eden 영역이 꽉차게 되면, 살아있는 객체들은 Survivor space(S0) 영역으로 복사되고, 이후에 생성되는 객체들은 다시 Eden 영역을 채운다.

이 후 S0 영역이 꽉차게 되면, 다른 Survivor Space(S1) 영역으로 객체가 복사되는데, 이때 Eden 영역에서 살아있는 객체들도 S1 영역으로 가게 되어 Eden 영역과 S0 영역이 비워진다.

 

즉, 무조건 하나의 Survivor Space 영역을 비게 만든다.

 

이것이 Minor GC(Young GC)이다.

만약, 다음 Minor GC 가 발생하면 다시 Eden 영역과 S1 영역에서 살아있는 객체들이 S0 영역으로 이동하면서 Eden, S1 영역을 비우는 방법으로 진행이 된다.

 

이렇게 계속 진행되다가 오래 살아있는 객체들은 Old 영역으로 이동하게 되는데 Old 영역도 꽉차게 되면 Major GC(Full GC)가 발생한다.

 

Minor GC, Major GC, Full GC

앞서 설명한 내용을 좀 더 정리해보면 다음과 같다.

  • Minor GC
    • Eden 영역이 가득 찼을 때 발생하는 GC
    • Minor GC 가 발생했을 때 Old 영역으로 이동된 객체는 관계가 없다.
    • Minor GC 가 발생해도 stop-the-world 가 발생할 수 있다.
  • Major GC
    • Old 영역에서 발생하는 GC
    • Minor GC 에 비해 속도가 느리다.
    • stop-the-world 문제가 발생한다.
  • Full GC
    • Young, Old 영역 전체에서 발생하는 GC
    • 거의 일어나면 안된다.

 

여기서 공통적으로 발생한 내용은 stop-the-world 가 발생한다는 것이다.

 

왜 stop-the-world 가 발생할까?

 

GC 는 Heap 영역과 연관되어 있는데 멈추지 않고 GC 가 실행될 경우 애플리케이션 동작에 문제가 발생할 수 있기 때문에 GC 와 관련된 스레드를 제외하고는 모든 스레드가 멈춘 뒤 Mark and Sweep 이 일어나게 된다.

따라서, 이 stop-the-world 시간을 줄이고 관련 문제를 개선하기 위해 끊임없이 알고리즘을 발전시켜왔고, 그 중에서도 필자는 Serial GC, Parallel GC, CMS GC, G1 GC 에 대해서 살펴보고자 한다.

 

그 전에 위에서 Mark and Sweep 이 나왔는데, 기본적인 GC 과정이라서 Mark, Sweep, Compaction 의 개념에 대해 정리해보자!

더보기

Mark : 사용하지 않는 객체를 선별하는 작업
Sweep : 사용하지 않는 객체를 제거하는 작업, 사용하지 않는 메모리 회수
Compaction : 파편화된 메모리 영역을 앞에서부터 채워나가는 작업, 즉 밀어주는 작업

 

GC 알고리즘

Serial GC

  • 단일 코어를 가정하고 나온 GC 알고리즘
  • 스레드 1개로 Mark, Sweep 을 처리하기 때문에 stop-the-world 시간이 매우 길다.
  • 보통 잘 사용하지 않는다.

 

Parallel GC

  • 멀티 스레드 환경에서 빠르게 GC 를 수행하기 위해 만들어진 알고리즘
  • Java 8 디폴트 GC
  • 하지만, Mark 가 끝난 뒤에 Sweep 이 일어나기 때문에 stop-the-world 를 막을 수 없었다.
  • 즉, 멀티 스레드 환경에서도 Mark, Sweep 이 따로 일어나서 여전히 느린 GC

 

CMS(Concurrent Mark Sweep) GC

  • 애플리케이션 스레드와 GC 스레드가 동시에 실행되어 stop-the-world 시간 최소화
  • Mark 를 수행하면서 Sweep 이 실행되어 stop-the-world 시간이 거의 없다.
  • 하지만 CPU 사용량이 많고 메모리가 많이 필요로 하는 Compaction 이 수행되지 않아 메모리 파편화 문제가 발생한다.
  • Java 14 부터 사용하지 않는다.

 

💡 여기서 Compaction 이 발생하지 않으면 왜 메모리 파편화 문제가 발생할까?

만약 2MB 정도의 메모리 할당이 필요한 상황에서 5MB 정도 빈 공간이 있다면 할당이 가능하다고 생각할 수 있으나 Compaction 과정이 수행되지 않는다면 할당이 불가능할 수 있다.
즉, 5MB 공간이 linear 하게 있는 것이 아니라 1MB 씩 남아있다면, allocation 이 불가능해져서 out of memory 문제가 생길 수 있으며 애플리케이션이 실행되지 않을 수 있다.

 

 

G1(Garbage First) GC

G1 GC 는 앞의 GC 그림과는 다른 구조를 가진다.

  • 여기서는 Region 이라는 개념이 나오는데, New/Old 영역이 여러 군데에 퍼져있어서 전체 공간을 GC 안 해도 되고, 메모리가 꽉 찬 영역을 GC 하면 된다.
  • 이 경우 빠르게 메모리를 회수하여 빈 공간을 확보하므로 GC 성능을 높였고, Java 9 부터는 디폴트 GC 이다.
  • 또한 Java 14 부터는 Region 에 걸쳐서 Compaction 이 가능해졌다.

 

 

 

[참고]

- 자바의 신 2편