Part1에 이어서 JVM에 대해 공부해보자.
💻 JVM 구조
JVM은 크게 3가지로 볼 수 있다.
- Class Loader
- Runtime Data Area
- Execution Engine
💻 Java Application 실행 과정
1. Application이 실행되면 JVM이 OS로부터 메모리를 할당 받음
- JVM은 할당 받은 메모리를 용도에 따라 영역을 구분하여 관리
2. 자바 컴파일러(javac.exe)가 자바 소스코드(.java)를 읽어 바이트코드(.class)로 변환
3. Class Loader를 통해 바이트코드를 JVM으로 로딩
4. 로딩된 바이트코드는 Execution Engine을 통해 해석됨
5. 해석된 바이트코드는 Runtime Data Areas에 배치되어 실행됨
- 실행되는 과정에서 GC같은 작업이 수행됨
이렇듯 자바는 다음과 같은 과정을 거치게 된다.
- 바이트코드로 Compile(컴파일)하는 과정
- 바이트코드를 Interpreter(인터프리터)하는 과정
왜냐하면 바이트코드를 기계는 읽지못하기 때문에 기계어로 번역이 되어야 한다.
그래서 실행엔진(Execution Engine)은 이와 같은 바이트코드를 실제로 JVM 내부에서 기계가 실행할 수 있는 형태로 변경하는 것이다.
이러한 특징때문에 성능 이슈가 항상 있었다.
파이썬을 예시로 들면 한번에 기계가 읽는다.
하지만 자바는 바이트코드로 컴파일 후 기계가 읽을 수 있게 인터프리터로 기계어를 변환하는 과정이므로
여러 언어들로부터 느리다고 공격받는 것이다.
- Compile 방식 : 런타임 전 소스코드를 미리 읽어서 기계어로 변환하는 방식
- Interpreter 방식 : 런타임 시에 한줄씩 읽으면서 변환
이런 문제를 개선하기위해 나온 것이 JIT Compiler이다.
한번 읽은 코드를 캐싱해둔 다음 똑같은 코드가 있다면 다시 읽지않고 캐싱해둔 값을 사용하여 인터프리터와 컴파일 방식을 적절히 혼합해 속도를 개선한 것이다.
이 부분은 다음 실행 엔진(Execution Engine)을 설명하면서 얘기해보자.
💻 Execution Engine
Runtime Data Area에 할당된 바이트코드를 실행시키는 주체이다.
CPU는 바이트코드를 바로 실행할 수 없기때문에 Execution Engine이 기계어로 변환하여 실행한다.
코드를 실행하는 방식은 크게 2가지로 Interpreter와 JIT Compiler가 존재한다.
1. Interpreter
- 바이트코드를 하나씩 읽어서 해석하고 실행하는 역할을 수행
- 바이트코드 하나 하나의 해석은 빠르지만 인터프리팅 결과의 실행은 느리다는 단점이 있음
- 같은 메서드라도 여러번 호출될 때 매번 새로 수행해야 함
- 바이트코드라는 '언어'는 기본적으로 인터프리터 방식으로 동작함
2. JIT ( Just In Time ) Compiler
- Interpreter의 단점을 보완하기위해 도입했음
- 인터프리터 방식으로 실행하다가 적절한 시점에 바이트코드 전체를 컴파일하여 Native Code로 변경하고, 그 이후에는 해당 메서드를 인터프리팅하지 않고 Native Code로 직접 실행하는 방식
- Native Code를 실행하는 것이 하나씩 인터프리팅하는 것보다 빠르고, 캐시에 보관되기에 한 번 컴파일된 코드는 계속 빠르게 수행된다
* Native Code는 자바에서 부모가 되는 C언어나 C++, 어셈블리어를 의미한다.
JIT 컴파일러가 컴파일하는 과정은 바이트코드를 하나씩 인터프리팅하는 것보다 훨씬 오래 걸린다.
만약 한 번만 실행되는 코드라면 인터프리팅하는 것이 훨씬 유리하다는 것이다.
따라서, JIT 컴파일러를 사용하는 JVM들은 내부적으로 해당 메서드가 얼마나 자주 수행되는지 체크하고, 일정 정도를 넘을 때에만 컴파일을 수행하게 된다.
🤔 그렇다면 어떤 기준으로 컴파일을 수행할까?? - 컴파일 임계치(Compile Threshold)
- 메서드가 호출된 횟수
- 메서드가 루프를 빠져나오기까지 반복한 횟수
두 카운터의 합계를 확인하고 메서드가 컴파일될 자격이 있는지 결정한다.
메서드가 자격이 있다면 컴파일되기 위해 큐에서 대기한다.
큐에 있는 메서드들은 컴파일 스레드에 의해 컴파일된다.
예시로 한번 테스트해보자.
System.nanoTime() 은 작동중인 JVM의 정밀한 시간 소스의 현재 값을 long타입으로 나노세컨드(1/1000000000초)를 반환한다.
위 test클래스를 실행하면 i번이 1번 돌 때마다 a가 +1씩 1000번을 수행하게되는데 그 과정을 측정한다.
위 실행결과처럼 갑자기 속도가 엄청나게 빨리지는 부분들이 있었는데 저 부근이 컴파일 임계치인듯하다.
위 방식대로 테스트한 자료가 있는데 우리가 테스트한 결과랑 비슷하다.
여기 자료는 AZUL SYSTEMS의 JDK로 테스트했다.
인터프리터에 수행하는 코드가 오랫동안 루프 지속되는 경우에는 중간에 컴파일될 필요가 있다.
따라서 루프의 실행을 카운트하고 임계치를 넘게되면 전체 메서드가 아닌 루프만을 컴파일하여 컴파일 버전을 바로 실행시키는데 이것을 OSR이라고 한다.
지금까지 JIT 컴파일러는 어떤 기준점을 가지고 컴파일을 하는지 대략적으로 적어보았는데 이것은 모든 VM의 공통된 기준점이 아니다. Part1에서도 말했듯이 이 부분은 JVM 명세에 규정되어 있지 않고 구현자의 재량에 맡겨둔 부분이다. 따라서 JVM 벤더(Vendor)들은 다양한 기법으로 실행 엔진을 향상시키고 다양한 방식의 JIT 컴파일러를 도입하고 있다.
대부분의 JIT 컴파일러는 다음 그림과 같은 형태로 동작하고 있다.
JIT 컴파일러는 바이트코드를 중간 단계의 표현인 IR로 변환하여 최적화를 수행하고, 그 후 네이티브 코드를 생성한다.
오라클 핫스팟 VM은 핫스팟 컴파일러라고 불리는 JIT 컴파일러를 사용한다.
내부적으로 프로파일링을 통해 가장 컴파일이 필요한 부분인 핫스팟을 찾아낸 다음, 이 핫스팟을 네이티브 코드로 컴파일하기 때문이다.
핫스팟 VM은 한번 컴파일된 바이트코드라도 해당 메서드가 자주 쓰이지 않는다면 캐시에서 네이티브 코드를 덜어내고 다시 인터프리터 모드로 동작한다.
핫스팟 VM은 서버 VM과 클라이언트 VM으로 나뉘어 있고, 각각 다른 JIT 컴파일러를 사용한다.
클라이언트 VM과 서버 VM은 동일한 런타임을 사용하지만 다른 JIT 컴파일러를 사용한다.
서버 VM에서 사용하는 Advanced Dynamic Optimizing Compiler가 더 복잡하고 다양한 성능 최적화 기법을 사용하고 있다.
이렇듯 자바 성능 개선의 많은 부분은 이 실행 엔진을 개선하여 이뤄지고 있다.
JIT 컴파일러는 물론 다양한 최적화 기법을 도입하여 JVM의 성능은 계속해서 향상되고 있다.
3. Garbage Collector
앞으로 사용되지 않는 객체의 메모리를 Garbage라고 부른다.
이런 Garbage를 정해진 스케줄에 의해 정리해주는 것을 GC(Garbage Collector)이다.
- Runtime Data Area의 Heap Area에 생성된 객체들 중 더 이상 참조되지 않는 객체를 모아 제거함
* C 또는 C++의 경우에는 메모리 할당/사용 후 반환하지 않으면 메모리 공간을 차지하여 낭비함
- 일반적으로 자동 실행되지만, 수동으로 실행하기 위해 `System.gc()` 를 사용할 수 있음
- 즉, Java의 경우 GC가 메모리 공간을 알아서 관리 및 최적화를 해준다
Stop The World
GC를 수행하기 위해 JVM이 멈추는 현상을 의미한다.
GC가 작동하는 동안 GC관련 Thread를 제외한 모든 Thread는 멈추는데,
일반적으로 '튜닝'이라는 것은 이 시간을 최소화하는 것을 의미한다.
GC의 종류
- Serial GC
- Parallel GC (자바 8)
- CMS GC
- G1 GC (자바 9, 자바 10)
- Z GC
이렇게 JVM의 Execution Engine(실행 엔진) 에 대해 적어보았는데 구조상 ClassLoader를 먼저 적었어야 이해하기 좀 더 쉬웠을거 같다.
GC도 중요하지만 여기서는 간단하게 설명만하고 넘어간다.
참고
https://tecoble.techcourse.co.kr/post/2021-07-15-jvm-classloader/
https://d2.naver.com/helloworld/1230
https://www.youtube.com/watch?v=zta7kVTVkuk
'프로그래밍 > Java' 카테고리의 다른 글
자바 getBytes() (0) | 2022.01.28 |
---|---|
자바 toCharArray() (2) | 2022.01.28 |
JVM(자바가상머신)이란? - Part 1, 소개 (0) | 2022.01.23 |
valueOf() 와 parseInt() 의 차이점 (0) | 2022.01.21 |
컬렉션 프레임워크(Collections Framework) (0) | 2022.01.19 |