Search
Duplicate
🍁

JVM 이야기 (Overview of the JVM)

태그
Java
공개여부
작성일자
2021/08/01
1.
이 챕터는 다른 챕터를 이해하기 위해 JVM이 자바 코드를 실행하는 방법을 소개한다.
2.
성능에 관심이 있는 개발자라면 JVM 기술 스택의 구조를 이해해야 한다
3.
JVM 기술을 이해하면 더 좋은 소프트웨어를 개발할 수 있고 성능 이슈를 탐구할 때 필요한 이론적 지식을 갖는다.

인터프리팅과 클래스로딩 (Interpreting and Classloading)

JVM은 스택 기반의 해석 머신이다.
register 는 없지만 일부 결과를 실행 스택에 보관하며, 이 스택의 가장 위에 쌓인 값을 가져와 계산한다.
JVM interpreter 는 while문 안에 switch 문 으로 이해할 수 있다.
while(조건) { switch(대상) { case 1: case 2: default: } }
Java
open JDK 나 oracle JDK 는 물론 이것보다 심오하지만, 이 개념에서 접근하면 이해하기 쉽다.

java HelloWorld 라는 명령을 실행하면

1.
OS는 가상 머신 프로세스(java binary)를 구동한다.
2.
자바 가상 환경이 구성된다.
3.
stack 머신이 초기화 된다.
4.
HelloWorld 클래스가 실행된다.
여기서 application 의 진입은 HelloWorld.classmain() method 이다.
제어권을 이 클래스로 넘기기 위해 가상 머신(이하 VM) 이 실행되기 전에 이 클래스를 load 해야한다.

classloading 매커니즘

자바 process 가 초기화되면 사슬처럼 연결된 클래스 로더가 차례차례 작동한다.
1.
bootstrap class 실행
다른 클래스로더가 나머지 시스템에 필요한 class 를 로드할 수 있게 최소한의 필수 클래스만 로드한다.
java.lang.Object, Class, Cloassload 가 이에 해당함
2.
java ≤ 8 은 rt.jar 에서 runtime core class 를 로드한다.
java > 9 는 runtime 이 모듈화 되고 클래스 로딩 개념 자체가 달라졌다.
3.
확장 클래스 로더(The Extension classloader)
OS나 플랫폼에 native code 를 제공하고 기본 환경을 overriding 하는데 사용된다.
bootstrap 을 부모로 인식하여 필요할 때 부모로 클래스 로딩 작업을 넘긴다.
4.
끝으로 application class loader 가 생성된다.
classpath 에 위치한 user class 를 로드한다.
종종 이 클래스 로더를 system class load 라고 부르기도 하는데 이는 system 에 관련된 클래스를 로드하지 않기 때문에 사용하지 않는 것을 권장한다.
💡
이는 모두 상속 관계이다. bootstrap (최상단 부모) → extension classload → application classloader (최하단 자식)
java program 을 실행하다가 새 클래스를 발견하여 찾지 못하면 한 단계씩 상위로 올려 lookup 을 하고,
만약 찾지 못한다면 ClassNotFoundException 을 일으킨다.
java 의 클래스를 로드하는 것은 runtime 에서 해당 클래스를 나타내는 Class 객체를 만들어 내는데 이것은 runtime 에서 결정되기 때문에 상이한 클래스를 두번 load 할 수 있어 주의가 필요하다
클래스의 식별: 패키지명을 포함하여 full class 이름 + 자신을 로드한 classloader 두가지 정보로 식별한다.

바이트코드 실행 (Executing Bytecode)

