JVM Overview
Java는 전 세계에서 가장 큰 기술 플랫폼 중 하나로, 약 900만에서 1000만 명의 개발자를 보유하고 있는 것은 의심의 여지가 없습니다. 설계상 많은 개발자들이 자신이 사용하는 플랫폼의 저수준 복잡성에 대해 알 필요가 없습니다.
그러나 성능에 관심이 있는 개발자들에게는 JVM 기술 스택의 기본을 이해하는 것이 중요합니다. JVM 기술을 이해하면 개발자는 더 나은 소프트웨어를 작성할 수 있으며, 성능 관련 문제를 조사하는 데 필요한 이론적 배경을 제공합니다.
인터프리팅과 클래스 로딩
Java 가상 머신을 정의하는 명세서에 따르면, JVM은 스택 기반의 인터프리터 기계입니다. 이는 물리적 하드웨어 CPU와 같은 레지스터 대신, 부분 결과를 저장하는 실행 스택을 사용하며, 그 스택의 최상위 값(또는 값들)을 조작하여 계산을 수행함을 의미합니다.
JVM 인터프리터의 기본 동작은 프로그램의 각 opcode를 마지막 것과 독립적으로 처리하며, 평가 스택을 사용하여 중간 값을 저장하는 "while 루프 내의 스위치"로 생각할 수 있습니다.
바이트코드 실행
Java 소스 코드는 실행 전에 상당한 수의 변환 과정을 거친다는 점을 이해하는 것이 중요합니다.
첫 번째 단계는 종종 더 큰 빌드 프로세스의 일부로 호출되는 Java 컴파일러 javac
를 사용한 컴파일 단계입니다.
javac
의 역할은 Java 코드를 바이트코드를 포함하는 .class
파일로 변환하는 것입니다.
이는 Java 소스 코드를 비교적 직접적으로 번역하여 이루어집니다.
컴파일 중에 javac
는 거의 최적화를 수행하지 않으며, 생성된 바이트코드는 표준 javap
과 같은 디스어셈블리 도구로 볼 때 여전히 상당히 읽기 쉽고 Java 코드로 인식할 수 있습니다.
바이트코드는 특정 기계 아키텍처에 종속되지 않은 중간 표현입니다. 기계 아키텍처로부터 분리함으로써 포터블리티를 제공하며, 이는 이미 개발된(또는 컴파일된) 소프트웨어가 JVM이 지원하는 모든 플랫폼에서 실행될 수 있음을 의미합니다. 또한 Java 언어로부터의 추상화를 제공합니다. 이는 JVM이 코드를 실행하는 방식을 이해하는 데 첫 번째 중요한 통찰력을 제공합니다.
구성 요소
설명
매직 넘버(Magic number)
0xCAFEBABE
클래스 파일 형식 버전
클래스 파일을 컴파일하는 데 사용된 마이너 및 메이저 버전
상수 풀(Constant pool)
클래스의 상수 풀
액세스 플래그(Access flags)
클래스가 추상적인지, 정적인지 등의 수정자를 결정
이 클래스(This class)
현재 클래스의 이름
슈퍼 클래스(Superclass)
슈퍼 클래스의 이름
인터페이스(Interfaces)
클래스의 인터페이스
필드(Fields)
클래스의 필드
메서드(Methods)
클래스의 메서드
속성(Attributes)
클래스의 속성 (예: 소스 파일 이름 등)
모든 클래스 파일은 매직 넘버 0xCAFEBABE
로 시작하며, 이는 클래스 파일 형식에 대한 준수를 나타내는 첫 번째 4바이트입니다.
다음 4바이트는 클래스 파일을 컴파일하는 데 사용된 마이너 및 메이저 버전을 나타내며, 이는 대상 JVM이 클래스 파일을 컴파일한 버전보다 낮지 않은지 확인하기 위해 검사됩니다.
마이너 및 메이저 버전은 클래스 로더에 의해 호환성이 있는지 확인하기 위해 검사되며, 호환되지 않을 경우 런타임에 UnsupportedClassVersionError
가 발생하여 실행 중인 JVM이 컴파일된 클래스 파일보다 낮은 버전임을 나타냅니다.
public class HelloWorld {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
System.out.println("Hello World");
}
}
}
Java에는 .class
파일을 검사할 수 있는 javap
라는 클래스 파일 디스어셈블러가 함께 제공됩니다.
HelloWorld.class
파일을 가져와 javap -c HelloWorld
명령을 실행하면 다음과 같은 출력이 나옵니다
public class HelloWorld {
public HelloWorld();
Code:
0: aload_0
1: invokespecial #1 <init>":()V
4: return
// Method java/lang/Object.<init>():V
public static void main(java.lang.String[]);
Code:
0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmplt 8
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
}
이 레이아웃은 HelloWorld.class
파일의 바이트코드를 설명합니다.
더 자세한 내용은 javap
에 -v
옵션을 추가하여 전체 클래스 파일 헤더 정보와 상수 풀 세부 사항을 제공할 수 있습니다.
클래스 파일에는 두 개의 메서드가 포함되어 있지만, 소스 파일에 제공된 단일 main()
메서드만 포함되어 있습니다.
생성자에서 실행되는 첫 번째 명령어는 aload_0
으로, 이는 this
참조를 스택의 첫 번째 위치에 놓습니다.
그 다음 invokespecial
명령어가 호출되는데, 이는 슈퍼 생성자를 호출하거나 객체를 생성하는 특정한 인스턴스 메서드를 호출합니다.
기본 생성자에서는 호출이 Object
의 기본 생성자와 일치합니다. 이는 오버라이드가 제공되지 않았기 때문입니다.
HotSpot 소개
1999년 4월, Sun은 성능 측면에서 Java에 가장 큰 변화를 가져온 것 중 하나인 HotSpot 가상 머신을 도입했습니다. HotSpot 가상 머신은 C 및 C++와 같은 언어와 비교할 때 동등하거나 더 나은 성능을 가능하게 하는 Java의 핵심 기능입니다
언어 및 플랫폼 설계는 종종 원하는 기능과 관련된 결정을 내리고 트레이드오프를 하는 것을 포함합니다. 이 경우, "제로-오버헤드 추상화(zero-cost abstractions)"와 같은 아이디어에 의존하는 언어와, 개발자 생산성과 "일을 끝내는 것(getting things done)"을 우선시하는 언어 간의 분할이 이루어집니다.
—Bjarne Stroustrup
제로 오버헤드 원칙은 이론상으로는 훌륭하지만, 이는 언어 사용자 모두가 운영 체제와 컴퓨터가 실제로 작동하는 저수준 현실을 다루어야 함을 요구합니다. 이는 원시 성능을 주요 목표로 하지 않는 개발자들에게 큰 추가 인지적 부담을 줍니다.
뿐만 아니라, 소스 코드를 빌드 시점에 특정 플랫폼의 기계 코드로 컴파일해야 한다는 점도 문제입니다—이는 보통 Ahead-of-Time (AOT) 컴파일이라고 합니다. 이는 인터프리터, 가상 머신 및 포터블리티 레이어와 같은 대체 실행 모델이 제로 오버헤드를 제공하지 않기 때문입니다.
또한 "사용하는 것은 자동화된 시스템보다 더 잘 손으로 코딩할 수 없다"는 문구는 여러 가지 문제를 내포하고 있습니다. 이는 개발자가 자동화된 시스템(예: 컴파일러)보다 더 나은 코드를 생성할 수 있다는 것을 전제로 합니다. 이는 안전한 가정이 아닙니다. 대부분의 사람들은 더 이상 어셈블리 언어로 코딩하고 싶어하지 않으므로, 자동화된 시스템(예: 컴파일러)을 사용하는 것은 분명히 대부분의 프로그래머에게 이점이 있습니다.
Java는 제로 오버헤드 추상화 철학을 따르지 않았습니다. 대신, HotSpot 가상 머신이 취한 접근 방식은 프로그램의 런타임 동작을 분석하고 성능에 가장 큰 이점을 제공할 수 있는 곳에 지능적으로 최적화를 적용하는 것입니다. HotSpot VM의 목표는 JVM에 맞추기 위해 프로그램을 왜곡하는 대신, 관용적인 Java를 작성하고 좋은 설계 원칙을 따를 수 있도록 하는 것입니다.
Just-in-Time 컴파일 소개
Java 프로그램은 바이트코드 인터프리터에서 실행을 시작하며, 여기서 명령어가 가상화된 스택 머신에서 수행됩니다. CPU로부터의 추상화는 클래스 파일의 포터블리티 이점을 제공하지만, 최대 성능을 얻으려면 프로그램이 CPU에서 직접 실행되어야 하며, CPU의 네이티브 기능을 활용해야 합니다.
HotSpot은 이를 달성하기 위해 프로그램의 단위를 인터프리터된 바이트코드에서 네이티브 코드로 컴파일합니다. HotSpot VM에서 컴파일 단위는 메서드와 루프입니다. 이는 Just-in-Time (JIT) 컴파일이라고 합니다.
JIT 컴파일은 애플리케이션이 인터프리터 모드에서 실행되는 동안 애플리케이션을 모니터링하고 가장 자주 실행되는 코드 부분을 관찰하여 작동합니다. 이 분석 과정에서 더 정교한 최적화를 가능하게 하는 프로그램적 추적 정보가 캡처됩니다. 특정 메서드의 실행이 임계값을 초과하면, 프로파일러는 해당 코드 섹션을 컴파일하고 최적화하려고 합니다.
JIT 접근 방식에는 많은 이점이 있지만, 주요 이점 중 하나는 인터프리터 단계에서 수집된 추적 정보를 기반으로 컴파일러 최적화 결정을 내릴 수 있다는 것입니다. 이는 HotSpot이 더 정보에 입각한 최적화를 수행할 수 있게 합니다.
뿐만 아니라, HotSpot은 수백 년 이상의 공학적 개발 시간이 축적되었으며, 거의 모든 새로운 릴리스마다 새로운 최적화와 이점이 추가되고 있습니다. 이는 HotSpot의 최신 릴리스에서 실행되는 모든 Java 애플리케이션이 VM에 존재하는 새로운 성능 최적화를 활용할 수 있음을 의미하며, 재컴파일할 필요도 없습니다.
JVM 메모리 관리
C, C++, Objective-C와 같은 언어에서는 프로그래머가 메모리 할당과 해제를 관리할 책임이 있습니다. 메모리와 객체의 수명을 직접 관리하는 이점은 더 예측 가능한 성능과 객체 생성 및 삭제에 자원 수명을 묶을 수 있다는 점입니다. 그러나 이러한 이점은 큰 비용을 수반합니다—정확하게 메모리를 계산할 수 있어야 하며, 이는 프로그래머에게 큰 부담을 줍니다.
불행히도, 수십 년간의 실무 경험을 통해 많은 개발자들이 메모리 관리에 대한 관용구와 패턴을 제대로 이해하지 못한다는 것이 드러났습니다. 이후 C++와 Objective-C의 최신 버전은 표준 라이브러리에서 스마트 포인터 관용구를 사용하여 이를 개선했습니다. 그러나 Java가 만들어질 당시에는 메모리 관리가 애플리케이션 오류의 주요 원인이었습니다. 이는 개발자와 관리자들 사이에서 비즈니스 가치를 제공하는 대신 언어 기능을 다루는 데 많은 시간을 소비하는 것에 대한 우려를 불러일으켰습니다.
Java는 가비지 컬렉션(GC)으로 알려진 프로세스를 사용하여 자동으로 관리되는 힙 메모리를 도입함으로써 문제를 해결하려고 했습니다. 간단히 말해, 가비지 컬렉션은 JVM이 할당을 위해 더 많은 메모리가 필요할 때 더 이상 필요하지 않은 메모리를 회수하고 재사용하기 위해 트리거되는 비결정론적 프로세스입니다.
그러나 GC에 대한 이야기는 그리 간단하지 않으며, Java의 역사 동안 다양한 GC 알고리즘이 개발되고 적용되었습니다. GC는 비용을 수반합니다: 실행될 때 종종 "Stop The World"라는 뜻으로, GC가 진행되는 동안 애플리케이션이 일시 중지됩니다. 보통 이러한 일시 중지는 매우 작게 설계되었지만, 애플리케이션에 압력이 가해질수록 증가할 수 있습니다.
스레딩과 Java 메모리 모델
Java가 첫 번째 버전에서 도입한 주요 발전 중 하나는 멀티스레드 프로그래밍에 대한 내장 지원이었습니다. Java 플랫폼은 개발자가 새로운 실행 스레드를 생성할 수 있도록 합니다.
Thread t = new Thread(() -> { System.out.println("Hello World!"); });
t.start();
뿐만 아니라, Java 환경 자체도 본질적으로 멀티스레드이며, JVM도 마찬가지입니다.
대부분의 주요 JVM 구현에서, 각 Java 애플리케이션 스레드는 정확히 하나의 전용 운영 체제 스레드와 일치합니다. 모든 Java 애플리케이션 스레드를 실행하기 위해 스레드 풀을 공유하는 대안(그린 스레드라고 함)은 허용할 만한 성능 프로파일을 제공하지 못하고 불필요한 복잡성을 추가하는 것으로 나타났습니다.
JVM 모니터링 및 툴링
JVM은 성숙한 실행 플랫폼으로, 실행 중인 애플리케이션의 계측, 모니터링, 가시성을 위한 다양한 기술 대안을 제공합니다. JVM 애플리케이션을 위한 이러한 유형의 도구들에 사용할 수 있는 주요 기술은 다음과 같습니다
Java Management Extensions (JMX)
Java 에이전트
JVM Tool Interface (JVMTI)
Serviceability Agent (SA)
Last updated