1. Garbage Collection(가비지 컬렉션)이란?
프로그램을 개발 하다 보면 유효하지 않은 메모리 인 가비지(Garbage)가 발생하게 됩니다. C언어를 이용하면 free()라는 메서드를 통해 메모리 해제를 해주어야 하지만 Java나 Kotlin을 이용해 개발을 하다 보면 개발자가 직접 메모리를 해제해주는 일이 없습니다. JVM의 가비지 컬렉터가 불필요한 메모리를 알아서 정리해주기 때문입니다. 대신 Java에서 명시적으로 불필요한 데이터를 표현하기 위해서는 일반적으로 null로 선언 해줍니다
Person person = new Person();
person.setName("ahkong");
person = null;
// 가비지 발생
person = new Person();
pserson.setName("ahreum");
기존의 ahkong으로 생성된 person 객체는 더이상 참조를 하지 않고 사용이 되지 않아서 가바지(Garbage)가 되었습니다.
Java나 Kotlin은 이러한 메모리 누수를 방지하기 위해 가비지 컬렉터(Garbage Collector, GC)가 주기적으로 검사하여 메모리를 정리합니다.
2. Minor GC와 Major GC
JVM의 Heap영역은 처음 설계될 때 2가지 전제(Weak Generational Hyphthesis)로 설계됐습니다.
- 대부분의 객체는 금방 접근 불가능한 상태(Unreachable)가 된다.
- 오래된 객체에서 새로운 객체로의 참조는 아주 적게 존재한다.
즉, 객체는 대부분 일회성이며, 메모리에 오랫동안 남아있는 경우는 드물다는 것입니다. 그렇기 때문에 객체의 생존 기간에 따라 물리적인 Heap영역을 나누게 되었고 Young,Old 총 2가지 영역으로 설계되었습니다. 초기에는 Perm영역이 존재하였지만 Java8 부터 제거되었습니다.

- Young 영역 (Young Generation)
- 새롭게 생성된 객체가 할당(Allocation)되는 영역
- 대부분의 객체가 금방 Unreachable 상태가 되기 때문에, 많은 객체가 Young 영역에 생성되었다가 사라짐
- Young 영역에 대한 가비지 컬렉션을 Minor GC라고 부름
- Old 영역(Old Generation)
- Young 영역에서 Reachable 상태를 유지하여 살아남은 객체가 복사되는 영역
- Young 영역보다 크게 할당되며, 영역의 크기가 큰 만큼 가비지는 적게 발생
- old 영역에 대한 가비지 컬렉션을 Major GC 또는 Full GC라고 부름
Old 영역이 Young 영역보다 크게 할당되는 이유는 Young 영역의 수명이 짧은 객체들은 큰 공간을 필요로 하지 않으며 큰 객체들은 Young 영역이 아니라 바로 Old 영역에 할당되기 때문입니다.
예외적인 상황으로 Old 영역에 있는 객체가 Young 영역의 객체를 참조하는 경우도 존재할 것입니다. 이러한 경우를 대비하여 Old 영역에는 512 bytes의 덩어리(Chunk)로 되어 있는 카드 테이블(Card Table)이 존재합니다.

