Chapter 2. JVM 이야기
성능에 관심 있는 개발자라면 기본적인 JVM 기술 스택 구조를 이해해야 한다.
2.1 인터프리팅과 클래스로딩
- JVM은 스택 기반의 해석 머신
- 일부 결과를 실행 스택에 보관하며 스택 맨 위에 쌓인 값을 가져와 계산
인터프리팅
- JVM 인터프리터 기본 로직은 ‘while 루프 안의 switch 문’
- 평가 스택을 이용해 중간값들을 담아두고 가장 마지막에 실행된 명령어와 독립적으로 프로그램을 구성하는 옵코드(명령 코드, operation code)를 하나씩 순서대로 처리
- 오라클/openJDK/VM 같은 실제 제품급은 이보다 더 복잡함
클래스 로딩
- 가상 머신 실행이 시작되기 전 클래스를 로드해야 하는데 이 과정을 클래스로딩이라고 한다.
- 제일 먼저 부트스트랩 클래스 로더가 자바 런타임 코어 클래스를 로드
- 부트스트랩 클래스 로더의 주 임무는 최소한의 필수 클래스만 로드하는 것 (
java.lang.Object
, Class
, Classloader
)
- 다른 클래스 로더가 나머지 클래스를 로드
- 그 다음 확장 클래스로더가 생성된다
- 부트스트랩 클래스로더를 부모로 설정하여 필요할 때 클래스 로딩 작업을 넘긴다.
- 특정한 OS나 플랫폼에 네이티브 코드를 제공하고 기본 환경을 오버라이드할 수 있다.
- 끝으로 애플리케이션 클래스로더가 생성된다.
- 지정된 클래스패스에 위치한 유저 클래스를 로드
- 자바는 런타임에 처음 보는 새 클래스를 디벤던시에 로드한다.
- 클래스로더가 클래스를 못 찾으면 부모 클래스로더에 위임하는데 그래도 찾지 못하면
ClassNotFoundException
이 발생한다.
2.2 바이트코드 실행
- javac(자바 컴파일러)는 .java 소스 코드를 .class 바이트코드 파일로 변환한다.
- 바이트코드는 특정 컴퓨터 아키텍처에 특정하지 않은 중간 표현형이다.
- 즉 컴파일된 바이트코드는 JVM 지원 플랫폼 어디서든 실행될 수 있다.
자바 언어와 JVM은 어느 정도 독립적이다. JVM 규격에 따라 컴파일되는 JVM 언어는 모두 다 실행 된다. (ex. 스칼라)
클래스 파일 해부도
JVM은 클래스를 로드할 때 올바른 형식을 준수하고 있는지 빠짐 없이 검사한다.
- 매직 넘버: 0xCAFEBABE
- 모든 클래스 파일은 이 파일이 클래스 파일임을 나타내는 4바이트 16진수로 시작한다.
- 자바 9부터는 0xCAFEDADA
- 클래스 파일 포맷 버전: 클래스 파일의 메이저/마이너 버전
- 클래스로더의 호환성 보장을 위해 검사
- 클래스를 실행하는 JVM이 컴파일한 JVM보다 버전이 낮으면 런타임에
UnsupportedClassVersionError
예외 발생
- 상수 풀: 클래스 상수들이 모여 있는 위치
- ex) 클래스 명, 인터페이스명, 필드명 등
- JVM이 코드를 실행할 때 런타임에 배치된 메모리 대신 이 상수 풀 테이블을 찾아 값을 참조한다.
- 엑세스 플래그: 클래스 종류를 표시
- 플래그 첫 부분은 일반 프로퍼티로
public
인지 final
클래스인지를 판별
- 인터페이스인지 추상 클래스인지도 표시
- 플래그 끝부분엔 클래스 파일이 소스 코드에 없는 합성 클래스인지, 애너테이션 타임인지,
enum
인지를 각각 나타낸다.
- this 클래스, 슈퍼 클래스, 인터페이스 엔트리: 각각 클래스 파일의 클래스명, 슈퍼 클래스 명, 구현한 모든 인터페이스 명을 표시
- 클래스에 포함된 타입 계층을 나타낸다.
- 각각 상수 풀을 가리키는 인덱스로 표시
- 필드: 클래스에 있는 모든 필드
- 메서드: 클래스에 있는 모든 메서드
- 속성: 클래스가 지닌 모든 속성 (ex. 소스 파일명 등)
2.3 핫스팟 입문
1999년 4월 핫스팟 가상 머신 도입 이후 자바는 C/C++ 같은 언어에 필적하는 성능을 자랑하며 진화를 거듭했다.
- 언어 및 플랫폼을 설계할 때 두 가지 가치를 끊임없이 저울질해야 한다.
- 제로 코스트 추상화 사상에 근거한 ‘기계에 가까운’ 언어
- 개발자 생산성에 무게를 두는 엄격한 저수준 제어
- 제로-오버헤드 원칙을 준수하는 언어 ex) C++
- 성능은 좋다.
- 개발자가 세세한 저수준 레벨까지 컴퓨터와 OS를 학습해야 하기에 엄청난 러닝 커브
- 소스 코드를 빌드하면 플랫폼 종속적인 기계어로 컴파일된다. (AOT 컴파일, Ahead-of-Time)
- 자바의 핫스팟은 프로그램의 런타임 동작을 분석하고 성능에 가장 유리한 방향으로 영리한 최적화를 적용하는 가상 머신
- 핫스팟 VM의 목표
- 개발자가 억지로 VM 틀에 맞게 프로그램을 욱여넣지 않도록 한다.
- 개발자가 자연스럽게 자바 코드를 작성하고 바람직한 설꼐 원리를 따르도록 하는 것
2.3.1 JIT 컴파일이란?
전통적인 입장에서 컴퓨터 프로그램을 만드는 방법은 두 가지가 있는데, 인터프리트 방식과 정적 컴파일 방식으로 나눌 수 있다. 이 중 인터프리트 방식은 실행 중 프로그래밍 언어를 읽어가면서 해당 기능에 대응하는 기계어 코드를 실행하며, 반면 정적 컴파일은 실행하기 전에 프로그램 코드를 기계어로 번역한다.
JIT 컴파일러는 두 가지의 방식을 혼합한 방식. 실행 시점에서 인터프리트 방식으로 기계어 코드를 생성하면서 그 코드를 캐싱하여, 같은 함수가 여러 번 불릴 때 매번 기계어 코드를 생성하는 것을 방지한다.
즉, 자바 컴파일러가 자바 프로그램 코드를 바이트코드로 변환한 다음, 실제 바이트코드를 실행하는 시점에서 자바 가상 머신이 바이트코드를 JIT 컴파일을 통해 기계어로 변환한다.
참고 - 위키백과
- JIT (Just-in-Time) 컴파일 기술
- 핫스팟이 프로그램 단위(메서드와 루프)를 런타임에 인터프리티드 바이트코드에서 네이티브 코드로 컴파일하는 것
- 인터프리티드 바이트코드는 CPU를 추상화한 구조라 다른 플랫폼에서 실행될 수 있지만 느리다.
- 바이트코드는 기계어는 아니지만 가상 머신에 의해 기계어로 손쉽게 변환할 수 있는 코드이다.
- 성능을 최대로 내려면 네이티브 기능을 활용해 CPU에서 직접 프로그램을 실행시켜야 한다. → 네이티브 코드로 컴파일 필요
- 핫스팟은 인터프리티드 모드로 실행하는 동안 애플리케이션을 분석하며 JIT 컴파일을 수행
- 자주 실행되는 코드 파트를 JIT 컴파일한다.
- 분석하는 동안 정보가 취합되며 더 정교한 최적화가 가능해진다.
- 특정 메서드가 어느 임계를 넘어가면 프로파일러가 특정 코드 섹션을 컴파일/최적화한다.
- 자바는 프로필 기반 최적화(PGO, profile-guided optimization)를 응용하는 환경에서 실행된다.
- 런타임 정보를 활용하여 동적 인라이닝 또는 가상 호출 등으로 성능을 개선할 수 있다.
- 핫스팟 VM이 시동 시 CPU 타입을 정확히 감지해 특정 프로세서에 맞게 최적화를 적용할 수 있다.
- 자바는 제로-오버헤드 추상화를 포기했기에 애플리케이션의 실제 동작 방식을 단순히 넘겨 짚지 않도록 조심해야 한다.
2.4 JVM 메모리 관리
핫스팟의 컴파일 시스템과 더불어 자바를 독보적인 언어로 만들었던 특징은 자동 메모리 관리 기능이다.
- C, C++ 등은 개발자가 메모리 할당/해제 작업을 직접 수행한다
- 좀 더 확정적 성능을 낼 수 있음
- 리소스 수명을 객체 생성/삭제와 직접 결부시킬 수 있음
- 개발자의 책임이 막중함
- 자바는 가비지 콜렉션(Garbage Collection)을 통해 힙 메모리를 자동 관리한다.
2.5 스레딩과 자바 메모리 모델 (JMM)
2.6 JVM 구현체 종류
- OpenJDK
- 자바 기준 구현체를 제공하는 특별한 오픈 소스 프로젝트
- 오라클이 직접 주관/지원하며 자바 릴리즈 기준을 발표
- 오라클 자바 (Oracle)
- 가장 널리 알려진 구현체
- OpenJDK 기반이지만 오라클 사용 라이선스로 재라이선스를 받았다.
- 오라클 자바의 변경된 내용은 OpenJDK 공개 저장소에 커밋된다.
2.6.1 JVM 라이선스
- JVM 구현체는 거의 다 오픈 소스이고 대부분 핫스팟에서 비롯된 제품이다.
- 오라클 자바는 OpenJDK 코드 베이스이지만 오픈 소스가 아닌 상용 제품이다.
- OpenJDK와 오라클 자바는 라이선스 외에 아무런 차이가 없다.
- 회사 밖으로 오라클 바이너리를 재배포하는 행위는 허용되지 않는다.
- 사전 동의 없이 오라클 바이너리를 함부로 패치하면 안 된다.
2.7 JVM 모니터링과 툴링
JVM은 애플리케이션을 인스트루먼테이션, 모니터링, 관측하는 다양한 기술을 제공
인스트루먼테이션(instrumentation): 오류 진단이나 성능 개선을 위해 애플리케이션에 특정한 코드를 끼워 넣는 것
Java Management Extensions (JMX)
- JVM과 그 위에서 동작하는 애플리케이션을 제어하고 모니터링하는 범용 툴
- 메서드를 호출하고 매개변수를 바꿀 수 있다.
- JVM을 관리하는 기본 수단
Java agent
- 자바 언어로 작성된 툴 컴포넌트
java.lang.instrument
인터페이스로 메서드 바이트코드 조작
- 에이전트 JAR 파일에서 매니페스트(manifest, manifest.mf 파일)은 필수
- Premain-Class 속성에 에이전트 클래스명을 반드시 지정해야 하는데 자바 에이전트 등록 후크 역할을 수행하는
premain()
메서드를 구현해야 한다.
- 바이트코드를 조작하는 기능을 제공하며
premain()
은 main()
메서드 이전에 실행된다.
JVM Toll Interface (JVMTI)
- 자바 인스트루먼테이션 API로도 부족하면 JVMTI를 대신 사용할 수 있다.
- C/C++ 같은 네이티브 컴파일 언어로 작성해야 한다.
- 단 네이티브 코드로 작성해야하기에 실행 중인 애플리케이션에 악영향을 미칠 수 있다.
- 가급적 자바 에이전트로 작성하는 게 낫고 더 쉽다.
Serviceability Agent (SA)
- 자바 객체, 핫스팟 자료 구조 모두 표출 가능한 API 툴을 모아 놓은 것
- SA를 이용하면 대상 JVM에서 코드를 실행할 필요가 없다.
- symbol lookup 및 자바 프로세스까지 디버깅 가능
2.7.1 VisualVM
- 넷빈즈 플랫폼 기반의 시각화 툴
- 어태치 매커니즘을 이용해 실행 프로세스를 실시간 모니터링
- attach mechanism: 자바 Attach API를 이용해 애플리케이션을 타깃 JVM에 부착하는 장치