자바 최적화를 읽고 자바의 심연을 들여다봤습니다

문지은·2023년 12월 30일
1
post-thumbnail

책에 대한 짧은 잡담

이 책을 읽게 된 이유는 전 직장의 팀장님이 이 책으로 스터디를 한 것이 계기가 되었다. 그 때 나는 막 자바를 업무에 사용하면서 이제 이 정도면 자바를 어느 정도 안다고 생각하고 있었다. 하지만 팀장님의 발표는 아예 처음 듣는 내용으로 가득했다. 그렇게 우매함의 봉우리에서 좌절을 맛보고 이 책은 언젠가는 꼭 읽어야 겠다고 생각하고 있었다.

역시나 예상했던 대로 책의 내용은 처음보는 내용들과 이해하기 힘든 내용들도 가득했다. 그래도 이왕 시작한 책을 이해가 가지 않더라도 끝까지 읽어보겠다고 다짐했고 최선을 다해서 이해했던 내용을 바탕으로 글을 써보겠다. 공부가 더 필요한 부분은 구글링을 통해서 내용을 찾아보며 정리했기에 구글링 내용도 함께 정리했다. 이 글은 독자가 자바를 어느 정도 사용을 해봤고 GC와 JIT에 관련해서 개괄적인 내용을 알고 있다는 가정을 가지고 작성했다.

인상 깊었던 내용

GC

개발자가 GC를 선정할 때는 모든 소프트웨어 엔지니어링이 그렇듯이 trade-off를 고려해야 한다. 대표적인 GC 선정시 고려해야 할 관점은 CPU 효율 및 처리율이 우수한 알고리즘을 선택할 지, 혹은 중단시간이 짧은 알고리즘을 선택할 지 이다. 최신 GC의 특징에 대해서 알아보고 어떤 상황에 어떤 GC를 선택해야 좋을지 알아보자.

동시 GC 이론

최신 GC는 불확정적 STW 문제를 해결하기 위해 노력한다. 동시 수집기를 써서 애플리케이션 스레드의 실행 도중 수집에 필요한 작업 일부를 수행해서 중단 시간을 줄인다. 이를 위해서 필요한 두가지 개념을 소개한다.

  • safe point
    STW 가비지 수집을 실행하려면 애플리케이션 스레드를 모두 중단시켜야한다. 해당 작업을 하기 위해서 각 스레드의 제도권을 가져와야 하고 여기서 JVM은 fully preemptive 한 멀티 스레드 환경이 아니라는 것을 알 수 있다. 스레드를 중단시키며 조정 작업을 하기 위해 애플리케이션 스레드마다 safe point라는 특별한 실행 지점을 둔다. 그리고 safe point time 플래그를 세팅하면 모든 애플리케이션 스레드는 반드시 멈춰야 한다.


위의 그림을 보면 GC thread인 T1에서 Java thread인 T2와 T3를 멈춰야 하는 상황이다. T1 에서 T2, T3 에게 멈추라는 명령을 보낸다.
T2는 suspend_enable() 상태였기 때문에 safe_region을 true로 설정하고 스레드를 멈춘다. 그리고 T3가 멈추기를 기다린다.
T3는 suspend_enable() 상태가 아니였기 때문에 T1에게 요청을 받았을 때 바로 멈추지 못했지만 safe_point()를 만나서 멈춘다.
이렇게 모든 스레드가 멈추면 garbage collection이 일어나는 것을 볼 수 있다. 위의 예시에서는 GC thread가 Java thread에게 멈추라는 명령을 보냈지만 safe point를 전역 변수로 두고 애플리케이션 스레드들이 폴링을 통해서 safe point를 보고 스레드를 멈추기도 한다.

  • 삼색 마킹
    삼색 마킹 알고리즘을 통해서 동시성 알고리즘과 GC의 정확성을 알 수 있다. 알고리즘은 다음 순서로 이루어진다.

