Chapter 02. JVM 이야기
Opened this issue · 5 comments
LOG-INFO commented
Chapter 02. JVM 이야기
LOG-INFO commented
정리
- JVM 인터프리터의 기본로직은
while loop 안의 switch문
- JIT(Just-In-Time) 컴파일: 기존 인터프리터만으로 한 줄 한 줄 해석하며 실행하던 구조에서, 적시(JIT)에 네이티브 코드로 변환 & 최적화를 해서 더 빠르게 실행할 수 있도록 보완하는 기능
- 트정 메서드/코드블럭이 어느 threshold를 넘어가면 Profiler가 해당 코드를 컴파일/최적화 함
- 자바를 독보적인 언어로 만들었던 특징은 바로 자동 메모리 관리 기능
- GC(Garbage Collection): 사용되지 않는(Unreachable) 객체의 Heap memory 자동 수집
- Stop-the-world(어플리케이션이 모두 중단되는 현상) 발생
- GC(Garbage Collection): 사용되지 않는(Unreachable) 객체의 Heap memory 자동 수집
- 스레딩
- 자바 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....)
- 왜 원격 디버깅은 Java Agnet가 아니라 JVMTI로 구현했을까? (C++로 구현되어있다고 함) 실행 속도 때문?
- 네이티브 언어다 보니 어플리케이션에 악영향을 끼칠 수 있음에 주의
- 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 대체
- JVM의 Attache Mechanism을 이용해 실행 프로세스를 실시간 모니터링하는 툴
추가로 알아본 내용
-
Execution Engine
-
인터프리터
vsJIT(Just-In-Time)
vsAOT(Ahead-Of-Time)
- 인터프리터 방식: 소스코드 --(javac)--> 클래스파일(자바 바이트코드) --(인터프리터)--> 실행
- 자바 바이트코드 상태로 배포하기 때문에, 특정 플랫폼에 종속되지 않음 (이식성 좋음)
- 매번 한 줄 한 줄 해석하고 실행해야해서 느림
- ex)
iload
라는 바이트 코드는 스택에 정수형 변수 값을 로드하는 명령어. Interpreter는 이 명령어를 해석하고, 해당 변수 값을 스택에 로드하는 작업을 수행
- JIT: 소스코드 --(javac)--> 클래스파일(자바 바이트코드) --(JIT Compiler)--> 네이티브 코드(기계어) --> 실행
- 프로파일링하다가 자주 사용되는 코드(HotSpot)를 찾으면 네이티브 코드로 컴파일 후 캐싱해서 성능 향상
- 인터프리터를 대체하는건 아님. 같이 사용되며 인터프리터를 보완.
- 단, 초기 실행 시간이 느림(네이티브코드 변환 & 최적화). 점점 빨라짐.
- ex) 대표적으로 HotSpot VM에서 JIT 사용
- AOT: 소스코드 -> 컴파일 -> 기계어
- 특정 플랫폼의 기계어 상태로 배포하기 때문에, 특정 플랫폼에 종속됨 (이식성 낮음)
- 초기 실행 속도도 더 빠르고 이후 실행 속도도 빠름
- JDK 9 부터 실험적으로 들어갔다가, JDK 10부터 정식으로 지원된다고 함
- 인터프리터 방식: 소스코드 --(javac)--> 클래스파일(자바 바이트코드) --(인터프리터)--> 실행
-
Flutter가 사용하는 Dart 언어는 JIT / AOT 둘 다 지원
- 개발하는 동안은 JIT를 사용해서 빠르게 테스트해보고(Hot reload/restart), 배포할 때는 AOT로 컴파일해서 배포해서 실행 시간도 빠르게 가져감(어차피 우리가 배포할 플랫폼은 거의 정해져 있음)
- 출처
-
JIT가 제공하는 최적화 (출처: ChatGPT)
- 인라이닝 (Inlining): 메소드 호출을 줄이기 위해, 자주 호출되는 메소드의 코드를 호출하는 곳에 삽입
- 루프 최적화 (Loop Optimization): 루프를 실행하는 코드를 최적화하여 루프를 더 빠르게 실행하도록 함
- 캐시화 (Caching): 자주 사용되는 값이나 객체를 캐시에 저장하여, 반복적으로 접근하는 것을 방지하고 성능을 향상
- 코드 제거 (Dead Code Elimination): 실행되지 않는 코드를 제거하여, 불필요한 작업을 줄이고 성능을 향상
- 타입 추론 (Type Inference): 변수의 타입을 추론하여, 타입 변환 작업을 줄이고 성능을 향상
- 분기 최적화 (Branch Optimization): 조건문이나 스위치문의 분기를 최적화하여, 분기 작업을 줄이고 성능을 향상
잘 몰랐던 용어
- 평가스택
- opcode: operation code(명령 코드)의 줄임말. 수행할 명령어를 나타내는 부호. 기계어의 일부.
- ex)
- 가상 호출
- JVM Intrinsics(고유체, 내장 함수): 프로세서 기능을 정밀하게 감지하는 기법
이상한 점
- p.51 -
핫스팟은 개발에 공들인 시간만도 수백 년(또는 그 이상)에 이르고, ...
-> 수백 년?.? 수십 년 이겠져??
궁금한 점
- p.53 -
공유 스레드 풀을 잉요해 전체 자바 어플리케이션 스레드를 실행하는 방안(그린 스레드)도 있지만, 쓸데없이 복잡도만 가중시킬 뿐, 만족할 만한 수준의 성능은 나오지 않는 거로 밝혀졌습니다.
- 그린 스레드라면 Project Loom에서 도입중인 개념이고, 앞으로 이게 주류가 될 것으로 기대하고 있는데, 여기서는
쓸데없이 복잡도만 가중시킬 뿐, 만족할 만한 수준의 성능은 나오지 않
았다고 말하고 있다.- Q. 이 때 벤치마킹한 자료를 찾아보자
- Q. Project Loom에서는 이러한 성능을 개선한 것인가?
- 그린 스레드라면 Project Loom에서 도입중인 개념이고, 앞으로 이게 주류가 될 것으로 기대하고 있는데, 여기서는
참고
MinJunKweon commented
정리
인터프리팅(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) 표시하지 않는 한 바뀔 수 있다.
minkukjo commented
끄적 끄적
- 인터프리터는 while 루프 안의 switch 구문이다!
- opcode란 대학생때 배운 어셈블리의 operation code를 의미함. ( 물론 어셈블리의 그것은 아니고, 머신 코드 )
- 클래스 로더.. 바이트 코드... 취준할 때 열심히 봤었는데, 이렇게 디테일하게 나올 줄이야 ㅎㅎ
- JVM은 하위호환성을 잘 지원해준다고 하는데, 전에 회사에서 JDK 15에서 레거시 (7인가 8인가) jar 실행이 안된 적 있었음 -_- ;;
- 왜 클래스 파일 구조를 기억하기 위한 암기 요령이 있는거지..? ㅋㅋㅋㅋ
- 자바 클래스를 javac로 컴파일해서 바이트 코드로 보여주는 예시가 있는데, 요즘 IDE가 좋아져서 이거 다 지원함 (...)
핫스팟 가상머신
- 그 플랫폼에서만 사용 가능한 컴파일러를 AOT 컴파일이라고 함.
- 핫스팟 VM은 시동시 CPU 타입을 정확히 감지해 특정 프로세서의 기능에 맞게 최적화를 한다(!!)고 한다. 그럼 이게 인텔인지 암드인지를 보고 최적화를 한다는 건가..? 이야.. 기술력 오졌다..
- 이런걸 JVM 인트린직이라고 부른다고 한다. ( JVM intrinsics )
@HotSpotIntrinsicCandidate
HaeUlNam commented
정리
- JVM은 스택 기반의 해석 머신: 일부 결과를 실행 스택에 보관하고, 이 스택의 맨 위에 쌓인 값들을 가져와 계산
- Stack 기반 virtual machine: JVM
- Register 기반 virtual machine: DALVIK VM
- 예전에 구글에서 JVM 대신 DALVIK을 사용한 이유는 스택 기반 모델은 많은 메모리를 요구하며 안드로이드에서는 빠른 성능을 낼 수 없어서, 적은 메모리를 사용하는 레지스터 기반 모델인 DALVIK을 사용했다고 한다. 물론 지금은 DALVIK보다 더 성능이 좋은 ART(Android Runtime)라는 걸 쓰고 있다고는 한다.
- ref. https://www.charlezz.com/?p=42686
- https://markfaction.wordpress.com/2012/07/15/stack-based-vs-register-based-virtual-machine-architecture-and-the-dalvik-vm/
- 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 모니터링과 툴링
- JMX는 JVM과 그 위에서 동작하는 Application을 제어하고 모니터링하는 강력한 범용 툴
- Java agent는 java.lang.instrument 인터페이스로 method bytecode를 조작한다. main 메서드 이전에 실행되는 premain() 메서드를 구현해야 한다. open telemetry나 pinpoint등이 Java agent로 구현된 거 같음.
- https://github.com/open-telemetry/opentelemetry-java-instrumentation
- https://www.baeldung.com/java-instrumentation
MinJunKweon commented
Zoom 회의 참가
https://us05web.zoom.us/j/83849455459?pwd=TmxOT25QOTlsaUZiT0RaaDIramlYQT09
회의 ID: 838 4945 5459
암호: WkE1f5