class HelloWorld { public static void main(String[] args) { System.out.println("Hello World!"); System.out.println(3+4); } }
Java
HelloWorld.java
이 코드를 javac HelloWorld.java 로 실행하면 다음과 같이 HelloWorld.class 로 compile 된 파일이 생성된다..
˛∫æ7 <init>()VCodeLineNumberTablemain([Ljava/lang/String;)V SourceFileHelloWorld.java Hello World! HelloWorldjava/lang/Objectjava/lang/SystemoutLjava/io/PrintStream;java/io/PrintStreamprintln(Ljava/lang/String;)V(I)V *∑± 1≤∂≤∂±
Java
컴파일하여 생성된 bytecode HelloWorld.class
vim 에서 긁어오지 않고 cat 으로 찍어본 결과
이렇게 생성된 bytecode 는 별도의 최적화 작업이 수행되지 않기 때문에 해독이 쉽다(컴퓨터 입장에서)
java class file compilation

Bytecode

bytecode 는 컴퓨터의 아키텍쳐에 특정되지 않는 중간 표현형(Intermediate Representation, IR) 이다.
따라서 이식성이 좋고, JVM 이 지원되는 플랫폼 어디서든 실행할 수 있으며, java 에 대해서 추상화 되어있다.
JVM 이 코드를 실행하는 원리를 이해하는데 중요하다
💡
JVM 에서 J 는 오해의 소지가 있는 naming 이다. JVM 규격에 맞춰 만들어진 모든 언어가 JVM 에서 실행될 수 있다.(scala, kotlin)
컴파일러가 생성한 .class 파일은 VM spec 에 명확히 정의된 구조를 갖추고 있다.
클래스 파일 해부도 (spec)
component
설명
클래스 파일 format version
Open
class 파일의 major / minor 버젼
상수 풀(constant pool)
Open
class 파일의 상수가 모여있다
access flag
Open
class modifiers
this class
Open
현재 클래스의 이름
super class
Open
부모 클래스의 이름
interface
Open
현재 클래스가 implements 한 모든 interface
field
Open
클래스에 들어가 있는 모든 필드
method
Open
클래스에 들어가 있는 모든 methods
attribute
Open
클래스의 속성(소스 파일 이름 등)
COUNT10
매직 넘버: 0xCAFEBABE 으로 고정인데 이 값으로 시작해야 java 파일임을 컴퓨터가 인식한다
압축파일이나 여러 파일들은 이러한 magic number 를 모두 갖고있다
access flag: class 에 적용한 modifier, class 의 종류도 의미한다.
modifier
public, protected, default, private
static, final, abstract, native, transient, synchronized, volatile, strictfp
class 종류
interface, annotation, enum 등등
암기요령

클래스를 컴파일 해보자

class HelloWorld2 { public static void main(String[] args) { for (int i = 0; i < 10; i++) { System.out.println("Hello World"); } } }
Java
이러한 파일을 컴파일 하면 다음과 같은 결과가 나온다
˛∫æ7 <init>()VCodeLineNumberTablemain([Ljava/lang/String;)VStackMapTable SourceFileHelloWorld2.javaHello WorldHelloWorld2java/lang/Objectjava/lang/SystemoutLjava/io/PrintStream;java/io/PrintStreamprintln(Ljava/lang/String;)V *∑± J< ¢≤∂ÑߡԱ ¸˙
Java
이것을 javap -c 로 해석해보면 다음과 같다
Compiled from "HelloWorld2.java" class HelloWorld2 { HelloWorld2(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: iconst_0 1: istore_1 2: iload_1 3: iload_1 10 5: if_icmpge 22 8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 11: ldc #3 // String Hello World 13: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 16: iinc 1, 1 19: goto 2 22: return }
Java
이미지로 보기
더 자세한 버젼 (헤더 전체 정보, 상수 풀 세부 정보 등등)
위 코드를 보면 HelloWorld2()main() 두 개의 method 가 생성되는데 왜 그럴까?

명령의 해석

aload_0: this 의 레퍼런스를 stack 상단에 올려두는 명령
invokespecial: super 생성자를 호출하고 객체를 생성하는 method 를 실행한다.
default constructor 를 override 하지 않았으므로 Object 의 default constructor 매치
main method 를 해석해보자
iconst_0: 정수형 상수 0을 평가 stack 에 push 한다
istore_1: 상수값을 오프센 1에 위치한 지역변수(for loop 의 i)에 저장한다
0 부터 시작한다
instance method 에서 0번째 entry 는 this 이다
iload_1: 오프센 1의 변수를 스택에 로드한다
iload_1: for 문에서 i < 10 이다. 상수 10을 푸쉬한다
if_icmpge: i < 10 에 해당하는지 비교하는데 사용한다
getstatic: System.out static method 를 해석한다
ldc: 상수 "Hello World" 문자열을 로드한다
invokevirtual: instance method 실행
iinc(1, 1): 정수값을 하나 증가한다
goto : 2번으로 보낸다
if_icmpge 가 성공할 때 까지 반복하다가 마지막 22번 return 을 실행하고 제어권이 넘어간다

핫스팟 입문 (Introducing HotSpot)

1999년 4월 자바 성능의 가장 큰 변화를 가져온 요체가 hotspot 이다.
핫스팟 JVM
zero cost(비용이 들지 않는) 추상화 사상에 근거하여 '기계에 가까운' 언어와 그리고 개발자의 생산성을 염두한 엄격한 저수준 제어 이 두가지 사이에서 언어는 갈등을 겪고 있다.
C++ 코드는 제로-오버헤드 원칙을 준수합니다 사용하지 않는 것에는 대가를 치르지 않습니다 즉, 여러분이 사용하는 코드보다 더 나은 코드를 건네줄 수는 없습니다
C++ 은 이러한 철학을 가지고 기본적인 라이브러리도 제공하지 않아 굉장히 가볍게 수행할 수 있지만, 언어의 아주 세세한 저수준까지 개발자가 이해하고 다뤄야 한다. batch 와 같이 성능 자체에 중점을 두는 경우가 아니라면 엄청난 학습 부담이 된다.
C++ 과 같은 언어는 platform 즉 OS 에 영향을 받기 때문에 컴파일한 플랫폼에서만 실행 가능하다.
이것을 AOP(Ahead-of-time: 사전 컴파일) 이라하고 하드웨어와 OS에 최적화 되어 실행된다.
이식성이 있다는 것은 어떻게 보면 쓸데없는 여러 코드가 함께 들어가 있음을 의미한다.
Java 는 이러한 zero-overhead 추상화 원칙을 동조하지 않는다.
hotspot은 runtime 에 동작을 분석하고, 성능에 가장 유리한 방향으로 그때그때 최적화를 적용하는 가상머신이다. (상당히 똑똑함)
즉, 개발자가 VM 틀에 맞게 욱여넣는 대신, 자바 코드로 작성하고 바람직한 설계 원리를 따르도록 한다.

JIT 컴파일이란? (Introducing Just-in-Time Compilation)

자바 프로그램은 bytecoe interpreter 가 가상화한 stack 머신에서 실행하며 시작된다.
CPU 를 추상화 한 구조이기 때문에 다른 platform 에서 class 파일을 문제 없이 실행할 수 있찌만, 성능을 최대로 내려면 native 기능을 활용해 CPU 에서 직접 실행시켜야 한다.
hotspot 은 이를 위해 interpreted bytecode 에서 native 코드로 컴파일 하는데 이것을 JIT(Just-In-Time) 이라 한다. 애플리케이션을 모니터링하면서 자주 실행되는 코드 파트를 찾아내 JIT 컴파일을 수행한다.
이러한 코드는 최적화된 코드이다.
컴파일러가 해석 단계에서 수집한 정보를 근거로 최적화를 결정한다.
따라서 최신 성능 최적화의 덕을 보려면 hotspot 최신 버젼에서 application을 실행하는 것이 좋다.
java는 profile 기반 최적화(Profile-guided optimization PGO)를 응용하는 환경에서 대부분의 AOT 로 불가능한 방식을 선택해 runtime 정보를 활용하며, CPU 타입을 정확히 감지해 해당 프로세서의 기능에 맞게 최적화한다.
9, 10 장에서 이것은 자세히 다룬다
hotspot 이 자바를 독보적으로 만들었던 이유는 자동 메모리 관리 기능이다(Automatic memory management)

JVM 메모리 관리 (JVM Memory Management)

C, C++ 은 메모리를 개발자가 직접 관리하는데, 이 책임이 개발자에게 오면 막중환 책임이 수반된다.
자바는 Garbage Collection(이하 GC) 라는 프로세를 이용해 heap memory 를 자동으로 관리한다.
GC는 JVM이 더 많은 메모리를 할당해야 할 때 불필요한 메모리를 회수하거나 재사용하는 불확정적 프로세스이다.
GC가 실행되면 그 동안 다른 애플리케이션은 모두 중단되고 하던일을 멈춰야 하는데 이것은 애플리케이션의 부하가 늘어날 수록 무시할 수 없는 시간이 된다.

threading 과 자바 메모리 모델(Threading and the Java Memory Model JMM)

java 는 1.0 부터 multi-thread 프로그래밍을 지원하였고 다음과 같이 실행 thread 를 새로 만들 수 있다
Thread t = new Thread(() -> { System.out.println("Hello World"); });
Java
java 환경 자체가 JVM 처럼 multi-thread 기반이기 때문에 java 프로그램이 작동하는 방식은 어쩔 수 없이 한층 더 복잡하고 성능 분석도 어렵다.
주류 JVM 구현체에서 java application thread 는 각각 하나의 전용 OS thread 에 대응한다.
공유 thread pool 을 이용해 전체 자바 application thread 를 실행하는(green thread) 방안도 있지만 복잡도만 증가시킬 뿐 성능적인 측면에서 이득이 없는것으로 밝혀졌다.
1990년대 후반부터 java multi-thread 방식은 다음의 3가지 설계 원칙에 기반한다.
java process의 모든 thread는 GC되는 하나의 공용 heap 을 갖는다
한 thread 가 생성한 객체는 그 객체를 참조하는 다른 thread 가 access 할 수 있다.
기본적으로 객체는 변경이 가능하다.(final 이 붙은 클래스가 아니라면)
exclusive lock 에 대한 설명도 나오지만 이는 12장에서 자세히 다룬다.

JVM 구현체 종류

openJDK
자바 기준 구현체를 제공하는 특별한 open source 이다.
오라클이 직접 주관/지원하며 java release 기준을 발표한다
oracle Java
가장 널리 알려진 구현체로 OpenJDK 기반이지만, 오라클의 상용 라이센스이다.

JVM 라이센스

생략

JVM 모니터링과 툴링 (Monitoring and Tooling for the JVM)

JVM 은 성숙한 platform 으로 실행중인 application 을 instrumentation, 모니터링, 관측을 위한 다양한 기술을 제공한다.
JMX(Java management Extension)
JVM과 그 위에서 동작하는 애플리케이션을 제어하고 모니터링 하는 강력한 범용 툴이다.
method 를 호출하고 매개변수를 변경할 수 있다.
JVM 을 관리하는 기본 수단이다
프로메테우스와 같은 곳에서 사용한다
Java agent
java 언어로 작성된 tool component 이다
java.lang.instrument interface 로 method bytecode 를 조작한다
-javaagent: <agent jar 경로>=<옵션>
Java
java agent 실행 방법
agent jar 에 manifest 가 필수이다.
등록 hook 인 public static premain() 구현이 필요하다
JVM tool interface(JVMTI)
java instrument API 로 부족할 때 사용한다
C/C++ 과 같은 native compile 언어로 작성해야 한다
설치하는 플래그는 다음과 같다
-agentlib:<라이브러리명>=<옵션>
Java
-agentpath:<에이전트 경로>=<옵션>
Java
되도록 JVMTI 보다 java agent 를 사용하자
Serviceability Agent(SA)
java 객체, hotspot 자료구조 모두 표출이 가능한 API 와 툴을 모아두었다.
symbol lookup 과 같은 기본형을 이용하거나
process memory 를 읽는 방식으로 디버깅한다
core file(crash dump) 및 생생한 자바 프로세스까지 디버깅 할 수 있다.

VisualVM

JDK 에 유용한 가외 툴이 많은데 netbeans 의 VisualVM 은 모르고 지나치는 툴 중에 하나이다.
VisualVM 모니터 화면
Attach machanism을 이용해 실행 프로세스를 실시간으로 모니터링 한다.
프로세스가 local, remote 에 따라 작동 방식이 다르다
다음의 모니터링을 제공한다
개요(java process 요약)
모니터: CPU, heap 사용량 등 JVM 을 고수준에서 원격 측정한 값 제공
thread: 시간대별로 애플리케이션의 각 thread 를 표시한다
thread dump 를 뜰 수도 있다.
sampler and profiler 13장에서 자세히 다룬다
TOP