- Published on
Optimizing Java Chap2
- Authors
- Name
- ywj9811
인터프리팅과 클래스로딩
JVM은 스택 기반의 해석 머신으로 레지스터는 없지만 일부 결과를 실행 스택에 보관하며, 이 스택의 맨 위에 샇인 값들을 가져와 계산하는 것이다.
JVM 인터프리터(해석기)의 기본 로직은 평가 스택(메소드별로 하나씩 생성)을 이용해 중간 값들을 담아두고 가장 마지막에 실행된 명령어와 독립적으로 프로그램을 구성하는 옵코드(명령어)를 하나씩 순서대로 처리하는 방식으로
while 루프 안의 switch문
이렇게 떠올릴 수 있다.
자바 클래스로딩 매커니즘을 살펴보면 자바 프로세스가 새로 초기화되면 줄지어 연결된 클래스로더가 차례차례 작동한다.
위는 예시 사진으로, 위와 같이 우선 부트스트랩 클래스로더가 필수 클래스를 로드한다.
그리고 그 아래에 확장 클래스로더가 생겨서 부트스트랩 클래스로더를 자신의 부모로 설정하고 필요할 때 클래스로딩 작업을 부모에게 넘기게 된다.
이후에 어플리케이션 클래스로더가 생성되고 지정된 classPath에 유저가 만든 클래스를 로드한다.
즉, 순서대로 모두 상속관계를 가지게 된다.
이런식으로 클래스로더가 위치하게 되어 프로그램 실행 도중 새 클래스를 발견하면 디펜던시에 로드하게 된다.
만약 클래스로더가 해당 클래스를 발견하지 못하면 부모 클래스로더에게 룩업(찾아보기)를 넘긴다.
그렇게 쭉 부모로 올라가다가 부트스트랩에서도 룩업을 하지 못하게 되면 ClassNotFoundException
예외가 발생하게 되는 것이다.
바이트코드 실행
자바 소스 코드는 실행될때 까지 많은 변환 과정을 거친다.
위의 과정에서 보이 듯 첫 단계는 자바 컴파일러 javac를 이용해 컴파일 하는 것으로 자바 소스 코드를 바이트코드로 가득 찬 .class 파일로 바꾸는 것이다.
이 바이트코드는 특정 컴퓨터 아키텍처에 특정하지 않는 중간표현형으로 이식성이 좋아 개발을 마친 소프트웨어는 JVM 지원 플랫폼 어디에서도 실행할 수 있고 자바 언어에 대해서도 추상화 되어 있다.
이때 .class 파일을 해부하면 아래와 같은 내용을 가지고 이루어져있다.
컴포넌트 | 설명 |
---|---|
매직 넘버 | 0xCAFEBABE |
→ 모든 클래스파일의 시작으로 이 파일이 클래스 파일임을 나타내는 4바이트 16진수 | |
클래스 파일 포맷 버전 | 클래스 파일의 메이저/마이너 버전 |
→ 클래스 파일을 컴파일할 때 꼭 필요한 버전 숫자(4바이트) | |
상수 풀 | 클래스 상수들이 모여 있는 위치 |
→ 코드 곳곳에 등장하는 상숫값 (클래스명, 인터페이스명, 필드명…) | |
액세스 플래그 | 추상 클래스, 정적 클래스, enum… 클래스 종류 표시 |
→ class에 적용한 modifier, class의 종류 | |
this 클래스 | 현재 클래스명 |
슈퍼클래스 | 슈퍼클래스(부모클래스)명 |
인터페이스 | 클래스가 구현한 모든 인터페이스 |
필드 | 클래스에 들어 있는 모든 필드 |
메소드 | 클래스에 들어 있는 모든 메소드 |
속성 | 클래스가 지닌 모든 속성 (ex: 소스 파일명…) |
https://blog.lse.epita.fr//2014/04/28/0xcafebabe-java-class-file-format-an-overview.html (자세한 설명)
핫스팟 입문
성능 관점에서 자바에 가장 큰 변화를 가져온 것이 핫스팟 가상 머신이다.
언어는 zero cost 추상화 사상에 근거하여 ‘기계에 가까운 언어’와 ‘개발자 생산성 언어’ 사이에서 갈등을 겪게 된다.
이에 따른 zero overhead 원칙은 이론적으로는 아주 그럴싸하지만, 이는 결국 사용자가 매우 저수준까지 정해줘야 한다는 단점이 있다.
자바는 이러한 zero overhead 원칙에 동조하지 않는다.
핫스팟은 프로그램의 런타임 동작을 분석하고 성능에 가장 유리한 방향으로 최적화를 적용하는 가상 머신인 것이다.
JIT 컴파일이란?
그렇다면 JIT컴파일은 무엇일까
핫스팟은 프로그램 단위(메소드, 루프)를 인터프리티드 바이트코드에서 네이티브 코드로 컴파일 하는 것인데, 이 동작을 JIT 컴파일이라 한다.
즉, 핫스팟은 인터프리티드 모드(실행 중에 해석하여 사용) 도중 가장 자주 실행되는 코드 파트를 JIT 컴파일을 수행하는 것이다.
JIT 컴파일을 하게 되면 native 코드로 컴파일되어 CPU에서 직접 실행하기에 성능이 최대로 발휘될 수 있다.
이러한 방식을 사용하게 되면 컴파일러가 해석 단계에서 수집한 추적 정보를 근거로 최적화를 결정하기 때문에 굉장히 효율적으로 동작할 수 있다.
JVM 메모리 관리
핫스팟과 더불어 자바를 독보적인 언어로 만들어준 특징은 자동 메모리 관리 기능이다.
자바는 **가비지 수집(garbage collector)**이라는 프로세스를 이용해 힙 메모리를 자동으로 관리해준다.
이를 통해 JVM이 더 많은 메모리를 할당해야 할 때 불필요한 메모리를 회수하거나 재사용하는 불확정적 프로세스다.
이때 GC(Garbage Collector) 이 실행되면 어플리케이션이 중단되기 때문에 이후에 자바 성능 최적화를 다룰 때 깊이 다루도록 할 것이다.
스레딩과 자바 메모리 모델(JVM)
자바는 멀티스레드 프로그램을 기본적으로 지원한다.
이 자바 어플리케이션 스레드는 각각 정확히 하나의 전용 OS 스레드에 대응된다.
- 자바 프로세스의 모든 스레드는 가비지가 수집되는 하나의 공용 힙을 가진다.
- 한 스레드가 생성한 객체는 그 객체를 참조하는 다른 스레드가 액세스할 수 있다.
- 기본적으로 객체는 변경 가능하다. 즉, 객체 필드에 할당된 값은 프로그래머가 애써 final 키워드로 불변을 표시하지 않는 한 바뀔 수 있다.
자바의 멀티스레드 방식은 위의 세가지 기본 설계 원칙에 기반한다.
이때 JMM이 무엇인가, JVM은 서로 다른 실행 스레드가 객체 안에 변경되는 값을 어떻게 바라보는지를 기술한 공식 메모리 모델이다.
만약 스레드A 와 스레드B 가 둘 다 객체 obj객체를 참조할 때 A가 obj값을 바꾸게 되면 B는 무슨 값을 참조하게 될 것인가?
이때 자바가 대처할 수 있는 방법은 Mutual exclusion lock이 유일한 방법이다.
이것이 JMM의 작동 원리로 상세한 작동 원리는 추후에 다루도록 한다.
JVM 모니터링과 툴링
JVM은 실행중인 어플리케이션을 instrumentation(오류 진단 혹은 성능 개선을 위해 특정 코드를 끼워 넣는 것), 모니터링, 관측하는 다양한 기술을 제공한다.
- 자바 관리 확장 (Java Management Extentions) (JMX)
- JVM과 그 위에서 동작하는 어플리케이션을 제어하고 모니터링하는 강력한 범용 툴로 JVM을 관리하는 기본 수단이다.
- 자바 에이전트 (Java agent)
- 자바 언어로 작성된 툴 컴포너트로 메소드 바이트코드를 조작한다.
- JVM 툴 인터페이스 (JVM Tool Interface) (JVMTI)
- 자바 에이전트와 비슷한 역할을 하지만 이는 네이티브 코드로 작성해야 한다.
- 서비스어빌리티 에이전트 (Serviceability Agent) (SA)
- 자바 객체, 핫스팟 자료 구조 모두 표출 가능한 API와 툴을 모아놓은 것이다.
VisualVM
시각화 툴로 JVM 어태치 매커니즘을 통해 실행 프로세스를 실시간 모니터링 한다.
Java9 부터 주요 배포 파일에서 VisualVM이 빠졌기 때문에 이후의 버전은 개발자가 따로 바이너리 파일을 내려받아야 한다.
VisualVM 의 탭
- 개요
- 자바 프로세스에 관한 요약 정보 표시
- 모니터
- CPU, 힙 사용량 등등 JVM을 고수준에서 원격 측정한 값이 표시
- 스레드
- 실행중인 어플리케이션 각 스레드가 시간대별로 표시
- 샘플러 및 프로파일러
- CPU 및 메모리 사용률에 관한 단순 샘플링 결과 표시