DevSprout/optimizing-java

Chapter 02. JVM 이야기

Opened this issue · 5 comments

Chapter 02. JVM 이야기

정리

  • JVM 인터프리터의 기본로직은 while loop 안의 switch문
  • JIT(Just-In-Time) 컴파일: 기존 인터프리터만으로 한 줄 한 줄 해석하며 실행하던 구조에서, 적시(JIT)에 네이티브 코드로 변환 & 최적화를 해서 더 빠르게 실행할 수 있도록 보완하는 기능
    • 트정 메서드/코드블럭이 어느 threshold를 넘어가면 Profiler가 해당 코드를 컴파일/최적화 함
  • 자바를 독보적인 언어로 만들었던 특징은 바로 자동 메모리 관리 기능
    • GC(Garbage Collection): 사용되지 않는(Unreachable) 객체의 Heap memory 자동 수집
      • Stop-the-world(어플리케이션이 모두 중단되는 현상) 발생
  • 스레딩
    • 자바 Application Thread(= User Thread)는 각각 정확히 하나의 전용 OS Thread(= Platform Thread)에 매핑됨
    • Green Thread(= Virtual Thread, Light-weighted Thread) 개념을 고려는 했었으나 효용성 문제로 지금까지 도입되지 않았던 듯?
  • JMX(Java Management Extensions): JVM과 그 위에서 동작하는 애플리케이션을 제어하고 모니터링하는 강력한 범용 툴
    • VisualVM에서는 JMX를 통해
  • Java Agent: Java로 작성된 툴 컴포넌트
    • ex) Pinpoint
  • JVMTI(JVM Tool Interface): C/C++같은 네이티브 컴파일 언어로 작성한 툴(을 추상화한 인터페이스)
    • 네이티브 언어다 보니 어플리케이션에 악영향을 끼칠 수 있음에 주의
      • 가급적 Java Agent로 작성하는걸 추천
    • ex) 자바 원격 디버깅 -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
      • 왜 원격 디버깅은 Java Agnet가 아니라 JVMTI로 구현했을까? (C++로 구현되어있다고 함) 실행 속도 때문?
        • 메모리 사용량과 실행 속도에 이점이 있어서라고 함 (ChatGPT said....)
  • SA(Servicability Agent): 자바 객체, 핫스팟 자료 구조 모두 표출 가능한 API와 툴을 모아놓은 것?
    • ex) jstatd (GC 관련 정보를 얻을 수 있음. VisualVM의 Visual GC 플러그인을 사용해서 원격 서버의 JVM Application을 모니터링하기 위해 필요)
  • VisualVM
    • JVM의 Attache Mechanism을 이용해 실행 프로세스를 실시간 모니터링하는 툴
      • CPU, Heap, Metaspace, Class 수, Thread 실행 상태 등을 모니터링할 수 있으며, CPU나 Memoty를 샘플링을 할 수 있음
      • JVM Args / System Properties들도 볼 수 있음
      • Heap dump/Thread dump도 뜰 수 있음 (옮기는건 알아서 옮겨야함)
      • MBeans를 통해 여러 통계정보를 볼 수도 있음
        • ex) EhCache/Memcached 캐시 히트율, Kafka 메트릭(이건 신기하긴 한데 딱히 뭐 도움될 내용은 없어보임) 등
    • jconsole 대체

