목차

    배경

    지난번 JVM 메모리 영역의 역할과 구조에 대해 알아봤다.

    beststar-1.tistory.com/14

     

    JVM(Java Virtual Machine, 자바 가상 머신) - 3. 메모리 영역

    목차 배경 지난번 JVM의 클래스로더 시스템의 역할과 구조와 동작방식에 대해 알아봤다. beststar-1.tistory.com/13 JVM(Java Virtual Machine, 자바 가상 머신) - 2. 클래스로더 목차 배경 지난번 JVM이 바이트코.

    beststar-1.tistory.com

    이러한 메모리 영역에 적재된 바이트코드는 실행 엔진과 상호작용 한다.

    그렇다면 실행 엔진의 어떠한 구조속에서 어떻게 사용되는지 알아야

    앞서 JVM과 바이트코드를 설명했을때 JIT 컴파일러가 인터프리터의 속도 문제를 개선/보완 했는지 알 수 있으므로 자세히 알아보자.

     


    과정

    JIT 컴파일러

    자바는 바이트코드로 한번 컴파일 하는 과정과, 바이트코드를 인터프리터 하는 방식 2가지를 진행한다.

    그래서 일반적으로 순수 컴파일만 하는 방식 보다는 느리다.

    사람들이 C언어에 비해 자바가 느리다고 하는 이유와 개선 방안 - 인터프리팅 측면, JIT 컴파일러

    바이트코드도 결국엔 기계가 이해하기 위해 기계어로 번역이 돼야 한다.

    이때 인터프리터를 사용하여 프로그램이 실행 중인 런타임 시 한 줄씩 읽기 때문

    런타임 전 소스코드를 미리 읽어서 기계어로 변환하는 방식의 컴파일러보다는 느릴 수밖에 없다.

     

    이런 문제를 개선해 주는 것이 JIT 컴파일러이다.

    번역된 코드를 캐싱해둔 다음 똑같은 코드가 있다면 번역하지않고 캐싱해둔 값을 사용하여

    매번 기계어 코드가 생성되는 것을 방지해 인터프리팅 시간을 단축시킨다.

    인터프리터와 컴파일러의 방식을 적절히 혼합해서 속도를 개선한 것이다.

    어떻게 적절히 혼합해서 개선한걸까? - 컴파일 임계치(Compile Threshold)

    https://miro.medium.com/max/1400/1*VFo0CC-chzvqJk6sls6ukQ.png

    JIT 컴파일러의 JIT는 Just In Time (제 때) 라는 뜻이며 이는 자바 메서드를 호출할때를 말한다.

    해당 메서드의 바이트코드를 기계어로 컴파일해서 Just In Time 컴파일하여 실행한다.

     

    그런데 이 자바 메서드는 처음 호출 되자마자 바로 컴파일 되는게 아니다.

    JVM이 호출되는 메서드 각각에 대해 호출마다 호출 횟수를 누적해서 그 횟수가 특정 수치를 초과 할 때 컴파일하는 것이다.

    즉, 얼마나 자주 호출되는지 검사한 후 '이제는 컴파일이 필요한 시점이다'라고 판단하는 어떠한 기준이 있는 것이다.

    이 기준을 컴파일 임계치 라고 한다. 그런데 이게 무엇이기에 기준으로 삼는걸까?

     

    컴파일 임계치

    다음의 2가지를 합친 것을 말한다.

    • method entry counter (JVM 내에 있는 메서드가 호출된 횟수)
    • back-edge loop counter (메서드가 루프를 빠져나오기까지 돈 횟수)

    두 카운터의 합계를 확인하고 메서드가 컴파일될 자격이 있는지 결정한다.
    메서드가 컴파일 될 자격이 있다면 해당 메서드는 컴파일되기 위해 큐에서 대기하게 된다.

    그러면 이 메서드들은 이후 컴파일 스레드에 의해 컴파일 되는 것이다.

     

    대표적인 예시로 메서드가 실행되는 루프가 정말 길 경우 중간에 컴파일하는 경우이다.

    루프의 실행을 그때그때 카운트하고 컴파일 임계치를 넘게되면

    전체 메서드가 아닌 루프만을 컴파일하여 컴파일된 버전을 바로 실행시키는 것이다.

    for (int i = 0; i < 500; ++i) {
      long startTime = System.nanoTime();
    
      for (int j = 0; j < 1000; ++j) {
     	new Object();
      }
    
      long endTime = System.nanoTime();
      System.out.printf("%d\t%d\n", i, endTime - startTime);
    }

    루프 메서드에 대해 직접 나노초 단위로 실험해봤다.

    76회 실행 시점부터 실행시간 간격이 급격히 줄어든것으로 보아 저 부근이 컴파일 임계치인듯 하다. 

     

    실제로 아래와 같은 나노초 단위의 실험 통계자료도 존재하는 것을 확인했다.

    https://www.slideshare.net/dougqh/jvm-mechanics-understanding-the-jits-tricks-93206227

     

    💡 이렇게 메서드 스택상에서 컴파일된 버전을 바로 실행시키는 것을 OSR(On - Stack Replacement)이라고 한다.

    스택 프레임을 컴파일된 것으로 교체해서 속도를 개선한것이라 보면 된다.

    https://www.slideshare.net/dougqh/jvm-mechanics-understanding-the-jits-tricks-93206227

     

    그럼 이제 이러한 원리가 적용되는 JIT 컴파일러의 내부 구조를 살펴보며 어떻게 개선했는지 더 알아보자.  

     

    JIT 컴파일러 사용 옵션

    JIT 컴파일러는 사용하는 옵션(-client, -server)에 따라 클라이언트, 서버 컴파일러로 나뉜다.

    두 컴파일러의 주요 차이점은 코드 컴파일에 있어서 적극성의 유무이다.

    클라이언트 컴파일러(C1, 컴파일러 1)

    • 서버 컴파일러보다 먼저 컴파일을 시작한다. 적극성이 높다고 볼 수 있다.
    • 최적화를 위한 대기시간이 짧다.
    • 코드 분석과 컴파일 시간이 서버 컴파일러보다 빠르다.
    • 메모리도 더 적게 사용할 수 있다.
    • 하지만 코드실행은 서버가 더 빠르다.  

    서버 컴파일러(C2, 컴파일러 2)

    서버 측 엔터프라이즈 Java 애플리케이션과 같은 장기 실행 애플리케이션의 경우 C1으로는 충분하지 않을 수 있다.

    대신 C2와 같은 서버 측 컴파일러를 사용할 수 있다.

    대부분의 서버 측 프로그램은 오랫동안 실행될 것으로 예상되므로

    C2를 활성화하면 단기 실행되는 경량 클라이언트 응용 프로그램보다 더 많은 프로파일링 데이터를 수집 할 수 있다.

    따라서 더 고급 최적화 기술과 알고리즘을 적용 할 수 있다.

    • 컴파일 전에 많은 정보를 수집하여 최적화에 중점을 둔다.
    • 서버 컴파일러는 절대로 모든 코드를 컴파일하지 않는다.

    Tiered Compiler ( C1 + C2 )

    클라이언트와 서버 두 컴파일러의 장점을 조합한 컴파일러이다.

    먼저 클라이언트 컴파일러로 스타트업 시간을 빠르게 하고, 많이 쓰이는 부분을 서버 컴파일러로 재컴파일하는 방식을 사용한다.

    -server -xx:+TieredCompilation 옵션으로 사용 가능하다.

     

    💡 Java 7 부터 등장했으며 Java 8 부터는 기본 옵션이다.

     

    옵션별 성능 비교 - 스타트업 시간

    https://velog.io/@youngerjesus/자바-JIT-컴파일러#4-자바와-jit-컴파일러-버전

    자바 성능 튜닝 책에 나와있는 애플리케이션 규모별 스타트업 시간 실험 결과부터 보자.

     

    HelloWorld 같은 간단한 수준의 애플리케이션은 별 효과가 없다.

     

    NetBeans는 자바 GUI 애플리케이션이며 스타트업때 때 약 10,000개의 클래스를 로드하고 몇개의 그래픽 객체를 초기화시키는 등의 작업을 한다.

    여기서 클라이언트 컴파일러는 스타트업할 때 매우 유리한 것을 볼 수 있다.
    서버 컴파일러는 1초가 더 걸리는 뚜렷한 차이(2.83 vs 3.92)를 보인다.

    여기서 티어드(Tiered) 컴파일러도 클라이언트 컴파일러보다 약 8% 느리다는 점에 주목해보자.

    GUI 프로그램 같은 경우는 더 빨리 시작할 수록 사용자에게 더 성능이 좋다고 느껴지는 프로그램인데,

    때문에 만약 전반적 성능이 확실히 더 중요하다고 느껴지는 경우가 아니라면 클라이언트 컴파일러를 사용하는게 맞다.

     

    BigApp 같은 경우는 20,000개 이상의 클래스를 로드하고 막대한 초기화를 수행하는 대규모 서버 프로그램이다. 이 프로그램의 초기 스타트업의 영향은 컴파일러보단 디스크에서 읽어야 하는 JAR 파일의 개수이며, 클라이언트 컴파일러를 사용한다고 눈에 띄는 이점은 별로 없다. 0.5초 차이(51.5 vs 52.0) 인데 이 영향이 JAR 파일 개수가 대부분이란 말이다.

     

    이렇게 어플리케이션의 성격에 따라서 옵션을 조절하는 것이 좋다.

    특히 배치같이 많은 작업을 실행하는 어플리케이션일 경우 Tiered, Server 컴파일러가 빠르다.

    배치 동작과 장기 실행 성능 비교 테스트에 대한 예시는 다른분이 잘 정리해두신 글을 참고하기 바란다.

     


    결과

    JIT 컴파일러에 대해 아래 사항들을 공부하고 정리했다.

    • JIT 컴파일러 개념
    • 인터프리터가 컴파일보다 느린 이유
    • 컴파일 임계치 개념과 실습
    • JIT 컴파일러 옵션과 사용 예시

    성과

    • 이 글을 처음 정리할때는 개념에 대한 실습과 참고자료가 부족해 독자가 이해되지 않는 부분이 있다고 피드백을 줬었다. 다시 공부하고 읽어보며 단순히 특징과 현상만 나열해두는 식으로 글이 전개되는 것이 보였다.
      '컴퓨터에게 물어보고 스스로에게 물어보라'는 내 각오처럼 이론에 대한 실습결과를 근거로 남겨 나와 독자의 이해도를 같이 높이고, 시각적 자료를 기반으로 한 설명을 습관화하는 계기가 됐다. 

     


    본 글은 JVM 글에 있던 내용을 분리해둔 것이며,

    추후 자바 성능 튜닝 관련자료를 보며 공부할때 내용이 추가될 예정이다.

    여러 자료를 취합해보니 확실히 JIT 컴파일러를 다룰 일이 있다면 책 자바 성능 튜닝으로 이어서 공부하면 좋을것 같다.


    출처

    프로파일링

    ko.wikipedia.org/wiki/프로파일링_(컴퓨터_프로그래밍)

     

    자바 성능 튜닝 책 - JIT 컴파일러 부분 요약 정리글

    velog.io/@youngerjesus/자바-JIT-컴파일러#4-자바와-jit-컴파일러-버전

     

    자바 성능 튜닝 스터디(1) JIT compiler

    nopainnocode.tistory.com/15