7. JVM 과 GC 그리고 객체
Last updated
Last updated
대다수 객체는 일찍 사라짐, 참조가 사용되지 않음. (약한 세대 가설)
Eden 영역에서 대부분 사라질 것이다!
GC 과정에서 살아남은 회수가 늘어날 수록 (앞으로도) 생존 가능성이 높아짐. (강한 세대 가설)
다른 세대에 속한 객체간 참조는 같은 세대에 속한 객체간 참조보다 훨씬 적음 (세대 간 참조 가설)
Hotspot VM 에서 에덴과 생존자 공간비율은 보통 8:1
IBM 연구에 따름 (보통 첫 GC 에 대략 98% 객체가 소멸)
파편화 방지를 위해 활용 Survivor space 사용
Eden + Survivor(S0, S1) 공간으로 구성됨
새로 생성된 객체 대부분은 Eden 영역에 먼저 할당
Minor GC가 발생하면 Eden + 하나의 Survivor를 청소하고, 살아남은 객체는 다른 Survivor 또는 Old Generation으로 "승격(Promotion)" 됨
▸ Eden
객체가 최초로 할당되는 공간
GC가 자주 일어나고, 객체 생명주기가 짧음
▸ Survivor space (S0, S1)
GC 시 복사-이동 대상이 되는 공간
S0와 S1은 번갈아가며 사용됨 (from-space, to-space)
여러 번 살아남은 객체는 Old Generation으로 승격
age bit 사용 (참조계수) -> 클래스 메타데이터에 포함
장기 생존 객체가 저장되는 공간
Minor GC에선 건드리지 않으며, Major GC나 Full GC 시에 청소됨
Full GC는 이 영역이 꽉 찼거나, Promotion 대상이 들어갈 공간이 없을 때 발생
📆 1960년대 초 John McCarthy 제안 (LISP GC의 기초)
🔧 작동 방식
Mark 단계
GC Root(스택, static 변수 등)에서 도달 가능한 객체를 표시(mark)
이때 방문한 객체에 "mark bit"를 세팅함
Sweep 단계
힙 전체를 선형 탐색하며, mark되지 않은(dead) 객체를 회수(free)
단, 메모리 정리는 안 함 → 단편화 발생
⚠️ 단점
파편화 발생: 객체가 삭제돼도 자리는 남아있어 조각난 공간이 생김
연속 공간 확보 어려움 → 큰 객체 할당 실패
STW 시간 길어짐: 전체 Heap을 선형으로 탐색하기 때문
🧠 JVM 적용
현대 JVM에서는 단독으로 사용되지 않음
하지만 기본 개념이 CMS, G1, ZGC 등 거의 모든 GC에 내재
📆 1974년 Robert Fenichel & Peter Bishop 제안 → Copying Collector라고도 불림
🔧 작동 방식
Heap을 두 영역으로 나눔 (From / To Space)
객체는 항상 From 영역에 생성
GC 시:
살아있는 객체만 To 영역에 복사
참조 포인터를 모두 새 객체로 갱신
GC 후, From ↔ To 역할을 스왑
⚠️ 단점
메모리 낭비: 절반은 항상 비어 있어야 → 유효 메모리는 전체 Heap의 50% 미만
복사 비용 존재: 살아있는 객체를 이동해야 함
📌 JVM 적용
Young 영역 (Eden + Survivor)에서 사용
Survivor 영역이 2개 (S0, S1): 교대로 사용되며, Eden:S0:S1 비율은 기본 8:1:1
객체 생존율이 낮은 Young 영역에 최적화된 방식
📆 1970년대 중반 Edward L. Ruders 등 제안 → Mark-Sweep의 파편화 문제 + Copy의 메모리 낭비를 함께 해결
🔧 작동 방식
Mark 단계
도달 가능한 객체에 mark bit 설정
Compact 단계
힙 앞쪽으로 객체들을 밀어넣으며 정렬 압축
이후 공간은 free로 남김 → 연속 공간 확보
⚠️ 단점
객체가 많을수록 이동 비용 커짐
포인터 갱신 비용: 객체 주소가 바뀌므로 참조하는 곳도 모두 변경
STW가 발생함 (전체 압축 동안 애플리케이션 멈춤)
📌 JVM 적용
주로 Old 영역에서 사용
대표적으로 Parallel Old GC, G1 GC의 Old Region, Serial Old GC에서 사용됨
도달 가능성 분석(Reachability analysis) 알고리즘에 기초해 회수 대상 인스턴스를 판단
GC 루트 객체들을 기점으로 참조하는 다른 객체들을 탐색하는 방식
힙 메모리가 크다보면 Full GC 가 났을 때 도달 가능성 분석 시간이 너무 오래 걸릴 수 있기 때문에, 힙 메모리를 늘리는 것이 좋은가? 는 한번 더 생각해 볼 필요가 있다.
JVM stack frame 지역변수 테이블에서 참조하는 객체 (현재 실행 중인 메서드)
Synchronized
키워드로 잠겨 있는(모니터 락) 객체
JNI 가 참조하는 객체
메서드 영역에서 클래스 정적 필드로 참조하는 객체
메서드 영역에서 상수로 참조되는 객체
JVM 내부에서 사용되는 참조
Stop-The-World(STW) ?
Stop-The-World(STW)는 GC가 실행될 때 모든 애플리케이션 스레드를 멈추는 순간입니다.
왜 멈추냐면? 👉 GC는 메모리 객체를 스캔하면서 참조 그래프를 따라 이동하는데, 그 사이 애플리케이션이 객체를 수정하면 무결성이 깨지기 때문입니다.
Serial
STW + 단일 스레드
❌
단순성, 저자원
유지
ParNew
STW + Young 병렬
✅
CMS 보조용
CMS 제거와 함께 사라지는 중
Parallel (Scavenge)
STW + Young/Old 병렬
✅
Throughput 극대화
유지
CMS
Old 영역 병행 수집
✅ (일부 STW)
정지시간 최소화
제거됨 (Java 14)
G1
Region 기반 수집
✅
Predictable pause time
✅ 기본 GC
Java에서 가장 기본적인 GC 방식입니다.
Young과 Old 영역 모두 단일 스레드로 GC 작업을 수행합니다.
GC가 실행되면 전체 애플리케이션이 Stop-The-World(STW) 상태에 진입합니다.
Young 영역은 Mark and Copy, Old 영역은 Mark and Compact 알고리즘을 사용합니다.
구현이 매우 단순하고 리소스 사용량이 적기 때문에, 임베디드 환경이나 테스트용 소형 애플리케이션에 적합합니다.
병렬/병행 처리가 불가능하므로, Heap 크기가 커질수록 GC 정지 시간이 급격히 증가합니다.
🧱 특징:
구조가 단순하여 이해와 디버깅이 쉬움
작은 Heap에서 효율적
STW 시간이 비교적 길어질 수 있음
병렬 GC 기능이 없어 서버 환경에는 적합하지 않음
Serial GC의 Young 영역 수집을 멀티스레드화한 GC입니다.
Young 영역만 병렬 수집하며, Old 영역은 CMS 또는 Serial Old와 함께 처리됩니다.
단독 사용보다는 CMS GC의 Young 영역 수집기로 주로 사용됩니다.
GC 중 일부 STW가 발생하지만, Serial GC보다는 정지 시간이 짧아집니다.
CMS와 조합 시 병렬 Young 수집 + 병행 Old 수집 구조로 활용됩니다.
🧱 특징:
Young 영역에서 병렬 수집을 지원
Old 영역은 여전히 CMS나 Serial Old에 의존
CMS 기반 시스템에서 Young GC 성능을 향상시킴
완전한 병렬 GC는 아님 (Old 영역은 병렬화되지 않음)
전체 처리량(Throughput)을 극대화하기 위해 설계된 GC입니다.
Young 영역과 Old 영역 모두를 멀티스레드로 병렬 수집합니다.
Young 영역은 Parallel Scavenge, Old 영역은 Parallel Old 알고리즘을 사용합니다.
GC 중에는 STW가 발생하지만, 작업을 병렬로 수행하므로 총 시간은 짧아질 수 있습니다.
Throughput이 중요한 배치 처리, 대용량 연산 중심의 서버 환경에서 적합합니다.
🧱 특징:
Young + Old 영역 모두 병렬 처리 가능
Throughput 중심 애플리케이션에 최적화
GC 시간 자체는 길 수 있으나, 애플리케이션 전체 처리 효율은 높음
지연시간이 중요한 서비스에는 적합하지 않을 수 있음
GC로 인한 정지 시간(STW)을 최소화하기 위해 도입된 GC입니다.
Old 영역의 수집을 병행(Concurrent)으로 수행하며, 일부 단계에서만 STW가 발생합니다.
수집 단계는 Initial Mark(STW) → Concurrent Mark → Remark(STW) → Concurrent Sweep 순으로 진행됩니다.
Old 영역에서는 Compaction을 수행하지 않기 때문에 메모리 단편화가 발생할 수 있습니다.
이로 인해 할당 실패(Allocation Failure)나 OOM 가능성이 존재합니다.
Java 9에서 deprecated, Java 14에서 완전히 제거되었습니다.
🧱 특징:
STW 시간을 줄이기 위한 병행 수집 방식
메모리 단편화 발생 가능성
실시간 응답이 중요한 시스템에 한때 많이 사용되었음
G1 GC의 등장으로 대체됨
CMS의 단점을 보완하기 위해 Java 7에 도입되었으며, Java 9부터 기본 GC로 채택되었습니다.
CMS 는 compaction 을 수행하지 않아서, 메모리 단편화로 인해 OOM 이 발생할 수 있다.
CMS는 대부분 병행으로 실행되지만, Initial Mark, Remark 단계는 여전히 STW, 이로인해 GC 발생시기나, STW 길이가 예측 불가하다.
전체 Heap을 고정 크기의 Region 단위로 나누어 관리합니다.
각 Region은 GC 시점마다 Eden, Survivor, Old 등의 역할이 동적으로 할당됩니다.
Young/Old 영역 구분 없이 가비지가 많은 Region을 우선적으로 수집합니다.
GC 정지 시간을 예측하기 위해 -XX:MaxGCPauseMillis 옵션으로 목표 시간을 설정할 수 있으며, 내부적으로 작업을 분산(GC Scheduling)하여 정지 시간을 예측 가능하게 조절합니다.
대용량 Heap에서도 파편화 없이 연속 공간을 확보할 수 있습니다.
🧱 특징:
4GB 이상 대용량 Heap에 적합
Region 기반 관리로 단편화 방지
Predictable pause time 설정 가능
CMS의 단점을 대부분 보완한 현대적 GC 방식
Mark word
Hash code
Hash code 는 Object.hashCode()
함수가 호출되는 시점에 계산
Hash code 는 연산에 비용이 들어가기 때문에, 실제 함수가 호출될 때만 계산한다.
Hash code 는 어떤 인스턴스에 대해서 Unique 하게 식별할 수 있는 값을 의미한다.
Native 언어는 주소가 있기 때문에, 해당 값이 필요하지 않을 수 있다..
하지만 Java 는 JVM 이 주소를 관리하기 때문에, 개발자 입장에서 Unique 하게 식별할 수 있는 방법이 필요하다.
Object age
GC 에서 살아남은 횟수 (비트단위 데이터)
Lock flag
Lock flag 를 중심으로 멀티스레드 환경에서 경쟁조건이 발생하는 문제를 해결하기 위한 것
모니터 락
Klass work
Class
예약어가 있기에 Klass 로 표현
Class meta data(Metaspace 영역) 의 레퍼런스
length(Array)
배열임을 나타내는 정보
Hash(함수) 의 특징
f(x) -> y 만 가능한 단방향 함수
단방향성 (이 특정을 사용해서 DB 에 메모리를 저장할 때 해시값을 저장)
입력 값의 크기와 상관 없이 결과 값의 길이(혹은 크기) 가 일정 (Unique)
데이터 무결성 확보와 관련해 IT 기술 전반(특히 보안)에서 사용된다.
대표적 해시 알고리즘
MD-5, SHA-1
SHA-128, 256, 384, 512