추가로 알아본 내용

  • Execution Engine

    • image
    • Profiler랑 GC도 Execution Engine에 속해있었다니..!
  • 인터프리터 vs JIT(Just-In-Time) vs AOT(Ahead-Of-Time)

    • 인터프리터 방식: 소스코드 --(javac)--> 클래스파일(자바 바이트코드) --(인터프리터)--> 실행
      • 자바 바이트코드 상태로 배포하기 때문에, 특정 플랫폼에 종속되지 않음 (이식성 좋음)
      • 매번 한 줄 한 줄 해석하고 실행해야해서 느림
      • ex) iload라는 바이트 코드는 스택에 정수형 변수 값을 로드하는 명령어. Interpreter는 이 명령어를 해석하고, 해당 변수 값을 스택에 로드하는 작업을 수행
    • JIT: 소스코드 --(javac)--> 클래스파일(자바 바이트코드) --(JIT Compiler)--> 네이티브 코드(기계어) --> 실행
      • 프로파일링하다가 자주 사용되는 코드(HotSpot)를 찾으면 네이티브 코드로 컴파일 후 캐싱해서 성능 향상
      • 인터프리터를 대체하는건 아님. 같이 사용되며 인터프리터를 보완.
      • 단, 초기 실행 시간이 느림(네이티브코드 변환 & 최적화). 점점 빨라짐.
      • ex) 대표적으로 HotSpot VM에서 JIT 사용
    • AOT: 소스코드 -> 컴파일 -> 기계어
      • 특정 플랫폼의 기계어 상태로 배포하기 때문에, 특정 플랫폼에 종속됨 (이식성 낮음)
      • 초기 실행 속도도 더 빠르고 이후 실행 속도도 빠름
      • JDK 9 부터 실험적으로 들어갔다가, JDK 10부터 정식으로 지원된다고 함
  • Flutter가 사용하는 Dart 언어는 JIT / AOT 둘 다 지원

  • JIT가 제공하는 최적화 (출처: ChatGPT)

    • 인라이닝 (Inlining): 메소드 호출을 줄이기 위해, 자주 호출되는 메소드의 코드를 호출하는 곳에 삽입
    • 루프 최적화 (Loop Optimization): 루프를 실행하는 코드를 최적화하여 루프를 더 빠르게 실행하도록 함
    • 캐시화 (Caching): 자주 사용되는 값이나 객체를 캐시에 저장하여, 반복적으로 접근하는 것을 방지하고 성능을 향상
    • 코드 제거 (Dead Code Elimination): 실행되지 않는 코드를 제거하여, 불필요한 작업을 줄이고 성능을 향상
    • 타입 추론 (Type Inference): 변수의 타입을 추론하여, 타입 변환 작업을 줄이고 성능을 향상
    • 분기 최적화 (Branch Optimization): 조건문이나 스위치문의 분기를 최적화하여, 분기 작업을 줄이고 성능을 향상

잘 몰랐던 용어

  • 평가스택
  • opcode: operation code(명령 코드)의 줄임말. 수행할 명령어를 나타내는 부호. 기계어의 일부.
  • 가상 호출
  • JVM Intrinsics(고유체, 내장 함수): 프로세서 기능을 정밀하게 감지하는 기법

이상한 점

  • p.51 - 핫스팟은 개발에 공들인 시간만도 수백 년(또는 그 이상)에 이르고, ... -> 수백 년?.? 수십 년 이겠져??

궁금한 점

  • p.53 - 공유 스레드 풀을 잉요해 전체 자바 어플리케이션 스레드를 실행하는 방안(그린 스레드)도 있지만, 쓸데없이 복잡도만 가중시킬 뿐, 만족할 만한 수준의 성능은 나오지 않는 거로 밝혀졌습니다.
    • 그린 스레드라면 Project Loom에서 도입중인 개념이고, 앞으로 이게 주류가 될 것으로 기대하고 있는데, 여기서는 쓸데없이 복잡도만 가중시킬 뿐, 만족할 만한 수준의 성능은 나오지 않았다고 말하고 있다.
      • Q. 이 때 벤치마킹한 자료를 찾아보자
      • Q. Project Loom에서는 이러한 성능을 개선한 것인가?

참고

정리

인터프리팅(Interpreting)

  • JVM은 스택 기반의 해석 머신
  • 결과값들을 스택에 저장하고 맨 위에서부터 읽어서 처리함
  • 인터프리터는 스택을 사용해 중간값들을 담아두고 가장 마지막에 실행된 명령어와 독립적으로 프로그램을 구성하는 명령코드(Opcode) 를 하나씩 순서대로 처리함
  • ‘while 루프 안의 switch문’처럼 동작한다고 생각하면 됨 (실제론 더 복잡함)

