Java 컴파일
1. 컴파일 과정
JVM 실행 전
- 1) 소스 코드(.java)를 바이트 코드(.class)로 변환 (Java compiler)
JVM 실행 후
- 2) 해당 클래스를 사용하는 시점에 클래스 로더가 lazy loading
- 3) 로딩 된 클래스를 OS가 이해할 수 있는 기계어로 변환 (인터프리터)
여기서 알 수 있는 사실은 Java는 C 언어 처럼 미리 기계어로 변환해두지 않고, 자바 컴파일러로는 바이트 코드로 변환 후
실제 클래스가 사용되는 시점에 인터프리터가 기계어로 변환한다는 것이다.
클래스 로더에 대한 자세한 설명은 다음 포스팅을 참고
2. JIT 컴파일러
소스코드를 바로 기계어로 번역하는 것 보다는 빠르겠지만, 매번 클래스가 사용될 때 마다 기계어로 번역하는 것은 비효율적이다.
이를 개선하기 위해 Java 1.2 부터 JIT(Just In Time) 컴파일러 개념이 등장했다.
JVM vendor에 따라 JIT 컴파일러를 구현한 방식에 차이는 있지만, 공통적인 목표는 자주 실행되는 바이트 코드가 있다면 매번 인터프리터로 변환하지 않고 변환된 기계어를 캐싱하여 재사용 하는 것이다.
이때 캐싱된 코드는 JVM 내의 code cache 영역에 저장된다.
Java 컴파일 과정의 특징으로 다음과 같은 현상을 발견 할 수 있다.
1) 클래스는 실행 시점에 동적으로 지연로딩 되므로 처음 로딩할 때 시간이 더 소요된다.
2) JIT 컴파일러가 자주 사용되는 코드는 캐싱하고 최적화하므로, 인터프리터로 번역해서 실행할 때와 캐싱에 존재하는 코드를 바로 실행할 때 성능이 다르다.
JVM의 동작 방식 (자바 코드 실행 과정) 에 대한 내용은 아래 포스팅 참고
JVM Warm up
우리는 운동을 하기 전에 최대의 능률을 끌어올리기 위해 체조나 가벼운 달리기를 하는 등 워밍업을 한다. JVM도 마찬가지로 최대의 성능을 끌어올리기 위해 워밍업이 필요하다. 이를 JVM Warm-up 이라고 한다.
지금까지 JVM Warm-up을 이해하기 위해 빌드업을 해왔다. 복잡한 내용들이 많았지만, 실은 ‘JVM은 자주 실행 되는 코드를 컴파일하고 캐시한다’, ‘클래스는 필요할 때 Lazy Loading 으로 메모리에 적재된다’ 라는 사실 두가지만 기억해두면된다. 이것이 JVM Warm-up의 핵심 아이디어이다.
JVM Warm-up은 Java 컴파일 과정의 특징으로 발생할 수 있는 성능 차이를 개선하기 위해 수행하는 작업이다. 자주 사용하는 클래스를 일부러 호출하여 클래스가 메모리에 로딩되고 code cache 영역에 기계어가 캐싱되는 것을 의도한다.
JVM Warm-up은 운영환경에서 서버를 재기동 하였을 때, 성능 측정 테스트를 해보고 싶을 때 등에 사용된다.
JVM Warm up 방법
warm-up 방법에는 여러가지가 있겠지만 아래 두가지 방법이 접근하기 쉬워서 소개하고자 한다.
1. 사전에 수동으로 warm-up 수행
아래 코드 예제에서 compareSearchTime()이 성능 측정 대상 메서드 이다.
측정할 메서드를 사전에 동일하게 반복 실행하면서 JIT 컴파일러가 최적화하기를 유도한다.
public class RandomArrayMain{
static {
System.out.println("static block start --");
compareSearchTime();
System.out.println("static block finish --\n");
}
public static void main(String[] args) {
RandomArrayMain randomArrayMain = new RandomArrayMain();
System.out.println("warm-up start --");
for(int i=0; i<10; i++){
randomArrayMain.compareSearchTime();
}
System.out.println("warm-up finish --\n");
System.out.println("measure start --");
randomArrayMain.compareSearchTime();
System.out.println("measure finish --");
}
public void compareSearchTime() {
...
}
}
2. JMH(Java Microbenchmark Harness)
JMH는 OpenJDK에서 만든 벤치마크 라이브러리이다. JVM warm-up 기능을 제공하여 편리하게 성능 측정을 할 수 있도록 돕는다.
1) gradle 빌드 도구를 사용하는 경우 다음과 같이 설정하여 관련 라이브러리를 다운받다. (gradle 7.1 기준)
build.gradle
plugins {
id 'java'
id "me.champeau.gradle.jmh" version "0.5.3"
}
2) jmh 디렉토리를 추가한 프로젝트 구성
jmh를 사용하여 성능을 측정할 소스코드는 별도의 jmh 디렉토리를 사용해야한다.
- gradle 기반 프로젝트 생성시 기본으로 생성되는 main, test와 동일 레벨에 jmh 디렉토리를 생성한다.
(성능 측정 전용 프로젝트이면 main, test를 삭제하셔도 무관하다.) - jmh 디렉토리 하위에는 java와 resourse 디렉토리를 생성해준다.
- java 디렉토리에 패키지를 하나 만들고 그 안에 소스코드를 작성한다.
(java 디렉토리 하위에 패키지를 생성하지 않고 바로 클래스를 넣으면 컴파일 오류가 발생)
다음과 같은 패키지 구조가 된다.
study-test
├──src
| └──jmh
| | └──java
| | | └──array
| | | └──RandomArrayBenchmarkTest.java
| | └──resource
| └──main
| └──test
├──build.gradle
├──gradlew
3) 성능 측정 코드 작성
package study;
@BenchmarkMode(Mode.AverageTime) // 평균 시간 측정. 결과 파일에 Score로 표시
@OutputTimeUnit(TimeUnit.NANOSECONDS) // 시간 단위 설정
@Fork(2) // 벤치 마크 수행 횟수. 횟수가 늘어날수록 정밀도는 높아지지만 오래걸림
@Warmup(iterations = 5) // warm-up 5번 수행
public class RandomArrayBenchmarkTest {
@Benchmark // 벤치마크 대상
public void sum() {
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += i;
}
}
}
4) jmh 실행 커맨드
프로젝트 최상위 레벨에 있는 gradlew 를 이용해 jmh를 실행 한다.
$ ./gradlew jmh
5) 실행결과 확인
정상적으로 실행되었다면 다음과 같은 출력 메세지가 뜰것이다.
해당 경로에 가서 텍스트 파일을 열면 실행 결과를 확인할 수 있다.
Benchmark result is saved to /프로젝트 경로/study-test/build/reports/jmh/results.txt
두 가지 방법으로 각각 Array, ArrayList, LinkedList의 속도 비교를 수행한 결과가 궁금하시면 다음 포스팅을 참고.