일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- BOJ
- 페이지 부재율
- 세마포어
- 교착상태
- 알고리즘
- gc
- 프로세스
- 우선순위
- fork()
- 단편화
- redis
- 컴퓨터구조
- 스케줄링
- 페이지 대치
- local cache
- 인터럽트
- 가상 메모리
- mutex
- PYTHON
- garbage collection
- 스레드
- 페이징
- concurrency
- ALU
- 백준
- mips
- Algorithm
- 기아 상태
- 운영체제
- 부동소수점
- Today
- Total
봉황대 in CS
[JVM] JVM의 메모리 할당 전략과 Garbage Collection 본문
* 본 글은 ‘JVM 밑바닥까지 파헤치기’ 책과 Oracle 공식 문서들을 바탕으로 작성하였습니다. (참고 문서의 링크는 하단에 첨부)
JVM's Structure & GC
Java에서 객체 또는 배열을 생성하면 JVM의 Heap이라는 영역에서 메모리를 할당하게 된다. (The heap is the run-time data area from which memory for all class instances and arrays is allocated.) JVM을 구성하는 요소는 Heap 영역 말고도 메서드 호출과 지역 변수를 관리하는 Stack 영역, 그리고 프로그램 실행 시에 필요한 여러 공통 데이터들을 관리하는 Method 영역이 존재한다. 메모리라는 자원은 한정되어 있기 때문에 메모리 고갈 문제를 겪지 않으려면 할당 후 더 이상 사용하지 않는 메모리들은 회수(free) 해야 한다. 이러한 메모리 회수 작업은 Garbage collection(GC)라고 부른다.
그렇다면 3가지 궁금증이 남는다. 첫 번째, GC의 대상을 판별하는 기준은 무엇인가? 즉, 어떤 메모리를 회수해야 하는가? 두 번째, GC를 발생시키는 시점은 언제인가? 세 번째, GC를 어떤 방식으로 하는가?
우선, GC의 집중 관리 대상이 되는 부분은 Heap 영역이다. Heap 영역의 할당은 동적으로 발생하기에 프로그램의 Runtime에만 그 메모리 요구량을 알 수 있기 때문이다. 반대로 Stack 영역은 메서드가 끝나거나 스레드가 종료된다면 자연스럽게 회수되기 때문에 GC를 명시적으로 진행하지 않아도 된다.
[참고] 엄밀하게는, Method 영역은 Heap 영역에 속한다. 하지만 이들이 저장하는 요소(존재하는 목적)이나 관리 매커니즘이 서로 다르다. (1) Method 영역은 클래스의 정보, static 변수나 상수들을 저장하여 Heap 영역의 인스턴스들이 이 값들을 쓸데없이 중복해서 가지지 않도록 한다. (2) GC 전략도 다르게 가져간다. (Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it.)
GC 대상 판별법: 도달 가능성 분석 알고리즘
객체가 '죽었다'는 것은 어떻게 알아낼 수 있을까? 여기서 '죽었다'라는 말은 '해당 객체에 대해서 참조가 존재하지 않는다, 객체가 더 이상 사용되지 않는다, 따라서 할당한 메모리는 회수해도 된다'와 같다.
JVM은 도달 가능성 분석 알고리즘(Reachability analysis algorithm)을 통해 메모리 회수 가능 여부를 판단한다. 핵심 아이디어는 GC root를 시작으로 이들을 참조하는 객체들을 그래프로 그려보았을 때, 즉 참조 체인(reference chain)을 그렸을 때 해당 객체에 도달할 수 있는지이다. 만약 GC root로부터 도달할 길이 없다면 참조가 끊겼으므로 GC의 대상이 된다. 참조가 없다는 것은 이 객체가 더 이상 사용되지 않는다는 것과 동일하다. 반대의 경우에는 참조가 존재하므로 GC의 대상이 되지 않는다.
[참고] GC root가 될 수 있는 객체들의 종류 : https://www.baeldung.com/java-gc-roots

JVM의 Heap memory 할당 전략
GC를 효율적으로 진행하기 위해서는 어떻게 해야 할까? Heap 영역을 전부 훑어야 한다면 GC에 드는 시간이 너무나도 커질 것이고 비효율적이게 된다. 이에 JVM은 아래 두 가지 가정에 의하여 Heap을 크게 두 영역, Young generation과 Old generation 영역으로 나누고 객체들의 나이에 따라 할당 위치를 결정한다. Young generation은 내부적으로 Eden space와 Survivor space 2개로 나뉘어 관리된다.
첫 번째, 대다수 객체는 일찍 죽는다. (통계적으로 GC가 한 번 발생했을 때 Young generation의 98%가 회수된다.)
두 번째, 객체가 GC에서 살아남은 횟수가 늘어날수록 더 오래 살 가능성이 커진다.
주의 : 객체 할당 규칙은 고정된 것이 아니며, 현재 사용하는 가비지 컬렉터와 메모리 관련 가상 머신 매개 변수 설정값에 따라 달라진다.