클래스로딩(Classloading)

  • JVM은 OS 위에서 동작하고, 사용자가 작성한 클래스는 JVM이 실행함
  • 사용자가 작성한 클래스로 제어권(Control)을 넘기려면 EntryPoint가 필요함
  • 자바에서 EntryPoint는 클래스에 정의된 main() 클래스 메소드
  • 그러므로 클래스를 로딩할 필요가 있음. 이때 관여하는 것이 클래스로딩 메커니즘
  • 자바 프로세스가 초기화되면 연결된 클래스로더가 차례차례 동작함
  • 부트스트랩 클래스가 로딩되고 Java Runtime Core 클래스를 로딩함. 최소한의 필수 클래스들만 로드함.
  • 그 다음 확장 클래스로더가 생김. 확장 클래스로더가 하는 일:
    • 특정 OS나 플랫폼 네이티브 코드를 제공
    • 기본 환경을 오버라이드(재정의) 가능
  • 확장 클래스로더는 필요할 때 부트스트랩 클래스로더에 로딩 작업을 넘김
  • 마지막으로, 애플리케이션 클래스로더가 생김
    • 클래스패스에 위치한 유저 클래스를 로드함
  • 자바 프로그램 실행 중 처음 보는 새 클래스를 디펜던시(Dependency) 에 로드함
  • 클래스로더들이 전부 못찾는 클래스는 ClassNotFoundException이 발생함
  • 똑같은 클래스를 두 개의 클래스로더가 로드할 가능성도 있음
    • 어떻게 가능한가? 풀 클래스명과 로드한 클래스로더, 두 개를 한쌍으로 식별됨

바이트코드(Bytecode) 실행

  • 자바 코드는 javac에 의해 바이트코드로 가득 찬 .class 파일로 생성됨
  • 최적화는 거의 하지 않기 때문에 그 결과로 생성된 바이트코드는 쉽게 해독할 수 있음
  • 바이트코드는 특정 컴퓨터 아키텍처에 특정하지 않은 Intermediate Representation
  • 클래스 파일에는 포맷 버전이 있어서 JVM이 실행할 수 있는 클래스인지 호환성 여부를 판단 가능
    • 만약 버전이 호환이 안되면 UnsupportedClassVersionError 예외 발생
  • 클래스파일 구조 : https://blog.lse.epita.fr//2014/04/28/0xcafebabe-java-class-file-format-an-overview.html
  • 바이트코드는 Opcode로 이루어짐

핫스팟(HotSpot) JVM

  • 언어 및 플랫폼 설계 과정에서 생산성과 퍼포먼스 간의 Trade-Off를 계속 고민함
    • 편의성을 제공하면 부가적인 것을 함께 제공해서 오버헤드 발생
    • 성능을 선택하면, 저수준의 명령까지 기계에 일러 줘야해서 생산성이 덜어짐
  • C++은 제로-오버헤드 원칙을 준수하지만 Java는 그렇지 않음.
  • 중간 코드(Bytecode)를 생성하여 쓰기 때문에 이식성은 좋아지지만 제로-오버헤드는 아니게 됨
  • 하지만, Java Hotspot JVM은 프로그램의 런타임 동작을 분석하고 성능을 최적화하는 방식

JIT 컴파일

  • 성능향상을 위해 자주 사용되는 바이트코드를 네이티브 코드로 컴파일하는 방식
  • Interpreted 모드로 실행되는 동안 애플리케이션을 모니터링해서 자주 호출되는 코드를 발견해 JIT를 수행함
  • JIT는 실행되는 동안 수집한 정보로 최적화를 결정한다는 것이 가장 큰 장점
  • JIT 컴파일을 거친 코드는 처음 작성한 코드와 전혀 딴판일 가능성이 큼

JVM 메모리 관리

  • 여타 다른 프로그래밍 언어(C, C++, Objective-C 등)들은 개발자가 직접 메모리 할당/해제를 수행
  • 그만큼 개발자가 메모리를 정확하게 계산해서 처리해야하는 책임이 수반됨
  • 하지만, 대부분의 개발자들이 메모리 관리 용어나 패턴을 모름
  • 자바 초창기에도 메모리 관리를 부실하게 해서 애플리케이션이 에러가 나는 일이 매우 많았음
  • 자바는 GC(Garbage Collection)이라는 프로세스를 이용해 힙 메모리를 자동 관리함
  • 가비지 컬렉션은 메모리 할당과 회수를 예측하기 어려움 (Nondeterministic)