1. 처음에 모든 객체는 흰색으로 표시, gc 루트는 회색으로 표시한다.
2. 마킹 스레드가 회색 노드로 랜덤하게 이동하며 거쳐간 노드를 검은색으로 표시하고 해당 노드가 가리키는 노드를 모두 회색 표시한다.
3. 회색 노드가 더 이상 없을 때까지 2번 과정을 되풀이한다.
4. 회색 노드가 없어지면 검은색 노드는 살아남고 흰색 노드는 수집 대상이 된다.

이 과정에서 SATB (snapshot at the beginning)라는 기법이 적극 활용된다. 그렇기 때문에 마킹 스레드가 동작하는 중간에 노드의 정보가 수정되어 라이브 객체가 수정될 수 있다는 단점도 존재한다. 이를 위해서 새로 생긴 객체에 대해서 따로 처리하는 쓰기 배리어같은 기법이 활용되기도 한다.

위와 같은 기법들이 적용된 대표적인 GC인 CMSG1 수집기를 살펴보자!

CMS

  • 중단 시간을 아주 짧게 하려고 설계된 테뉴어드(old) 공간 전용 수집기
  • 중단 시간을 최소화하기 위해 어플리케이션 스레드 실행 중에 가급적 많은 일을 한다. 그 대신 CPU를 많이 사용하므로 애플리케이션 처리율이 감소한다.

CMS 에서 메모리를 바라보는 관점은 아래와 같다.

오라클 홈페이지에서 가져온 CMS 과정에 대한 설명이다. 애플리케이션 스레드와 동시에 일어나면서 STW이 덜 일어나긴 하지만 Initial Mark, Remark 과정에서는 STW 가 일어나는 걸 확인할 수 있다.

  • 동시 모드 실패 (CMF)
    • CMS 실행 도중 에덴 공간이 꽉 차면 조기 승격이 일어나기도 한다. → 풀 STW 를 발생시키는 Parallel GC 방식 사용

G1

  • 병렬 수집기, CMS 와는 전혀 다른 스타일의 가비지 수집기
  • RSet로 영역을 추적
  • 연속된 메모리 공간이 없으므로 부유 가비지 이슈, 조기 승격 이슈를 해결하는데 효과적이고 수집기가 실행될 때마다 전체 가비지를 수집할 필요가 없으므로 풀 STW 를 매우 줄일 수 있다.
  • CMS 보다 튜닝이 쉽다.

G1 에서 메모리를 바라보는 관점은 아래와 같다. 위에서 봤던 CMS 에서의 관점과 사뭇 다른 것을 확인할 수 있다.

마찬가지로 오라클 홈체이지에서 가져온 G1 과정에 대한 설명이다.

JIT

JVM은 인터프리터로 자바 바이트코드를 실행하고 있고 인터프리터로 해석하여 구동하는 환경은 기계어를 직접 실행하는 프로그래밍 환경인 AOT보다 성능이 떨어지기 마련이다. 그렇기 때문에 자바는 동적 컴파일 기능인 JIT를 이용해 이 문제를 해결한다.

  • AOT
    • 소스 코드에서 바로 기계어가 생성되고 컴파일 단위별로 대응되는 기계어를 어셈블리어로 바로 사용하여 속도가 빠르다.
    • 어떤 플랫폼에서 실행될 지 모르는 상태에서 컴파일되므로 보수적 선택으로 인한 CPU 낭비가 발생한다.
    • C/C++ 에서 주로 사용
  • JIT
    • JVM이 런타임 실행 정보를 수집해서 어느 부분이 자주 쓰이고 어느 부분을 최적화해야 가장 효과가 좋은지 프로파일을 만들어 결정한다. (이 과정을 PGO 라고 부름)
    • 바이트코드를 네이티브 코드로 컴파일하는 비용이 런타임에 지불되며 JIT 컴파일이 산발적으로 수행된다.
    • AOT 보다는 속도가 느리지만 여러 상황에 따라 런타임에 동적으로 최적화를 수행하여 트래픽이 갑자기 치솟는 등의 상황이 발생할 때 더 유리하게 동작한다.
    • Java 에서 주로 사용