1. 객체가 처음 할당되는 경우에는 Young generation의 Eden space에 할당된다.
객체가 태어났을 때에 나이는 0이다. Young generation이 꽉 차게 된다면 이 영역만 GC를 진행하는데 이를 Minor GC라고 부르고, 객체가 Minor GC를 겪고 살아남았을 때마다 나이가 1씩 증가한다.
2. Minor GC에서 살아남은 객체들은 Young generation의 Survivor space로 옮겨진다.
3. 객체의 나이가 특정 threshold를 넘었다면 Old generation으로 승격된다.
Old generation도 꽉 차게 된다면 이 영역만 GC를 진행하는 Major GC가 발생한다.
[참고] Heap 영역과 Method 영역 전체를 대상으로 하는 GC도 존재하며 이를 Full GC라고 하는데, 이는 앞서 말한 것처럼 매우 비효율적이며 시간이 많이 소요되는 작업이다. 따라서 Full GC를 피하기 위해 Minor GC를 강제로 수행하는 전략도 존재한다. (G1 collector의 Preventive collection)
4. 거대한 객체는 곧바로 Old generation에 할당된다.
단, 만약 Eden space에 충분한 공간이 존재한다면 Eden space에 할당한다. Eden에 충분한 공간이 존재하지 않으며 기준 크기를 넘는다면 곧바로 Old generation에 할당되는 것이다.
GC Algorithms
GC를 진행하는 방법은 크게 3가지가 존재한다. 각 방법마다 장단점이 존재하기 때문에 무엇 하나가 최고라고 단언할 수는 없다. GC를 수행하는 주체인 Garbage collector들은 이 알고리즘들을 세대별로 또는 상황별로 조합하여 사용한다.
Mark and sweep
1. Mark : GC 대상을 표시(marking) 한다.
2. Sweep : 표시된 놈들을 GC 한다.

장점 1 : 알고리즘이 매우 간단하다.
장점 2 : GC thread와 Application thread가 동시에(concurrently) 일을 할 수 있다.
즉, GC를 위해서 모든 시스템 동작을 멈추는 Stop-the-world가 발생하지 않는다.
단점 1 : 실행 효율이 일정하지 않다. 회수할 객체가 많아질수록 작업의 효율이 떨어진다.
단점 2 : Memory fragmentation, 메모리 단편화가 발생한다. 살아남은 객체들을 다른 곳으로 옮기지 않기 때문이다. Memory fragmentation이 발생한다면 이후 거대한 객체나 배열을 할당할 경우에 연속적인 메모리 공간을 찾지 못하므로 추가적인 매커니즘이 필요하다.
Mark and copy
0. 메모리를 두 영역으로 나누고, 한 번에 한 영역만 사용한다.
1. Mark : GC 대상을 표시한다.
2. Copy : 표시된 놈들을 예약된 공간으로 복사한 후, 사용한 영역을 한 번에 청소한다.

장점 : Memory fragmentation 문제는 해결된다. 연속적인 메모리 영역을 쉽게 찾을 수 있다.
단점 1 : Copy에는 비용이 발생한다. 따라서 객체의 생존율이 높을수록 효율이 떨어진다.
단점 2 : 객체를 복사하기 때문에 GC 발생 시 시스템의 모든 동작을 멈춰야 한다. 즉, Stop-the-world가 발생한다.
단점 3 : 메모리의 절반이나 사용하지 못하는 것은 큰 낭비이다.
해결법 : 예약된 공간은 Survivor space 중 하나만 작게 둔다. Eden space에서 대부분의 객체는 회수되므로 예약된 공간이 작아도 괜찮으며, 만약 이를 넘는 경우 Old generation으로 바로 승격하는 전략을 둘 수 있다.
이 방식은 복사를 사용하기 때문에 대다수 객체가 살아남는 상황에 반드시 대처해야 한다. (예약 공간을 남겨두기 등..) 따라서 객체들이 높은 확률로 살아남는 Old generation 영역에는 적합하지 않은 알고리즘이다.
Mark and compact
1. Mark : GC 대상을 표시한다.
2. Compact : GC 되지 않는 놈들을 한쪽 끝으로 모은 후, 나머지 공간을 한꺼번에 비운다.

장점 1 : Memory fragmentation 문제를 해결한다.
장점 2 : Mark and copy처럼 따로 예약 공간을 남겨두지 않아도 된다. 즉, 메모리를 보다 효율적으로 사용한다.
단점 : Mark and copy의 1, 2번 단점과 동일하다. 메모리 이동이 발생하며 기존 참조들을 모두 갱신해야 한다.
정리
가장 재미있었던 부분은 GC 시 객체를 이동시키는 것이 좋은지 이동시키지 않는 것이 좋은지는 명확히 가를 수 없고 기준에 따라서 더 좋다고 하는 전략이 바뀌는 것이었다. 그만큼 서로의 장단점이 너무 명확하다. 객체를 이동시키는 경우에는 Garbage collector의 효율이 높아지지만, GC 작업이 복잡해지며 Stop-the-world가 필수적이다. 반대로, 객체를 이동시키지 않는 경우에는 Stop-the-world 시간이 없지만 memory fragmentation 때문에 메모리 할당 작업이 복잡해진다. 따라서 CMS collector는 Mark and sweep을 사용하다가 memory fragmentation이 심해지면 Mark and compact를 돌리는 전략을 선택하며, 각 garbage collector가 GC의 throughput 또는 latency 중 어느 것에 집중하느냐에 따라 사용하는 알고리즘이 달라지게 된다.
다음 포스팅은 G1 collector에 대해서, 그리고 이전 포스팅에서 Grafana 모니터링으로 발견하게 된 GC 대량 발생 현상에 대한 분석 결과를 작성하고자 한다.
참고
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html
https://docs.oracle.com/cd/E13150_01/jrockit_jvm/jrockit/geninfo/diagnos/garbage_collect.html
'Java' 카테고리의 다른 글
[JVM] G1 Garbage Collector의 기본 개념과 동작 과정 (+ Serial, ParNew, CMS Collector) (1) | 2025.04.08 |
---|