카드 테이블에는 Old 영역에 있는 객체가 Young 영역의 객체를 참조할 때 마다 그에 대한 정보가 표시 됩니다.
카드 테이블이 도입된 이유는 무엇일까요? 답은 간단합니다! Young 영역에서 가비지 컬렉션 (Minor GC)가 실행될 때 모든 Old 영역에 존재하는 객체를 검사하여 참조되지 않는 Young 영역의 객체를 식별하는 것이 비효율 적이기 때문입니다. 그렇기 때문에 Young 영역에서 가비지 컬렉션이 진행될 때 카드테이블만 조회하여 GC의 대상인지 식별 할 수 있도록 하고 있습니다.
3. Garbage Collection (가비지 컬렉션) 과정
Young 영역과 Old 영역은 서로 다른 메모리 구조로 되어 있기 때문에, 세부족인 동작 방식은 다르지만 기본적으로 가비지 컬렉션이 실행된다고 하면 다음의2가지 공통적인 단계를 따르게 됩니다.
- Stop The world
- Mark and Sweep
Stop The World
stop-the-world란 GC를 실행하기 위해 JVM이 애플리케이션 실행을 멈추는 것입니다. stop-the-world가 발생하면 GC를 실행하는 쓰레드를 제외한 나머지 쓰레드는 모두 작업을 멈춥니다. GC 작업을 완료한 이후에 중단했던 작업을 다시 시작 합니다. 어떤 GC 알고리즘을 사용하더라도 stop-the-world는 발생하기 때문에 GC 튜닝 작업은 결국 stop-the-world 시간을 단축하는 작업이라고 볼 수 있습니다.
위에서도 언급하였듯이 Java는 프로그램 코드에서 메모리를 명시적으로 지정하여 해제하지 않습니다.명시적으로 해제 할 수 있는 방법은 null로 선언하거나 System.gc() 메서드를 호출 해야 합니다. null로 지정하는 것은 큰 문제가 안되지만 System.gc()메서드를 호출하는 것은 시스템의 성능에 매우 큰 영향을 끼치므로 System.gc()는 절대로 사용하면 안됩니다.
Mark and Sweep
- Mark : 사용되는 메모리와 사용되지 않는 메모리를 식별하는 작업
- Sweep : Mark 단계에서 사용되지 않음으로 식별된 메모리를 해제하는 작업
즉, 가비지 컬렉션의 과정은 다음과 같습니다.
Stop The world를 통해 모든 작업을 중단시키면, GC는 스택의 모든 변수 또는 Reachable 객체를 스캔하면서 각각이 어떤 객체를 참고하고 있는지를 탐색하게 됩니다. 그리고 사용되고 있는 메모리를 식별(Mark)하여 Mark가 되지 않은 객체들을 메모리에서 제거(Sweep)합니다.
1. Minor GC의 동작 방식
Minor GC를 정확하게 이해하기 위해서는 Young 영역의 구조에 대해 이해를 해야 합니다. Young 영역은 3개의 영역으로 나뉩니다.
- Eden 영역
- Survivor 영역(2개)
Survivor 영역이 2개이기 때문에 총 3개의 영역으로 나뉘는 것입니다. 각 영역의 처리 절차를 순서에 따라 기술하면 다음과 같습니다.
- 새로 생성한 대부분의 객체는 Eden 영역에 위치한다
- Eden 영역에서 GC가 한 번 발생한 후 살아남은 객체는 Survivor 영역 중 하나로 이동된다.
- Eden 영역에서 GC가 발생하면 이미 살아남은 객체가 존재하는 Survivor 영역으로 객체가 계속 쌓인다.
- 하나의 Survivor 영역이 가득 차게 되면, 그 중에서 살아남은 객체를 다른 Survivor 영역으로 이동한다.
그리고 가득 찬 Survivor 영역은 아무 데이터도 없는 상태로 된다.
- 이 과정을 반복하다가 계속해서 살아남아 있는 객체는 Old 영역으로 이동하게 된다.
결국 위의 절차에서 기억해야 할 점은 Eden 영역에서 최초로 객체가 만들어지고, Survivor 영역을 통해 Old 영역으로 오래 살아남은 객체가 이동한다는 것입니다
이 절차를 확인해보면 알겠지만 Survivor 영역 중 하나는 반드시 비어 있는 상태로 남아있어야 합니다. 만약 두 Survivor 영역에 모두 데이터가 존재하거나, 두 영역 모두 사용량이 0이라면 비정상적인 상황이라고 합니다.
Minor GC를 통해서 Old 영역까지 데이터가 쌓이는 것을 간단히 나타내면 아래의 그림과 같습니다.