JIT 기법들

JIT 컴파일에서는 우리가 모르는 사이 다양한 기법을 활용해서 우리 코드를 최적화하고 있다. 기법들을 확인해보자!

  • 인라이닝 (Inlining)

    • 설명: 함수 호출 시, 호출된 함수의 코드를 호출 위치에 직접 삽입하는 최적화 기법.
    • 이점: 함수 호출에 따른 오버헤드를 감소시켜 실행 속도를 향상시키며, 컴파일러가 더 효율적인 코드를 생성할 수 있음.
  • 루프 펼치기 (Loop Unrolling)

    • 설명: 루프 내의 반복문을 복제하여 반복 횟수를 줄이는 최적화 기법.
    • 이점: 루프 제어문의 오버헤드를 감소시키고, 파이프라인화와 같은 다른 최적화 기법을 통해 성능을 향상시킬 수 있음.
  • 탈출 분석 (Escape Analysis)

    • 설명: 객체가 메서드를 떠날 때까지만 사용되는지 확인하여, 해당 객체를 스택에 할당하거나 더 효율적인 메모리 구조를 사용하는 최적화 기법.
    • 이점: 불필요한 힙 할당을 피하고, 가비지 컬렉션의 부담을 감소시켜 성능을 향상시킴.
  • 단형성 디스패치 (Monomorphic Dispatch)

    • 설명: 메서드나 함수 호출 시, 단일한 타입의 객체에 대한 호출만을 처리하는 최적화 기법.
    • 이점: 다형성 관련 오버헤드를 감소시켜 실행 속도를 향상시키고, 런타임에 불필요한 타입 체크를 제거함.
  • 인트린직 (Intrinsic)

    • 설명: 특정 하드웨어 아키텍처에서 제공되는 기계어 명령어를 직접 사용하는 함수나 메서드를 가리키는 최적화 기법.
    • 이점: 특정 작업을 하드웨어 수준에서 최적화하여 성능을 극대화하며, 일반적인 함수 호출 및 루프를 피하는 데에 중점을 둠. 종종 수학적 계산이나 비트 조작과 같은 연산에서 활용됨.
  • 온-스택 치환 (On-Stack Replacement)

    • 설명: 메서드가 재귀적으로 호출될 때, 호출 스택의 프레임을 힙 메모리로 이동시켜 스택의 사용을 최적화하는 최적화 기법.
    • 이점: 호출 스택의 크기를 줄이고, 재귀적 호출에 대한 오버헤드를 감소시킴.
  • 세이프포인트 복습 (Safepoint Revisit)

    • 설명: 쓰레드가 안전 지점에 도달할 때까지 반복적으로 확인하여 쓰레드의 상태를 안정화하는 최적화 기법.
    • 이점: 가비지 컬렉션 및 다른 쓰레드와의 동기화에서 발생하는 지연을 최소화하여 성능을 향상시킴.
  • 코어 라이브러리 메서드 (Core Library Methods)

    • 설명: 특정 언어나 런타임 환경에서 기본적으로 제공하는 핵심 라이브러리의 메서드에 대한 최적화.
    • 이점: 일반적으로 사용되는 메서드에 대한 최적화를 통해 전반적인 응용 프로그램의 성능을 향상시킴.

참고 자료

profile
백엔드 개발자입니다.

1개의 댓글

comment-user-thumbnail
2024년 2월 9일

GC와 JIT에 관해서 예전에 봤던 글이 새록새록 떠오르네요
그림까지 포함해서 잘 정리하신글 편하게 읽고 갑니다.

아쉽지만 여기도 옥의티가 하나 있네요
"스레드의 제도권을" --> "스레드의 제어권을"

답글 달기