스레딩과 자바 메모리 모델(JMM)

  • 자바는 멀티 스레딩으로 동작함
  • 메이저 JVM들은 애플리케이션에서 쓰는 스레드가 각각 OS의 전용 스레드로 대응됨
  • 공유 스레드 풀을 사용해서 전체 애플리케이션 스레드를 사용하는 그린 스레딩이라는 기법도 있지만 성능이 나빠서 사용하지 않음
  • 자바 멀티 스레드 기본 설계 원칙
    • 자바 프로세스의 모든 스레드는 가비지가 수집되는 하나의 공용 힙을 가진다
    • 한 스레드가 생성한 객체는 그 객체를 참조하는 다른 스레드가 액세스할 수 있다
    • 기본적으로 객체는 변경 가능하다. 즉, final로 불변(Immutable) 표시하지 않는 한 바뀔 수 있다.

끄적 끄적

  • 인터프리터는 while 루프 안의 switch 구문이다!

image

  • opcode란 대학생때 배운 어셈블리의 operation code를 의미함. ( 물론 어셈블리의 그것은 아니고, 머신 코드 )
  • 클래스 로더.. 바이트 코드... 취준할 때 열심히 봤었는데, 이렇게 디테일하게 나올 줄이야 ㅎㅎ
  • JVM은 하위호환성을 잘 지원해준다고 하는데, 전에 회사에서 JDK 15에서 레거시 (7인가 8인가) jar 실행이 안된 적 있었음 -_- ;;
  • 왜 클래스 파일 구조를 기억하기 위한 암기 요령이 있는거지..? ㅋㅋㅋㅋ
  • 자바 클래스를 javac로 컴파일해서 바이트 코드로 보여주는 예시가 있는데, 요즘 IDE가 좋아져서 이거 다 지원함 (...)

핫스팟 가상머신

image

  • 그 플랫폼에서만 사용 가능한 컴파일러를 AOT 컴파일이라고 함.
  • 핫스팟 VM은 시동시 CPU 타입을 정확히 감지해 특정 프로세서의 기능에 맞게 최적화를 한다(!!)고 한다. 그럼 이게 인텔인지 암드인지를 보고 최적화를 한다는 건가..? 이야.. 기술력 오졌다..
  • 이런걸 JVM 인트린직이라고 부른다고 한다. ( JVM intrinsics )
  • @HotSpotIntrinsicCandidate

정리

  • JVM은 스택 기반의 해석 머신: 일부 결과를 실행 스택에 보관하고, 이 스택의 맨 위에 쌓인 값들을 가져와 계산
  • Java sourecode 실행 과정: javac로 컴파일 → .class 바이트 코드 → Class loader → Bytecode interpreter 실행 & JIT compile
    • .class 바이트코드는 javap 같은 표준 역어셈블리 툴로 자바 코드를 볼 수 있음.
    • JIT compile: 자주 실행되는 코드 파트를 발견해 profiler가 특정 코드 섹션을 컴파일/최적화
    • AOT(Ahead-Of-Time compile)는 컴파일 타임에 최적화를 하고, 특정 아키텍처를 타겟으로 하면 효과적 vs JIT(Just In Time) 컴파일은 런타임에 최적화, 런타임에 유용한 데이터 활용 가능 및 확장성. 9장/10장에 더 자세히 나올 듯
  • Thread와 JMM
    • 멀티 스레드 기반이고, 자바 Application thread는 정확히 하나의 OS thread와 대응됨.
    • 스레드 3가지 원칙
      • 자바 프로세스의 모든 스레드는 가비지가 수집되는 하나의 공용 힙을 가진다
      • 한 스레드가 생성한 객체는 그 객체를 참조하는 다른 스레드에 액세스할 수 있다
      • final이지 않는 이상 기본적으로 mutable.
  • JVM 모니터링과 툴링

Zoom 회의 참가
https://us05web.zoom.us/j/83849455459?pwd=TmxOT25QOTlsaUZiT0RaaDIramlYQT09

회의 ID: 838 4945 5459
암호: WkE1f5