자바로 개발을 하다 보니 GC, JVM, G1 같은 용어들을 자주 접하게 되는데, 그 개념은 대충 알고 있지만, 좀 더 명확하게 정리해둘 필요가 있다는 생각이 들었다.
가비지 컬렉션(Garbage Collection(GC)은 자바의 메모리 관리 방법 중의 하나로, JVM(자바 가상 머신)의 Heap 영역에서 동적으로 할당했던 메모리중 필요 없게 된 메모리 객체(garbage) 를 모아 주기적으로 제거하는 프로세스를 말한다.
C 언어에서는 동적으로 할당한 메모리 공간을 malloc이나 calloc으로 할당하고, 사용 후엔 free로 명시적으로 해제해야 했지만 Java는 가비지 컬렉션(GC) 덕분에 자동으로 메모리를 관리해주기 때문에, 메모리 해제를 직접 신경 쓰지 않아도 된다.
먼저 JVM에 대해 정리하는게 좋다고 생각했다. JVM은 자바에서만 사용되는 것이 아니다. 예를 들면 대표적으로 코틀린도 JVM 동작방식을 사용한다. JVM은 운영체제 위에서 동작하는 프로세스로 자바 코드를 컴파일해서 얻은 바이트 코드를 해당 운영체제가 이해할 수 있는 기계어로 바꿔 실행시켜주는 역할을 한다.JVM의 구성을 살펴보면 크게 4가지(Class Loader, Execution Engine, Garbage Collector, Runtime Data Area)로 나뉜다.

자바 소스를 작성하면 .java 파일이 생성되고, 이 파일은 자바 컴파일러를 통해 컴파일되어 .class 파일(바이트코드)이 생성된다. 이렇게 생성된 .class 파일들을 JVM이 운영체제로부터 할당받은 메모리 영역인 Runtime Data Area에 적재하는 역할을 하는 것이 Class Loader다. (자바 애플리케이션이 실행될 때 이 작업이 수행된다.)
Class Loader에 의해 메모리에 적재된 클래스(바이트 코드)를 기계어로 변환하여 명령어 단위로 실행하는 역할을 한다. 명령어를 하나하나 실행하는 인터프리터(Interpreter) 방식이 있고, JIT(Just-In-Time) 컴파일러를 이용해 바이트 코드를 네이티브 코드로 변환하여 실행하는 방식도 있다. JIT 컴파일러는 적절한 시점에 바이트 코드를 네이티브 코드로 컴파일하고, Execution Engine은 이를 실행하여 성능을 높입니다.
GC는 Heap 메모리 영역에 생성된 객체들 중에서 더 이상 참조되지 않는 객체들을 탐색하여 제거하는 역할을 한다. GC가 실행되는 정확한 시점을 알 수 없으며, 객체가 참조를 잃자마자 해제되는 것을 보장하지 않는다. 또한, GC가 실행되는 동안 GC 쓰레드 외의 다른 모든 쓰레드는 일시 정지된다. 특히 Full GC가 발생하면 수 초간 모든 쓰레드가 멈추기 때문에, 이로 인해 장애가 발생할 수 있습니다.
JVM의 메모리 영역으로, 자바 애플리케이션을 실행할 때 필요한 데이터들이 적재되는 공간이다. 이 영역은 크게 다음과 같이 나눠진다.
Method Area: 클래스 정보와 메서드 데이터를 저장하는 영역
Heap Area: 동적으로 할당된 객체를 저장하는 영역
Stack Area: 메서드 호출과 로컬 변수들을 저장하는 영역
PC Register: 현재 실행 중인 명령어를 저장하는 레지스터
Native Method Stack: 네이티브 메서드를 호출할 때 사용하는 스택
힙 영역은 우선 5개의 영역(eden, survivor1, survivor2, old, permanent)으로 나뉜다. Java 메모리의 각 영역에서 GC가 발생하면, 사용하지 않는(참조가 존재하지 않는) 객체들은 메모리에서 제거된다. 사용하지 않는 객체를 제거한다고 하는데 어떤 기준으로 제거를 하는 걸까? 따라서 GC를 위해서는 우선 메모리에 있는 객체가 현재 사용중인지 사용중이 아닌지를 구분할 수 있어야 한다.

자바의 가비지 컬렉션(GC) 과정은 객체의 생명 주기에 따라 메모리에서 객체를 관리하고 제거하는 방식이다. 이를 Young Generation과 Old Generation을 나누어 처리한다.
처음 생성된 객체는 Young Generation의 Eden 영역에 할당된다.
Minor GC가 발생하면, Eden 영역에서 사용되지 않고 참조되지 않는 객체들은 메모리에서 제거된다.
Eden 영역에서 살아남은 객체는 Survivor 영역으로 이동한다. 이때 객체는 Survivor1과 Survivor2 사이를 번갈아가며 이동한다.
각 Minor GC가 발생할 때마다, 살아남은 객체는 Survivor1 ↔ Survivor2 영역을 오가며, 참조되지 않는 객체는 제거된다.
Survivor 영역에는 특별한 규칙이 있는데, Survivor 0 또는 Survivor 1 둘 중 하나에는 꼭 비어 있어야 하는 것이다.
Survivor 영역에서 여러 번 살아남은 객체는 최종적으로 Old Generation 영역으로 이동한다.
Old Generation에 있는 객체들은 더 이상 참조되지 않으면 Full GC를 통해 제거된다.
Full GC는 Old Generation에서 사용되지 않는 객체를 찾아 제거하는 작업이다. 이 과정에서 메모리 관리가 이루어진다.
Minor GC
🤔 Young Generation 영역은 짧게 살아남는 메모리들이 존재하는 공간이다. 모든 객체는 처음에는 Young Generation에 생성된다. Young Generation의 공간은 Old Generation에 비해 상대적으로 작아서, 메모리 상의 객체를 찾아 제거하는 데 시간이 적게 걸린다. (작은 공간에서 데이터를 찾으니까) 이 때문에 Young Generation에서 발생하는 GC는 Minor GC라고 불린다.
Major GC
🤔 Old Generation은 길게 살아남는 메모리들이 존재하는 공간이다. Old Generation의 객체들은 처음에는 Young Generation에서 시작되지만, GC 과정 중에 제거되지 않으면 age 임계값을 넘어서 Old Generation으로 이동된 객체들이다. 그리고 Major GC는 객체들이 계속 Promotion되어 Old Generation의 메모리가 부족해지면 발생하게 된다.
Young Generation 영역에서 오래 살아남은 객체는 Old Generation으로 옮겨지는데, 그 기준은 Young Generation에서 Minor GC가 발생하는 동안 얼마나 살아남았는지로 판단한다. 각 객체는 Minor GC에서 살아남은 횟수를 기록하는 age bit를 가지고 있으며, Minor GC가 발생할 때마다 age bit 값은 1씩 증가한다. age bit 값이 MaxTenuringThreshold라는 설정값을 초과하면 객체는 Old Generation으로 이동된다. 또한 age bit가 MaxTenuringThreshold를 초과하기 전이라도 Survivor 영역의 메모리가 부족하면 객체가 미리 Old Generation으로 옮겨질 수도 있다.
Stop-the-world는 GC(Garbage Collection)가 수행될 때, 애플리케이션의 모든 쓰레드가 멈추는 현상을 말한다.
GC는 메모리를 정리하는 과정이기 때문에, 그 과정에서 객체를 재배치하거나 정리하는 작업을 해야 하는데, 이 작업 중에는 애플리케이션의 실행을 잠시 멈춰야 한다. 그래서 모든 애플리케이션 쓰레드가 중지되는 거다.
이때 발생하는 멈춤 현상이 Stop-the-world 이벤트라고 불린다. GC가 진행되는 동안 애플리케이션은 일시적으로 중지되고, GC가 끝나면 다시 실행을 재개한다.
주로 Full GC나 Major GC에서 발생하고, Young Generation에서 일어나는 Minor GC는 일반적으로 상대적으로 짧아서 Stop-the-world가 덜 느껴진다.
참고