JVM & GC
Answer
이번 주 차시는 2년 전인 2022년 8월 과거의 내가 작성했던 글, 저번달에 작성한 글과 관련이 있다
https://code-boki.tistory.com/80
https://code-boki.tistory.com/208
JVM은 크게 4개의 영역으로 나뉘는 구조를 갖고 있다
- 클래스 로더 (Class Loader)
- 실행 엔진 (Execution Engine)
- 런타임 데이터 영역 (Runtime Data Areas) [ 그림에서 JVM memory부분 ]
- 네이티브 메서드 인터페이스 (Native Method Interface)
클래스 로더는 JVM이 Java 프로그램에서 사용되는 클래스 파일을 찾아 로드하는 역할을 한다
종류: 클래스 로더는 기본적으로 3가지 종류로 나뉨
- 부트스트랩 클래스 로더 (Bootstrap Class Loader): 가장 기본적인 클래스를 로드(ex: rt.jar)
- 확장 클래스 로더 (Extension Class Loader): 표준 핵심 Java 클래스의 확장을 로드(ex: jre/lib/ext)
- 애플리케이션 클래스 로더 (Application Class Loader): 클래스패스에 지정된 애플리케이션 레벨의 클래스를 로드(ex: classpath)
모델: 클래스 로더는 Parent Delegation Model을 따른다
=> 어떤 클래스가 로드되기 전에 먼저 부모 클래스 로더에게 해당 클래스를 로드할 수 있는지 확인
=> 동일한 클래스가 여러번 로드되는 문제를 방지
위임과정: Application ClassLoader -> Extension ClassLoader -> Bootstrap ClassLoader
위임가능한지 확인이 안되면 역순으로 탐색
클래스 로더의 로딩 과정
1. Loading(로딩): 클래스 파일을 찾아 메모리에 로드
2. Linking(링킹): 검증(Verify), 준비(Prepare), 분석/해결(Resolve) 단계를 거침
3. Initialization(초기화): 클래스 변수를 초기화하고 정적 블록을 실행
JVM 메모리 구조는 크게 다섯 가지 영역으로 나눌 수 있다
1. Method Area(메서드 영역)
모든 스레드가 공유하는 영역
클래스 메타데이터, 메서드 데이터, 정적 변수, 상수, 생성자 코드 등이 저장됨
JVM당 하나만 생성됨
Java8이전: Permanent Generation(PermGen) / Java8이후: Metasapce 영역이라고 정정
Metaspace는 네이티브 메모리를 사용하므로, PermGen과 달리 자동으로 크기가 조절됨
2. Heap(힙 영역)
모든 스레드가 공유하는 영역
객체와 배열이 저장됨
가비지 컬렉션의 대상이 되는 영역
내부적으로 (Young / Old) Generation 영역이 존재
(Young Generation: 새로 생성된 객체가 저장되고, Old Generation: 오랫동안 살아남은 객체가 저장됨)
Generation을 Gen이라고 보통 줄여서 표현하며, Young Gen은 세부적으로 Eden과 두 개의 Survivor 공간으로 나뉨
3. Stack(스택 영역)
각 스레드마다 별도의 공간을 가짐(스레드 1개당 1개)
메서드 호출 시마다 프레임이 추가되고, 메서드가 종료되면 프레임이 제거됨
각 프레임은 지역 변수, 매개변수, 중산 연산 결과, 리턴 값 등을 저장
이 영역에서 java.lang.StackOverflowError 에러가 발생
4. PC Register (PC 레지스터)
각 스레드마다 별도의 공간을 가짐(스레드 1개당 1개)
현재 실행 중인 명령의 주소를 저장(JVM 명령의 주소를 가리키는 포인터 역할)
5. 네이티브 메서드 스택 (Native Method Stack)
각 스레드마다 별도의 공간을 가짐(스레드 1개당 1개)
JNI(Java Native Interface)를 통해 호출되는 C/C++ 등의 네이티브 코드를 위한 스택
객체의 수명에 따라 효율적인 메모리 관리를 위해 분할
Young Generation에서 단명 객체를 빠르게 처리하고, Old Generation에서 장수 객체를 관리
자동으로 사용하지 않는 메모리를 회수하는 JVM의 메커니즘
개발자가 명시적으로 메모리를 해제할 필요 없이 JVM이 자동으로 처리
이 GC로 인해 자바는 Managed Language(관리형 언어)라고 불림
- 장점
메모리 관리 자동화로 개발 생산성 향상
메모리 누수 방지
- 단점
GC 작동 시 애플리케이션 일시 정지
성능 오버헤드 발생 가능성
< 대분류 >
- 참조 횟수 카운팅 GC (Reference Counting Garbage Collection): PHP, Swift
- 추적 기반 GC (Tracing Garbage Collection): Java, Kotlin, Python
< 추적 기반 GC >
- Mark-Sweep Algorithm
- Mark-Sweep-Compact Algorithm
- Tri-color Marking Algorithm (Incremental GC)
- Copying Algorithm (Incremental GC)
- Generational Algorithm (Incremental GC)
Java 8의 GC방식은 Parallel GC 또는 Throughput GC라고 불리며 다수의 스레드(멀티 스레드)를 이용해서 GC의 속도가 향상됨
Young Generation에서 Minor GC가 수행되고 Old Generation에서 Major GC가 수행됨
Young Gen에서 동작하는 GC는 PS Scavenge
Old Gen에서 동작하는 GC는 PS MarkSweep
1. Serial GC
하나의 스레드로 GC를 실행하는 방식이며 STW(Stop-The-World) 시간이 길고, 메모리 파편화를 막기 위해 Compaction(메모리 압축)을 수행
2. Parallel/Throughput GC
여러개의 스레드로 GC를 실행하는 방식이며 STW가 Serial GC보다 상대적으로 짧음. 역시 메모리 파편화를 막기 위해 Compaction을 수행
3. CMS(Concurrent-Mark-Sweep) GC
Concurrent. 즉 동시에란 뜻!
가비지 수집 작업을 애플리케이션 스레드와 동시에 수행하기때문에 STW 시간이 감소
하지만 Compaction을 기본적으로 수행하지 않기때문에 시간이 지나면 메모리 파편화가 발생할 수 있음
동시 작업때문에 CPU를 더 많이 소모
자바9에서 deprecated되었으며 자바14에서 제거됨
4. G1(Garbage-First) GC
자바9+ 버전의 디폴트 GC
CMS처럼 Heap을 스캔하고 삭제하기 위해 여러개의 Background GC 스레드를 사용
incremental과 concurrent 알고리즘을 사용함과 동시에 긴 멈춤 시간을 방지하고, GC 시간을 예측 가능하고 짧게 유지하는 것이 목표
힙 영역을 동일한 크기의 Region이라 불리는 바둑판과 같은 모양의 논리적인 단위로 쪼개서 이 영역이 가비지로 채워진다면(G1 GC가 런타임에 이 영역을 튜닝), 이 공간을 먼저 회수하는데 집중 => G1이라 불리는 이유
5. ZGC
G1과 비슷하게 ZGC는 내부적으로 ZPage라는 영역을 사용하며, G1의 Region은 크기가 고정인데 비해서 ZPage는 가변
Reference Coloring, Relocation, Load Barriers, Remapping, Reclaimed 등의 기법을 사용
대량의 메모리를 낮은 지연으로 잘 처리하기 위해 고안됨
ZGC의 목적 중 하나는 STW 상태를 10ms 아래로 가져가는 것
자바15버전에서 release
=> 그럼 왜 성능이 좋은 ZGC를 기본 GC로 하지 않고 G1을 기본 GC로 채택한 것일까?
차이점
1) 용도 - G1: 범용 GC / ZGC: 대규모 힙 GC
2) 자원 사용 - G1: 비교적 효율적으로 사용 / ZGC: 짧은 STW를 유지하기 위해 Load Barriers와 같은 고급기술을 사용하여 CPU 사용량 증가
ZGC는 실시간 처리 시스템이나 대규모 메모리를 사용하는 애플리케이션에 유리
모든 애플리케이션이 다 고성능이어야 하는 것은 아님. 그리고 AWS 비용 생각하면......ㅇㅋ?
- 동작 순서
1. Heap 영역에 존재하는 객체들에 대해 접근 가능한지 확인
2. GC Root에서부터 시작하여 참조값을 따라가며 접근 가능한 객체들에 대해 Mark
3. Unreachable(접근할 수 없는) 객체는 제거(Sweep) 대상이 되고, 해당 객체들을 제거
+ 객체는 Reachable / Unreachable로 구분 가능
Reachable 객체들은 java.lang.ref 패키지를 이용해서 Strongly, Softly, Weakly, Phantomly로 더 자세히 구분되어 각 GC 마다 동작을 다르게 지정할 수 있다
GC 발생 영역 구분: Minor GC(Young Generation)와 Major GC(Old Generation)
Minor GC는 자주 발생하며 빠르고, Major GC는 덜 빈번하지만 더 오래 걸림
<Young Gen>
새로 생성된 객체: Eden -> S0또는 S1로 이동
Eden 영역이 가득 차면 Minor GC가 발생(Minor GC마다 S0, S1는 역할을 교대하며 생존할 때마다 Age 카운터 증가)
Age가 기본값인 15에 도달하면 Old Gen으로 이동
<Old Gen>
Minor GC를 여러번 통과한 객체 또는 장수객체가 존재하는 영역
Old Gen을 Tenured라고도 부름
이 영역이 가득차면 Major GC(Full GC)가 발생
default size) Young Gen : Old Gen = 1 : 3
단명 객체와 장수 객체의 차이는 객체의 수명(참조되는 기간) 차이
- 단명 객체(Short-lived Object): 메서드 호출 내부의 지역변수(non primitive type), 반복문에서 매번 생성되는 문자열 객체 등
- 장수 객체(Long-lived Object): 애플리케이션 전체에서 사용하는 전역변수, 캐시된 데이터 등
Java 8: Parallel GC / Java 11: G1 GC
코드로도 한번 확인해봤다!^^
G1(Garbage-First) GC
자바9+ 버전의 디폴트 GC
CMS처럼 Heap을 스캔하고 삭제하기 위해 여러개의 Background GC 스레드를 사용
incremental과 concurrent 알고리즘을 사용함과 동시에 긴 멈춤 시간을 방지하고, GC 시간을 예측 가능하고 짧게 유지하는 것이 목표
힙 영역을 동일한 크기의 Region이라 불리는 바둑판과 같은 모양의 논리적인 단위로 쪼개서 이 영역이 가비지로 채워진다면(G1 GC가 런타임에 이 영역을 튜닝), 이 공간을 먼저 회수하는데 집중 => G1이라 불리는 이유
전체 Heap을 동일한 크기의 여러 Region으로 분할
각 Region은 Eden, Survivor, Old, Humongous, Available/Unused로 구성되며 런타임 중 Available or Unused 영역에 동적 할당
(필요에 따라 Available/Unused Region이 메모리 할당 상황에 맞춰 동적으로 변경되는 방식)
Humongous 영역은 Region 크기의 50% 이상인 큰 객체를 저장
몇가지 특징과 장점들 때문에 디폴트 GC로 채택한것이 아닐까 싶음
1. 낮은 STW Time: G1 GC는 다른 GC 방식에 비해 STW 시간이 짧음
2. 효율적인 메모리 관리: Heap을 고정된 크기의 Region으로 나누어 관리하기때문에 메모리 파편화 문제를 최소화함
3. 우수한 성능: 대용량 메모리(4GB 이상)에서 우수한 성능을 가지며, 예측 가능한 응답 시간을 유지할 수 있음
애플리케이션의 성능과 안정성을 유지하기 위해 GC 모니터링은 중요
불필요한 Full GC가 발생해서 STW가 일어나면 애플리케이션이 긴 시간 멈출 수도 있음
또한 지나치게 잦은 Minor GC가 발생하면 애플리케이션의 응답 시간이 느려질 수도 있음
메모리 누수가 발생하면 GC가 메모리를 해제하지 못하고, OOM(OutOfMemoryError)이 발생할 수 있음
1. OOM이 발생한 경우 힙덤프 파일을 생성하여 분석
( -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump )
2. 실시간 스냅샷 생성, jmap 명령어로 힙덤프 파일 생성
( jmap -dump:format=b,file=testdump.hprof $(jps | grep <현재 실행중인 자바 프로그램 이름> | awk '{print $1}' )
OOM이 발생하지 않도록 GC로그 분석, 최대 힙 크기 조정
VisualVM, JProfiler, YourKit, Eclipse MAT 같은 프로파일링 도구를 사용하여 애플리케이션의 메모리 사용 현황을 실시간으로 모니터링하고, 객체가 계속해서 메모리에 남아있는 경우 메모리 누수를 의심
불변객체 사용, 자원반납 확인, 충분한 메모리 확보
출처
https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html
https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html
https://www.geeksforgeeks.org/jvm-works-jvm-architecture/
https://woooongs.tistory.com/51
https://siahn95.tistory.com/102?category=866017
https://siahn95.tistory.com/108?category=866017
https://incheol-jung.gitbook.io/docs/q-and-a/java/heap-dump-feat.-oom
https://stackoverflow.com/questions/39929758/ps-marksweep-is-which-garbage-collector
https://d2.naver.com/helloworld/0128759
https://d2.naver.com/helloworld/329631
https://www.baeldung.com/java-choosing-gc-algorithm
'Course > JSCODE - 자바' 카테고리의 다른 글
JSCODE 스터디 후기(자바 1기) (10) | 2024.09.17 |
---|---|
4주차 스터디 노트 (1) | 2024.09.05 |
3주차 스터디 노트 (2) | 2024.08.29 |
댓글