자바 성능 튜닝 이야기 7주차
자바 성능 튜닝 이야기 7주차
들어가며
이 포스트는 이상민의 「자바 성능 튜닝 이야기」 Chpater12 ~ 14을 읽고 개인적으로 학습한 내용을 정리한 글입니다.
- 책: 자바 성능 튜닝 이야기
- 저자: 이상민
- 출판사: 인사이트
- 챕터: Chapter16 ~ Chapter 17
핵심 정리 내용
16장 JVM은 도대체 어떻게 구동될까?
HotSpot VM은 어떻게 구성되어 있을까?
- HotSpot VM
- Just In Time(JIT) 컴파일러의 이름
- JIT을 사용하면, ‘언제나 자바 메서드가 호출되면 바이트 코드를 컴파일하고 실행 가능한 네이티브 코드로 변환
- JIT 컴파일러는 프로그램의 성능에 영향을 주는 지점을 지속적으로 분석해 부하를 최소화하고, 높은 성능을 내기 위한 최적화 대상으로 분석 지점을 선정
- 높은 성능과 확장성 제공
- Just In Time(JIT) 컴파일러의 이름
- HotSpot VM의 주요 컴포넌트
- VM(Virtual Machine) 런타임
- JIT(Just In Time) 컴파일러
- 메모리 관리자
- HotSpot VM 런타임 방식에 GC 방식, JIT 컴파일러를 끼워 맞춰 사용할 수 있다
- VM 런타임은 JIT 컴파일러용 API, GC API를 제공, JVM 을 시작하는 런처와 스레드 관리, JNI 등도 VM 런타임에게 제공
JIT Optimizer라는 게 도대체 뭘까?
- 자바는 javac 컴파일러 사용 > 소슼도르르 바이트 코드로 된 class 파일로 변환
- JVM은 항상 바이트 코드로 시작해, 동적으로 기계에 의존적인 코드로 변환
- JIT는 애플리케이션에서 각각의 메서드를 컴파일할 만큼 시간적 여유가 많지 않아, 모든 코드는 초기에 인터프리터에 의해 시작되고, 해당 코드가 충분히 많이 사용될 경우 컴파일 대상으로 지정
- HotSpot VM에서 이 작업은 각 메서드에 있는 카운터를 통해서 통제
- HotSpot VM이 메서드에 대한 카운터
- 수행 카운터(invocation counter) : 메서드를 시작할 때마다 증가
- 백에지 카운터(backedge counter) : 높은 바이트 코드 인덱스에서 낮은 인덱스로 컨트롤 흐름이 변경될 때마다 증가
- 백에지 카운터는 메서드가 루프가 존재하는지를 확인할 때 사용, 수행 카운터보다 컴파일 우선순위가 높음
- 이 카운터들은 인터프리터에 의해 증가할 때마다, 그 값들이 한계치에 도달했는지를 확인하고, 도달했을 경우 인터프리터는 컴파일을 요청
- 한계치(CompileThreshold) -> 백에지 카운터의 한계치 공식 : CompileThreshold * OnStackReplacePercentage / 100
- 수행 카운터 & 백에지 카운터 시작 옵션 설정
XX:CompileThreshold=35000 XX:OnStackReplacePercentage=80
- 컴파일이 요청되면 컴파일 대상 목록의 큐에 쌓이고, 하나 이상의 컴파일러 스레드가 이 큐를 모니터링
- 컴파일러 스레드가 바쁘지 않으면 큐에서 대상을 빼내서 컴파일을 시작
- 보통 인터프리터는 컴파일이 종료되기 기다리지 않는 대신, 수행 카운터를 리셋하고 인터프리터에서 메서드를 계속 수행
- 컴파일 종료 시 컴파일된 코드롸 메서드가 연결되어 그 이후 메서드가 호출되면 컴파일된 코드를 사용
- HotSpot VM
- OSR(On Stack Replacement) 특별한 컴파일
- 인터프리터에서 수행한 코드 중 오랫동안 루프가 지속되는 경우에 사용
- 해당 코드의 컴파일이 완료된 상태에서 최적화되지 않은 코드가 수행되고 있는 것을 발견한 경우 인터프리터에 계속 머무르지 않고 컴파일된 코드로 변경
- OSR(On Stack Replacement) 특별한 컴파일
JRockit의 JIT 컴파일 및 최적화 절차
- JRockit의 JIT 컴파일 및 최적화 절차
- JRockit runs JIT compilation
- 자바 애플리케이션을 실행하면 1단계인 JIT 컴파일을 거친 후 실행
- 이 단계를 거친 후 메서드가 수행되면, 그 다음부터 컴파일된 코드를 호출해 처리 성능이 빨라짐
- 애플리케이션이 시작하는 동안 몇천 개의 새로운 메서드가 수행되어 다른 JVM보다 JRockit JVM이 느려질 수 있음
- JIT를 사용하면 시작할 때의 성능은 느리지만, 지속적으로 수행할 때 더 빠른 처리가 가능
- JRockit monitors threads
- JRockit에는 ‘sampler thread’라는 스레드가 존재하며 주기적으로 애플리케이션의 스레드를 점검
- sampler thread는 어떤 스레드가 동작 중인지 여부와 수행 내력을 관리
- 이를 통해 어떤 메서드가 많이 사용되는지 확인해 최적화 대상을 찾음
- JRockit JVM Runs Optimization
- sampler thread가 식별한 대상을 최적화
- 이 작업은 백그라운드에서 진행ㄹ되며 수행중인 애플리케이션에 영향을 주지는 않는다
- JRockit runs JIT compilation
IBM JVM의 JIT 컴파일 및 최적화 절차
- IBM JVM의 JIT 컴파일 방식
- 인라이닝(Inlining)
- 메서드가 단순할 때 적용되는 방식
- 호출된 메서드가 단순할 경우 그 내용이 호출한 메서드의 코드에 포함시킴
- 이 경우 자주 호출되는 메서드의 성능이 향상
- 지역 최적화(Local optimizations)
- 작은 단위의 코드를 분석하고 개선하는 작업을 수행
- 조건 구문 최적화(Control flow Optimizations)
- 메서드 내의 조건 구문을 최적화하고, 효율성을 ㅜ이해 코드의 수행 경로를 변경
- 글로벌 최적화(Global Optimizations)
- 메서드 전체를 최적화하는 방식
- 매우 비싼 방식, 컴파일 시간이 만힝 소요되지만, 성능 개선이 많이 일어날 수 있음
- 네이티브 코드 최적화(Native code generation)
- 아키텍처에 따라 최적화를 다르게 함
- 인라이닝(Inlining)
JVM이 시작할 때의 절차는 이렇다
- java 명령어 줄에 있는 옵션 파싱
- 일부 명령은 자바 실행 프로그램에서 적절한 JIT 컴파일러를 선택하는 등의 작업을 하기 위해 사용하고, 다른 명령들은 HotSpot VM에 전달
- 자바 힙 크기 할당 및 JIT 컴파일러 타입 지정
- 메모리 크기나 JIT 컴파일러 종류가 명시적으로 지정되지 않은 경우 자바 실행 프로그램이 시스템의 상황에 맞게 선정
- CLASSPATH와 LD_LIBRARY_PATH 같은 환경 변수를 지정
- 자바의 Main 클래스가 지정되지 않았으면, jar 파일의 manifest 파일에서 Main 클래스를 확인
- JNI의 표준 API인 JNI_CreateJavaVM를 사용해 새로 생성한 non-primordial이라는 스레드에서 HotSpot VM을 생성
- HotSpot VM이 생성, 초기화된면, Main 클래스가 로딩된 런처에서 main() 메서드의 속성 정보를 읽는다
- CallStaticVoidMethod는 네이티브 인터페이스를 불러 HotSpot VM에 있는 main() 메서드가 수행
- JNI_CreateJavaVM 단계의 절차
- JNI_CreateJavaVM는 동시에 두개의 스레드에서 호출할 수 없고, 오직 하나의 HotSpot VM 인스턴스가 프로세스 내에서 생성될 수 있도록 보장
- HotSpot VM이 정적인 데이터 구조를 생성하기 때문에 다시 초기화는 불가능해, 오직 하나의 HotSpot VM이 프로세스에서 생성될 수 있다
- JNI 버전이 호환성이 있는지 점검하고, GC 로깅을 위한 준비 완료
- OS 모듈들이 초기화 - 랜덤 번호 생성기, PID 할당 등
- 커맨드 라인 변수와 속성들이 JNI_CreateJavaVM 변수에 전달되고, 나중에 사용하기 위해 파싱 후 보관
- 표준 자바 시스템 속성이 초기화됨
- 동기화, 메모리, safepoint 페이지와 같은 모듈들이 초기화
- libzip, libhpi, libjava, libthread와 같은 라이브러리들이 로드
- 시그널 처리기가 초기화 및 설정
- 스레드 라이브러리가 초기화
- output 스트림 로거가 초기화
- JVM을 모니터링하기 위한 에이전트 라이브러리가 설정되어 있으면 초기화 및 시작
- 스레드 처리를 위해 필요한 스레드 상태와 로컬 저장소 초기화
- HotSpot VM이 ‘글로벌 데이터’들이 초기화
- 글로벌 데이터 종류 : event log, OS 동기화, 성능 통계 메모리(perf memory), 메모리 할당자(chunkPool) 등이 있음
- HotSpot VM에서 스레드를 생성할 수 있는 상태가 됨. main 스레드가 생성되고, 현재 OS 스레드에 붙는다. 아직 스레드 목록에 추가되지 않음
- 자바 레벨의 동기화가 초기화 및 활성화됨
- 부트 클래스로더, 코드 캐시, 인터프리터, JIT 컴파일러, JNI, 시스템 dictionary, ‘글로벌 데이터’ 구조의 집합인 universe 등이 초기화
- 스레드 목록에 자바 main 스레드가ㅣ 추가되고, universe의 상태를 점검. HotSpot VM의 중요한 기능을 하는 HotSpot VMThread가 생성. 이 시점에 HotSpot VM의 형재상태를 JVMTI에 전달
- java.lang 패키지에 있는 String, System, Thread, ThreadGroup, Class 클래스와 java.lang의 하위 패키지에 있는 Method, Finalizer 클래스 등이 로딩되고 초기화됨
- HotSpot VM의 시그널 핸들러 스레드가 시작되고, JIT 컴파일러가 초기화되며, HotSpot의 컴파일 브로커 스레드가 시작, HotSpot VM과 관련된 각종 스레드들이 시작
- JNIEnv가 시작되며, HotSpot VM을 시작한 호출자에게 새로운 JNI 요청을 처리할 상황이 되었다고 전달
- JNI_CreateJavaVM는 동시에 두개의 스레드에서 호출할 수 없고, 오직 하나의 HotSpot VM 인스턴스가 프로세스 내에서 생성될 수 있도록 보장
JVM이 종료될 때의 절차는 이렇다
- HotSpot VM의 종료절차 > DestoryJavaVM 메서드 호출 > HotSpot 런처에서 호출
- HotSpot VM이 작동중인 상황에서는 단 하나의 데몬이 아닌 스레드가 수행될 때까지 대기
- java.lang 패키지에 Shutdown 클래스의 shutdown() 메서드가 수행됨. 이 메서드가 수행되면 자바 레벨의 shutdown hook이 수행되고, finalization-on-exit 이라는 값이 true일 경우 자바 객체 finalizer를 수행
- HotSpot VM 레벨이 shutdown hook을 수행함으로써 HotSpot VM의 종료를 준비. JVM_OnExit()메서드를 통해 지정. 그리고 HotSpot VM의 profiler, state sampler, watcher, garbage collector 스레드를 종료시킨다. 이 작업들이 종료되면 JVMTI를 비활성화하며, Signal 스레드를 종료
- HotSpot의 JavaHhread::exit() 메서드를 호출해 JNI 처리 블록을 해제. guard pages, 스레드 목록에 있는 스레드들을 삭제. 이 순간부터는 HotSpot VM에서 자바 코드를 실행하지 못함
- HotSpot VM 스레드를 종료. 이 작업을 수행하면 HotSpot VM에 남아있는 HotSpot VM 스레드들을 safepoint로 옮기고, JIT 컴파일러 스레드들을 중지시킴
- JNI, HotSpot VM, JVMTI barrier에 있는 tracing 기능을 종료시킴
- 네이티브 스레드에서 수행하고 있는 스레드들을 위해 HotSpot의 “vm exited” 값을 설정
- 현재 스레드를 삭제
- 입출력 스트림을 삭제하고, PerfMemory 리소스 연결을 해제
- JVM 종료를 호출한 호출자로 복귀
클래스 로딩 절차도 알고 싶어요?
- 클래스 로딩 절차
- 주어진 클래스의 이름으로 클래스 패스에 있는 바이너리로 된 자바 클래스를 찾는다
- 자바 클래스를 정의
- 해당 클래스를 나타내는 java.lang 패키지의 Class 클래스의 객체를 생성
- 링크 작업을 수행. 이 단계에서 static 필드를 생성 및 초기화하고, 메서드 테이블을 할당
- 클래스의 초기화가 진행되며, 클래스의 static 블록과 static 필드가 가장 먼저 초기화된다.
- loading > linking > Initializing
Bootstrap 클래스 로더
- HotSpot VM은 부트스트랩 클래스 로더를 구현
- Bootstrap 클래스 로더
- HotSpot VM의 BOOTCLASSPATH에서 클래스들을 로드
- java SE(Standard Edition) 클래스 라이브러리를 포함
- HotSpot VM의 BOOTCLASSPATH에서 클래스들을 로드
HotSpot의 클래스 메타데이터
- HotSpot VM 내에서 클래스를 로딩하면 클래스에 대한 instanceKlass와 arrayKlass라는 내부적인 형식을 VM의 Perm 영역에 생성
- instanceKlass는 클래스의 정보를 포함하는 java.lang.Class 클래스의 인스턴스
- HotSpot VM은 내부 데이터 구조인 klassOop라는 것을 사용해 내부적으로 instanceKlass에 접근
- Oop는 Ordinary object pointer
내부 클래스 로딩 데이터의 관리
- SystemDictionary
- 로드된 클래스를 포함하며, 클래스 이름 및 클래스 로더를 키를 갖고 그 키 값으로 klassOop를 갖고 있음
- 클래스 이름과 초기화한 로더의 정보, 클래스 이름과 정의한 로더의 정보도 포함
- safepoint에서만 제거됨
- PlaceholderTable
- 현재 로딩된 클래스들에 대한 정보를 관리
- ClassCircularity Error를 체크할 때 사용
- 다중 스레드에서 클래스를 로딩하는 클래스 로더에서도 사용
- LoaderConstraintTable
- 타입 체크시의 제약 사항을 추정하는 용도로 사용
예외는 JVM에서 어떻게 처리될까?
- 일반적인 예외 처리 경우
- 예외를 발생한 메서드에서 잡을 경우
- 호출한 메서드에 의해 잡힐 경우
- 예외
- 던져진 바이크 코드에 의해서 초기화 가능
- VM 내부 호출의 결과로 넘어올 수도 있고
- JNI 호출로부터 넘어올 수도 있고
- 자바 호출로부터 넘어올 수도 있다
17장 도대체 GC는 언제 발생할까?
GC란?
- GC
- 자바에서 쓰레기는 객체
- 하나의 객체는 메모리를 점유하고, 필요하지 않으면 메모리에서 해제되어야 한다
자바의 Runtime data area는 이렇게 구성된다
- 자바에서 사용하는 메모리 영역
- pc 레지스터
- JVM 스택
- Heap
- 메서드 영역
- 런타임 상수 풀
- 네이티브 메서드 스택
- GC는 힙 영역에서 발생
- 나머지는 GC 대상이 아님
- 클래스 로더 서브 시스템
- 클래스나 인터페이스를 JVM으로 로딩하는 기능울 수행
- 실행 엔진은 로딩된 클래스읭 메서드들에 포함되어 있는 모든 인스트럭션 정보를 실행
Heap 메모리
- 클래스 인스턴스, 배열이 이 메모리에 쌓인다
- 이 메모리는 shard 메모리라고도 불리며, 여러 스레드에서 공유하는 데이터들이 저장되는 메모리
Non-Heap 메모리
- 메서드 영역
- 모든 JVM 스레드에서 공유
- 이 영역에 저장되는 데이터들
- 런타임 상수 풀
- 메서드 데이터, 메서드와 ,생성자 코드
- 런타임 상수 풀
- JVM 스택
- 스레드가 시작할 때 JVM 스택이 생성
- 메서드가 호출되는 정보인 frame이 저장됨
- 지역 변수, 임시 결과와 메서드 수행과 리턴에 관한 정보도 포함
- 네이티브 메서드 스택
- 자바 코드가 아닌 다른 언어로 된 코드들이 실행하게 될 때의 스택 정보를 관리
- pc 레지스터
- 자바 스레드들은 각자의 pc(Program Counter) 레지스터를 갖는다
- 네이티브한 코드를 제외한 모든 자바 코드들이 수행될 때 JVM의 인스트럭션 주소를 pc 레지스터에 보관
GC의 원리
- GC의 역할
- 메모리 할당
- 사용 중인 메모리 인식
- 사용하지 않은 메모리 인식
- 사용하지 않는 메모리를 인식하는 작업을 수행하지 않으면, 할당한 메모리 영역이 꽉 차서 JVM에 hang이 걸리거나, 더 많은 메모리를 할당하려는 현상이 발생함
- 자바의 heap 영역
- Young, Old, Perm 영역
- Perm 영역은 거의 사용되지 않는 영역
- 클래스와 메서드 정보와 같이 자바 언어 레벨에서 사용하는 영역이 아님
- JDK8 부터 이 영역이 사라짐
- Young 영역
- Eden 영역
- 데이터가 꽉차면 Survivor 영역으로 이동
- 두 개의 Survivor 영역
- 둘 중 하나는 반드시 비어있어야한다
- 할당된 Survivor 영역이 차면, GC가 되면서 Eden 영역에 있는 객체와 꽉 찬 Survivor 영역에 있는 객체가 비어 있는 Survivor 영역으로 이동
- Young 영역에서 Old 영역으로 넘어가는 객체 중 Survivor 영역을 거치지 않고 바로 Old 영역으로 이동하는 객체가 있을 수 있음
- 객체가 매우 큰 경우
- Eden 영역
GC의 종류
- 마이너GC : Young 영역에서 발생하는 GC
- 메이저GC : Old 영역이나 Perm 영역에서 발생하는 GC
5가지 GC 방식
- JDK7 이상에서 지원하는 GC 방식
- Serial Collector
- parallel Collector
- parallel Compating Collector
- Concurrent Mark-Sweep Collector
- Garbage First Collector
시리얼 콜렉터
- Young , Old 영역이 Serial 하게 처리
- 하나의 CPU를 사용
- stop-the-world => 콜렉션이 수행될 때 애플리케이션 수행이 정지
- 수행 순서
- 일단 살아 있는 객체들은 Eden 영역에 있다
- Eden 영역이 꼬가차게 되면 To Survivor영역(비어 있는 영역)으로 살아 있는 개체가 이동 Survivor 영역에 들어가기에 너무 큰 객체는 바로 Old 영역으로 이동 From Survivor 영역에 살아 있는 객체는 To Survivor영역으로 이동
- To Survivor 영역이 꽉 찼을 경우, Eden 영역이나 From Survivor 영역에 남아 있는 객체들은 Old 영역으로 이동
- 이후 Old 영역이나 Perm 영역에 있는 객체들은 Mark-sweep-compact 콜렉션 알고리즘을 따른다
- Mark-sweep-compact 콜렉션 알고리즘 수행 순서
- Old 영역으로 이동된 객체들 중 살아 있는 객체를 식별 ( 표시 단계 )
- Old 영역의 객체들을 훑는 작업을 수행해 쓰레기 객체를 식별( 스윕 단계 )
- 필요 없는 객체들을 지우고 살아 있는 객체들을 한 곳으로 모은다( 콤팩트 단계 )
병렬 콜렉터
- 이 방식의 목표는 다른 CPU가 대기 상태로 남아 있는 것을 최소화하는 것
- Young 영역을 병렬로 처리
- Old 영역의 GC는 시리얼 콜렉터와 마찬가지로 Mark-sweep-compact 콜렉션 알고리즘 사용
병렬 콤팩팅 콜렉터
- Young 영역은 병렬 콜렉터와 동일
- Old 영역 GC의 3단계
- 표시 단계 : 살아 있는 객체를 식별해 표시하는 단계
- 종합 단계 : 이전에 GC를 수행해 컴팩션 된 영역에 살아 있는 객체의 위치를 조사하는 단계
- 컴팩션 단계 : 컴팩션을 수행하는 단계. 수행 이후에는 컴팩션된 영역과 비어 있는 영역으로 구분
CMS 콜렉터
- low-latency collector라고도 불림
- 힙 메모리 영역이 크기가 클 때 적합
- Young 영역은 병렬 콜렉터와 동일
- Old 영역 GC 방법
- 초기 표시 단계: 매우 짧은 대기 시간으로 살아 있는 객체를 찾는 단계
- 컨커런트 표시 단계 : 서버 수행과 도ㅠㅇ시에 살아 있는 객체에 표시 하는 단계
- 재표시 단계 : 컨커런트 표시 단계에서 표시하는 동안 변경된 객체에 대해 다시 표시하는 단계
- 컨커런트 스윕 단계 : 표시되어 있는 쓰레기를 정리하는 단계
- 2개 이상의 프로세서를 사용하는 서버에 적당
- 점진적 방식을 지원 > Young 영역의 GC를 더 잘게 쪼개어 서버의 대기 시간을 줄일 수 있음
- CPU가 많지 않고 시스템의 대기 시간이 짧아야 할 때 사용하면 좋음
G1 콜렉터
- G1은 Young 영역, Old영역으로 구분되어 있지 않고, 각 크기 구역별로 구분되어 있으며 이 구역의 크기는 모두 동일
- 구역의 개수는 약 2000개
- G1의 Young GC
- 몇 개의 구역을 선정해 Young 영역으로 지정
- 이 선형적이지 않은 구역에 객체가 생성되면서 데이터가 쌓임
- Young 영역으로 할당된 구역에 데이터가 꽉 차면, GC를 수행
- GC를 수행하면서 살아있는 객체들만 Survivor 구역으로 이동시킴
- 몇 번 aging 작업 수행 시 Old 영역으로 승격
- G1의 Old GC
- 초기 표시 단계 : Old 영역에 있는 객체에서 Survivor 영역의 객체를 참조하고 있는 객체들을 표시
- 기본 구역 스캔 단계 : Old 영역 참조를 위해 Survivor 영역을 훑는다. 이 작업은 Young GC가 발생하기 전에 수행
- 컨커런트 표시 단계 : 전체 힙 영역에 살아있는 객체를 찾는다. 만약 이 때 Young GC가 발생하면 잠시 멈춘다
- 재표시 단계 : 힙에 있는 살아있는 객체들의 표시 작업을 완료. 이 때 snapshot-at-the-begining 알고리즘 사용. CMS GC에서 사용하는 방식보다 빠름
- 청소 단계 : 살아있는 객체와 비어 있는 구역을 식별하고, 필요 없는 개체들을 지운다. 그리고 비어 있는 구역을 초기화
- 복사 단계 : 살아있는 객체들을 비어 있는 구역으로 모은다
강제로 GC시키기
This post is licensed under CC BY 4.0 by the author.