HotSpot JVM에서는 Eden 영역에 객체를 빠르게 할당 (Allocation) 하기 위해 bump the pointer 와 TLABs(Thread-Local Allocation Buffers)라는 기술을 사용하고 있습니다.
- bump the pointer ?
- Eden 영역에 마지막으로 할당된 객체의 주소를 캐싱해 두는 것
- 새로운 객체를 위해 유효한 메모리를 탐색할 필요 없이 마지막 주소의 다음을 사용하게 함으로써 속도를 높이고 있음
- 이를 통해 새로운 객체를 할당할 때 객체의 크기가 Eden 영역에 적합한지만 판별하면 되기 때문에 빠르게 메모리 할당을 할 수 있음
싱글 쓰레드 환경이라면 문제가 없겠지만 멀티쓰레드 환경이라면 객체를 Eden 영역에 할당할 때 락(Lock)을 걸어 동기화를 해주어야 합니다. 멀티쓰레드 환경에서의 성능 문제를 해결하기 위해 HotSpot JVM은 추가로 TLABs(Thread-Local Allocation Buffers) 라는 기술을 도입하였습니다.
- TLABs(Thread-Local Allocation Buffers) ?
- 각각의 쓰레드마다 Eden 영역에 객체를 할당하기 위한 주소를 부여함으로써 동기화 작업 없이 빠르게 메모리 할당하는 기술
- 각각의 쓰레드는 자신이 갖는 주소에만 객체를 할당함으로써 동기화 없이
bump the pointer를 통해 빠르게 객체를 할당하도록 해줌
2. Major GC의 동작 방식
Old 영역은 기본적으로 데이터가 가득 차면 GC를 실행합니다. Young 영역은 일반적으로 Old 영역보다 크기가 작기 때문에 GC가 보통 0.5초에서 1초 사이에 끝이 납니다. 따라서 Minor GC는 애플리케이션에 큰 영향을 주지 않습니다. 하지만 Old 영역은 Young 영역보다 크며 Young 영역을 참조 할 수도 있습니다. 그렇기 때문에 Major GC는 일반적으로 Minor GC보다 시간이 오래걸리며, 10배 이상의 시간을 사용합니다.
또한 Major GC의 동작 방식은 GC 방식에 따라 처리 절차가 달라집니다.
JDK 7을 기준으로 5가지의 방식이 있습니다.
- Serial GC
- Parallel GC
- Parallel Old GC(Parallel Compacting GC)
- Concurrent Mark & Sweep GC(이하 CMS)
- G1(Garbage First) GC
이 중에서 운영 서버에 절대 사용하면 안되는 방식이 Serial GC입니다. Serial GC는 데스크톱의 CPU 코어가 하나만 있을 때 사용하기 위해서 만든 방식으로 Serial GC를 사용하면 애플리케이션의 성능이 많이 떨어지게 됩니다.
(각 방식의 동작 방식에 대해서는 다음 포스팅에서 다루어 보도록 하겠습니다!)
마치며
지난 포스팅에서는 자바에서 메모리 누수가 발생하는원인(https://dev-ahkong.tistory.com/14) 에 대한 글을 작성하였고 이번 포스팅에서는 메모리를 관리(?) 해주는 Garbage Collection의 개념 및 간단한 동작 방식에 대한 글을 정리해보았습니다. 다음 포스팅에서는 위에서도 언급하였듯이 Major GC의 5가지 방식의 동작 방식에 대한 글을 작성 하고 메모리 누수 및 GC에 대한 글을 마무리 하려합니다.
코드의 스킬이 아닌 내부 동작 방식에 대한 공부는 오랜만에 해서 개인적으로는 흥미로운 주제를 공부할 수 있어서 재밌었습니다.
또한 어떤 개념이든 내부 동작 방식 및 개념 이해하는 것은 중요하고 꼭 필요한 시간이라는 생각이 들었습니다.
내부 동작 및 개념을 이해 함으로써 코드 작성시 고려 할 사이드 이펙트의 스펙트럼을 넓힐 수 있고 이슈가 발생 하였을 때도 다양한 시각에서 원인을 파악 할 수 있지 않을까요? ㅎㅎ
참고
https://d2.naver.com/helloworld/1329
https://mangkyu.tistory.com/118
'Dev > Java' 카테고리의 다른 글
| [Java] JVM 메모리 구조 (0) | 2022.12.05 |
|---|---|
| [Java] Default Method in Java8 (0) | 2022.11.07 |
| [JAVA] 메모리 누수(Memory Leak) (0) | 2022.09.18 |
| 쓰레드 동기화 (Thread Synchronization) (0) | 2022.08.06 |
| Optional의 안티 패턴을 피하는 방법 😎 (0) | 2022.07.22 |