안녕하세요. 오늘은 자바의 Garbage Collection에 대해서 알아보겠습니다.
1. 학습 목표
학습 목표는 당연히 Garbage Collection(이하 GC)에 대해서 공부하는 것이지만, 개인적으로 다른 목표도 있습니다.
그것은 '그래서, 왜 GC에 대해서 자세히 공부해야 하는데?' 에요.
GC의 기본적인 개념은 '할당된 메모리 영역 중 더 이상 사용하지 않는 영역에 대해서 할당을 해제하는 것'이라고 할 수 있습니다. 사실 자바를 사용한다면 사용자가 GC를 직접적으로 호출하지 않고, 오히려 이는 권장되지 않는 행동이에요. JVM이 자체적으로 판단하여 최적의 타이밍에 GC를 수행하기 때문입니다.
그렇다면 개발자가 관여할 수 있는 부분이 없거나 적다는 것인데, 저는 이런 의문을 가졌어요. GC가 어떻게 동작하는지 왜 알아야 할까요? 단순하게, '자바를 사용하면 자바에 대해서는 잘 알아야지!'라는 말로는 설득이 되지 않았습니다.
그래서 저는 제가 GC에 대해서 자세히 몰라서, 혹은 GC를 고려할 만큼의 규모 있는 프로그램을 개발해보지 않아서 그렇게 생각하는 것일지 궁금해졌어요. 그래서 GC에 대해서 학습하고, GC에 대해서 왜 공부해야 하는가 나름의 결론도 내려보려 합니다.
2. JAVA에서의 Garbage Collection
앞에서도 짧게 언급했지만, GC는 결국 한마디로 하면 '할당 된 메모리 영역 중 더 이상 사용하지 않는 영역에 대해서 할당을 해제하는 것'입니다. 자바는 C언어와 상당히 유사한데요, 개인적으로 가장 큰 차이점을 뽑으라고 한다면 메모리 관리라고 할 것 같습니다. C언어는 수동으로 메모리를 할당하고 해제하지만, 자바는 그렇지 않아요. Oracle의 Java 공식문서에서 GC를 소개할 때 What is Automatic Garbage Collection?이라는 말이 나와요. 즉, GC의 과정이 JVM의 판단 하에 자동으로 수행됩니다. 본격적으로 GC가 수행되는 과정에 대해서 알아보기 전에, GC 설계의 원칙이 된 이론들을 알아보겠습니다.
3-1. GC의 2가지 원칙
- 반드시 모든 Garbage를 수집해야 한다.
- 살아있는 객체는 절대로 수집해선 안 된다.
GC는 위의 2가지의 원칙을 준수해야 합니다. 당연한 말인 것 같지만 휴먼 에러가 가장 많이 발생할 수 있는 부분이라고 합니다.
3-2. Weak Generational Hypothesis
"대부분의 객체는 금방 만들어지고 금방 죽는다."라는 가설이에요.
이 가설은 대부분의 객체가 짧은 수명을 가지며, 그 수명이 짧은 객체들은 메모리에서 빠르게 회수될 수 있어야 한다는 것을 의미합니다. 또한, 이러한 객체가 아니라면 기대 수명이 훨씬 길다는 것을 의미합니다.
4. Mark-Sweep-Compact
이제, 자바의 기본 GC 알고리즘인 Mark-Sweep-Compact에 대해서 알아보겠습니다.
- 우선, 객체가 Heap에 생성되면 False(0)의 값을 가지는 마크 비트를 가집니다.
- GC가 시작되면, 가비지 컬렉터는 root(메모리 풀 외부에서 내부를 가리키는 포인터)에서 시작하여 객체 트리를 순회하면서 객체에 도달할 수 있다면 마크 비트를 True(1)로 변경합니다. > Mark
- 그 후에, 가비지 컬렉터는 힙을 순회하면서 마크 비트가 False(0)인 모든 항목에 대해서 메모리를 회수합니다. > Sweep
- 마지막으로 객체들의 마크비트를 0으로 초기화 하고 할당된 메모리들을 모아서 메모리 단편화를 방지합니다. > Compact
5. Stop the World
위에서 간단하게 GC가 수행되는 과정을 알아보았는데요, 이러한 과정이 애플리케이션이 동작할 때 이루어진다면 많은 문제를 야기할 것이라고 예상할 수 있습니다. 따라서 4-2가 시작될 때, 즉 GC가 시작할 때 'Stop thr World'라는 것이 발생합니다. 이는 GC를 수행하는 스레드를 제외한 모든 스레드가 동작을 멈추고, GC가 끝난 후에 중단했던 작업을 다시 수행하는 것입니다. 어떤 GC 알고리즘을 사용해도 발생하므로, GC 튜닝은 보통 이 시간을 줄이는 것을 의미합니다.
6. GC 수행 시점
그렇다면 GC는 언제 발생할까요? 각각의 환경에 따라서 다르지만, 일반적으로는 아래와 같이 분류할 수 있습니다.
- 할당 실패 : 사용 가능한 공간이 부족하여 힙에 객체를 할당할 수 없는 경우, JVM은 GC를 수행합니다.
- 힙 사이즈 : 힙이 특정 임계값에 도달하면 JVM은 GC를 수행하여 메모리를 회수합니다.
- 시간 : 일부 GC 알고리즘은 정해진 주기마다 GC를 수행합니다.
- 강제 호출 : System.gc(), Runtime.getRuntime().gc()로 강제로 GC를 수행할 수 있습니다.(단, 이 메소드들의 호출이 GC가 수행된다고 보장하지 않습니다. 강제로 호출한 시점에 JVM이 판단하여 GC를 진행합니다.)
7. Heap Memory
위에서 정리한 개념들을 바탕으로 조금 더 자세하게 GC가 동작하는 방법에 대해 알아보려고 합니다. 그러기 위해서는 새로 할당된 객체가 상주하는 공간인 JVM의 Heap에 대해서 조금 알아야 해요.
위의 사진에서도 저희가 주목할 부분은 New/Young Generation과 Old/Tenured Generation이에요. 위에서 언급한 Weak Generational Hypothesis에 의거하여, 젊은 객체와 늙은 객체를 따로 관리하는 것이 더 효율적이기 때문에 이렇게 구분했다고 합니다. Young과 Old가 분리되어 있고, Young은 Eden과 Survivor(From, To)로 나뉘어 있다는 것을 아시면 될 것 같아요.
아래서 언급할 내용을 이해하려면 이정도면 충분하지만, Reserved 영역이 무엇인지 궁금해서 조금 더 찾아보았습니다.
Reserved
위에서 각 공간마다 'Reserved'라는 예약되어 있는 공간이 있는 것을 확인할 수 있는데요, 이 공간은 객체를 저장하기 위해 우선적으로 할당되는 공간입니다. 즉, JVM이 해당 공간을 사용할 수 있도록 미리 확보해 놓은 상태를 의미합니다. 이 공간의 크기에 대해서는 -Xmx 옵션을 통해서 지정할 수 있어요.
Committed
위에 사진에서는 나와있지 않지만, Committed 영역은 JVM이 실제로 사용하는 영역으로 객체가 메모리에 할당되는 공간이에요. 즉, 실제로 사용할 수 있도록 보장된 메모리입니다. 객체가 메모리에 할당되면, Reserved 영역을 Committed 영역으로 변경하고 이 영역에서 객체를 생성하고 관리합니다.
8. Young Generation / Minor GC
Young Generation에서 발생하는 GC를 Minor GC라고 지칭합니다. Minor GC에 대해서 설명하기 전에, Young을 구성하는 Eden과 Survivor에 대해서 간단하게 알아보겠습니다.
Eden Space
새로운 객체가 생성되는 메모리 풀입니다. 위에서 새로운 객체가 생성되면 Heap에 할당된다는 말을 했었는데요, 조금 더 자세하게 얘기하면 Heap의 Eden 영역에 할당됩니다. 이 공간이 가득 차면 가비지 컬렉터는 Minor GC를 수행하고 죽은 객체가 아니라면 Survivor 영역에 저장합니다.
Survivor Space
Eden 영역의 GC로 인해 발생하는 오버헤드를 줄이고자 생긴 영역입니다. From(0)과 To(1)라는 두 개의 공간으로 나뉘며 Eden 영역보다 공간이 훨씬 작습니다.
그럼 이제 Young Generation에서 발생하는 Minor GC에 진행 과정에 대해서 알아보겠습니다.
Minor GC
- 새롭게 생성된 객체는 Eden 영역에 할당됩니다. 이때 마크비트는 False(0), 객체의 나이(Generational count)는 0으로 설정됩니다.
- 이때, Eden 영역에 공간이 없다면 Minor GC를 수행합니다. 이때가 Stop the World의 시작입니다.
- Eden 영역에 할당된 객체를 루트로부터 순회하면서 접근 가능한 객체의 참조비트를 True(1)로 변경합니다. > Mark
- 생존한 객체들은 나이가 1 증가하면서 Survivor 영역의 From으로 복사됩니다.
- Eden 영역의 마크비트가 False(0)인 객체들에 대해서 메모리를 회수합니다. 이때 Survivor 영역이 가득 차서 복사가 불가능한 상황이 아니라면, Stop the World가 종료됩니다. > Sweep
- Survivor 영역도 가득 차거나 연속된 메모리 공간이 없어 복사가 불가능한 상황이라면 먼저 From 영액에 있는 객체들을 To로 옮기고, Eden 영역에서 살아있는 객체들을 To로 복사합니다.
(Eden 영역에 있는 객체보다 From 영역에 있는 객체들이 더 오래 살 가능성이 높기 때문에 이와 같은 순서로 진행합니다.) - 위의 과정도 끝난다면, From공간과 To공간의 역할이 바뀌었으므로 둘의 이름을 바꿔줍니다.
이러한 과정을 Minor GC라고 합니다.
9. Promotion
Minor GC의 과정을 보다 보면, 그래서 결국 Eden이랑 Survivor가 다 차면 어떻게 하지?라는 의문이 생깁니다. 결국은 Old 영역으로 이동하는 과정이 있어야겠죠. 객체의 Aging(나이가 증가하는 것)을 통해서 Old 영역으로 보낼 객체를 판단합니다. 즉, 객체가 GC로부터 살아남을수록 나이가 1살씩 증가하는 노화가 발생하고 적당한 나이가 되면 Survivor 영역에 있는 객체는 Old 영역으로 이동하게 되는데, 이를 Promotion(승진)이라 합니다.
Premature Promotion
노화가 진행되어 Old 영역으로 이동하는 경우가 아닌, 다른 이유로 Old 영역에 할당되는 경우를 Premature Promotion이라 합니다. 메모리 할당이 너무 잦아서 공간이 없는 경우, Eden 영역의 용량보다 큰 객체가 할당되는 경우 등으로 발생합니다.
Card Table
위에서 Young 영역의 객체들을 Mark 하는 과정을 통해서 살아있는 객체인지 판별한다고 했는데요, 그렇다면 이렇게 Promotion으로 Old 영역으로 이동한 객체가 Young 영역의 객체를 참조하고 있다면 어떻게 할까요?
이런 경우에는, Old 영역에 Card Table을 두어 관리합니다. 그리고 Minor GC가 발생할 때 Old 영역을 전부 검사하지 않고 Card Table만 참조하는 방식으로 GC가 수행됩니다.
10. Old Generation / Major GC
Old Generation에서 발생하는 GC를 Major GC라고 합니다. Major GC도 Old 영역이 꽉 찼을 때 발생하는데, 이 공간자체가 Young 영역보다 크고, 메모리 할당률이 낮고, 많은 비용이 들기 때문에 Minor GC에 비해 발생하는 빈도가 낮습니다.
이곳에서도 Mark-Sweep 알고리즘이 사용되지만, Mark-Compact와 G1 같은 알고리즘도 많이 사용된다고 합니다.
(이 부분에 대해서는 추후에 Garbage Collector의 종류와 함께 새로운 글을 작성해 보겠습니다.)
11. 마무리
지금까지 자바의 GC에 대해서 정말 기본적인 내용들을 알아보았습니다.
혹시 여기까지 읽으시면서 처음에 제가 정한 학습 목표인 '그래서 왜 자동으로 이루어지는 GC에 대해서 알아야 하는데?'에 대한 답을 찾았냐고 물어보신다면, 어느 정도 찾았다고 대답할 것 같아요. 그 이유는
- 이 글에는 자세히 언급하지 않았지만, 여러 옵션을 통해 GC를 튜닝할 수 있음(각종 영역에 대한 크기 등...)
- GC를 튜닝하기 위해서 GC가 어떻게 동작하고, 어떤 알고리즘을 사용하고, 어떤 Collector를 사용할지를 결정해야 함
- 실제 서버를 운영하게 될 시 메모리 사용량 관리를 위해서
- 성능의 향상을 위해서
라고 정리할 것 같습니다.
조금 어려운 내용들이라 두렵긴 하지만 이후에 Major GC에 적용되는 알고리즘, Garbage Collector, GC 튜닝 옵션들과 같은 내용들을 공부해서 글로 작성해 보도록 하겠습니다.
잘못된 내용에 대한 피드백은 언제든지 환영입니다!
긴 글 읽어주셔서 감사합니다.
참고 자료
https://d2.naver.com/helloworld/1329
https://m.post.naver.com/viewer/postView.nhn?volumeNo=23726161&memberNo=36733075
'JAVA' 카테고리의 다른 글
자바의 Record로 DTO를 만들어보자 - 2 (0) | 2023.08.10 |
---|---|
Stream API의 map에 대해서 코드로 이해해보자 (2) | 2023.05.12 |
자바의 Record로 DTO를 만들어보자 (2) | 2023.03.22 |