-
JVM과 GC
- JVM (Java Virtual Machine)
- 자바 애플리케이션을 클래스 로더를 통해 읽어서 자바 API와 함께 실행하는 것
- JAVA와 OS 사이에서 중개자 역할 ~> OS에 구애받지 않고 재사용을 가능하게 함
- 메모리 관리, Garabage collection을 수행
- 스택 기반의 가상머신
- cf) ARM 아키텍처와 같은 하드웨어는 레지스터 기반으로 동작
- 자바 실행 과정
- JVM은 OS로부터 프로그램이 필요로 하는 메모리를 할당받음
- JVM은 이 메모리를 용도에 따라 여러 영역으로 나누어 관리
- 자바 컴파일러(javac)가 소스코드(.java)를 바이트코드(.class)로 변환
- Class Loader를 통해 class 파일들을 JVM으로 로딩
- 로딩된 class 파일들은 Execution engine을 통해서 해석
- 해석된 bite code는 runtime data area에 배치 ~> 수행
- 실행과정 속에서 필요에 따라 Thread Synchronization과 GC와 같은 관리작업 수행
- 구성
- Class Loader(클래스 로더)
- 클래스파일을 로드하고 링크를 통해 배치하는 작업을 수행하는 모듈
- runtime시 동적으로 클래스를 로드
- jar 파일 내 저장된 클래스를 JVM에 탑재 ~> 사용하지 않는 클래스는 메모리에서 삭제
- 클래스를 처음으로 참조할 때, 해당 클래스를 로드하고 링크하는 역할
- Execution Engine(실행 엔진)
- 클래스를 실행하는 역할로 클래스 로더가 JVM내의 런타임 데이터 영역에 바이트코드를 배치하면, 실행 엔진이 실행
- 바이트코드는 기계어가 아닌 사람이 보기 편한 형태로 기술 ~> 실행 엔진이 바이트코드를 실제 JVM이 해석할 수 있는 기계어로 변경
- Interpreter
- 실행 엔진은 바이트 코드를 명령어 단위로 읽어서 실행
- 한줄씩 실행하는 인터프리터 언어의 단점을 가지고 있음
- JIT(Just In Time)
- 인터프리터 방식의 단점을 보완하기 위해 도입된 JIT 컴파일러
- 인터프리터 방식으로 실행하다가 적절한 시점에 바이트 코드 전체를 컴파일 ~> 네이티브 코드로 변경 ~> 네이티브 코드로 직접 실행
- 네이티브 코드는 캐시에 보관 ~> 한 번 컴파일된 코드는 빠르게 수행
- 컴파일 과정은 인터프리팅보다 오래걸리기 때문에, JVM은 내부적으로 해당 메소드가 얼마나 자주 수행되는지 체크해서 일정 수준을 넘을 때만 컴파일 수행
- GC(Garbage Collector / Garbage Collection)
- Runtime Data Area
- 프로그램을 수행하기 위해 OS에서 할당받은 메모리 공간
- runtime data area는 저장 목적에 따라서 5개로 나뉨
- Method Area( == Class Area == Static Area)
- 모든 Thread에 공유. 클래스, 변수, Method, static 변수, 상수 정보 등 저장
- Heap Area
- 모든 Thread에 공유. new 명령으로 생성된 인스턴스와 객체가 저장
- 힙 공간은 세개의 영역으로 다시 분할
- New/Young 영역
- 객체들이 최초로 생성되는 공간과 이 객체에서 참조되는 객체들이 저장되는 공간
- Old 영역
- New area에서 일정 시간 참조하고 살아남은 객체들이 저장되는 공간
- New area 영역에 객체가 가득차면 GC가 발생
- Permanent Generation
- 생성된 객체 정보의 주소값이 저장된 공간
- Class loader에 의해 로드되는 데이터에 대한 meta 정보가 저장, JVM이 사용
- Reflection을 동적으로 클래스가 로딩되는 경우에 사용되는데, 내부적으로 Reflection 기능이 자주 사용되는 스프링 프레임워크를 이용할 때, 이 영역에 대한 고려가 필요함
- 공간이 부족해지면 GC 실행
- Stack Area
- 각 스레드마다 하나씩 생성됨
- Method 내부에서 사용되는 값(지역변수, 매개변수, 리턴값 등)이 저장
- PC Register
- 각 스레드마다 하나씩 생성됨
- 현재 수행중인 JVM 명령 주소값 저장 (CPU의 Register와 비슷한 역할)
- Native Method Stack
- 각 스레드마다 하나씩 생성됨
- OS의 고유 기능(Native)를 Java가 아닌 OS가 구현된 언어(보통 C, C++)로 OS의 고유 기능을 형성
- 해당 언어의 메소드 호출을 위해 할당되는 구역, 언어에 맞게 stack 형성
- JNI(Java Native Interface) 표준 규약 제공
- GC(Garvage Collection / Garvage Collector)
- Young 영역 GC
- Minor GC
- 새로 생성된 객체는 Eden 영역에 위치
- Eden에서 GC가 실행된 이후 살아남은 객체는 Survivor 영역 중 하나로 이동
- survivor에서 계속 살아있는 객체를 old 영역으로 이동
- Major GC
- old 영역에 있는 모든 객체들을 검사 ~> 참조되지 않은 객체를 한꺼번에 삭제
- 시간이 오래걸리고 실행 중 프로세스가 정지됨, 이를 stop-the-world라고 한다
- Major GC가 발생 ~> 실행하는 스레드를 제외한 나머지 스레드는 모두 작업을 멈추고, GC 작업을 완료한 이후에 작업 다시 시작
- Old 영역 GC
- Serial GC
- 적은 메모리와 CPU 코어 개수가 적을 때 적합한 방식
- Old 영역에 살아있는 객체 식별 후, 힙의 앞부분부터 살아있는 것만 남기고, 각 객체들이 연속되게 쌓이도록 힙의 앞부분부터 채워서 객체가 존재하는 부분과 객체가 없는 부분으로 나눈다.
- Parallel GC
- Serial GC와 기본적인 알고리즘은 같지만(Mark-Sweep-Compaction), 멀티 쓰레드로 GC를 처리
- 따라서, Serial GC보다 빠르게 처리 가능
- 메모리가 충분하고 코어의 개수가 많을 때 적합한 방식
- Parallel Old GC
- JDK 5 update 6부터 제공
- 개선된 알고리즘(Mark-Summary-Compaction) 사용
- Summary 단계에서 이미 GC가 수행된 영역에서 살아있는 객체를 식별하는 작업을 한다는 것이 Sweep과 차이점
- Old GC 처리량을 늘려주기 위한 작업
- Concurrent GC
- 프로세서가 GC와 처리 역할을 나누어 작업 ~> 일시 정지가 짧아짐
- 프로세서의 수를 늘릴수록 효과 O, but 한계가 존재
- 전체 처리량보다 응답 시간이 더 중요한 경우 사용
- 소멸 대상 선정 방식
- 알고리즘에 따라 다양하지만, 공통점이 존재
- 힙 영역 안의 객체 중에서 가비지를 찾고 처리해서 메모리를 회수한다.
- 참조되고 있지 않은 객체를 가비지라 하고, reachability라는 개념을 활용함
- 힙 영역에 할당된 객체가 유효한 참조가 있으면 reachability, 없으면 unreachability
- 참조 사슬 중 최초 참조한 것을 Root Set이라고 칭함
- 힙 내의 다른 객체에 의한 참조를 제외한 참조가 Root Set이 된다.
- Java 메서드 실행 시 사용되는 지역변수와 파라미터들에 의한 참조
- JNI에 의해 생성된 객체에 대한 참조
- 메서드 영역의 정적 변수에 의한 참조