본문 바로가기

Backend/Java

[Inside Java] 08. 마무리.. Java 실행 구조 핵심 위키!

 

앞선 1장부터 7장까지 다룬 내용을 다시 찾기 쉽도록 핵심 개념만 두줄로 압축했습니다.

자세한 동작 원리와 예시는 각 챕터에서 확인하고, 이 글에서는 개념의 연결 관계만 빠르게 정리합니다.

 

이번 시리즈를 정리하면서 Java는 단순히 컴파일만 하는 언어가 아니라, 인터프리터와 JIT 컴파일러를 함께 활용해 실행 성능을 높이는 구조라는 점이 특히 인상 깊었습니다.

또한 클래스 로딩부터 메모리 구조, 동적 디스패치, JIT 최적화까지 각각의 개념이 독립적으로 존재하는 것이 아니라 하나의 실행 흐름으로 연결되어 있다는 점도 새롭게 이해할 수 있었습니다.

 

 

예전에 Young Generation, Old Generation, Minor GC, Major GC 등 GC의 동작 방식도 한 번 공부했던 기억이 있는데, 이번 시리즈를 마무리하면서 다시 살펴보니 흐릿해진 부분도 있었습니다. 기회가 된다면 GC의 내부 동작과 다양한 JVM 최적화 기법도 다시 정리해 보고 싶습니다.

앞으로는 GC, JVM 최적화, 그리고 Spring이 이러한 JVM 위에서 어떻게 동작하는지까지 이어서 탐구해 볼 예정입니다.

 

 


전체 실행 흐름

.java
→ javac
→ .class
→ Class Loading
→ Linking
→ Initialization
→ Interpreter 실행
→ Runtime Profiling
→ JIT Compilation
→ Native Code 실행

 


1. Java Source와 Class File

Java Source

개발자가 작성한 .java 파일
javac가 이를 JVM이 실행할 수 있는 바이트코드로 컴파일 함

Class File

.class는 Java의 class 키워드 전용 파일이 아니라 JVM의 Class File Format

일반 클래스뿐 아니라 interface와 enum도 같은 형식으로 표현됨

Bytecode

특정 운영체제나 CPU의 기계어가 아니라 JVM 명령어임

각 플랫폼에 맞는 JVM이 동일한 바이트코드를 해석하거나 기계어로 변환함

Compile과 Build

Compile은 .java를 .class로 변환하는 과정

Build는 컴파일에 테스트, 리소스 처리, 의존성 구성, 패키징까지 포함하는 더 큰 과정!! (자바는 아님)


2. JVM과 실행 환경

JVM

JVM은 바이트코드를 실행하며 클래스 로딩, 메모리 관리, 메서드 호출, GC, JIT 컴파일을 담당하는 실행 환경임

Java Process

java Main을 실행하면 운영체제에 Java 프로세스가 만들어짐

JVM은 그 프로세스 안에서 Heap, 스레드별 Stack, 클래스 메타데이터, Code Cache 등 관리

Interpreter

바이트코드를 명령어 단위로 해석하며 빠르게 실행 시작

동시에 호출 횟수와 실제 타입 같은 런타임 정보를 수집함

JIT Compiler

자주 실행되는 코드를 네이티브 코드로 컴파일해 반복 실행 비용 줄임

컴파일된 코드는 Code Cache에 저장됨


3. JVM Runtime Data Areas

Heap

객체와 배열이 저장되는 스레드 공유 영역

지역 변수에는 객체 자체가 아니라 Heap 객체를 가리키는 참조가 저장될 수 있음

JVM Stack

스레드마다 독립적으로 존재하며 메서드 호출마다 Stack Frame이 쌓임

메서드가 종료(return … ;)되면 해당 Frame도 제거됨

Stack Frame

Local Variable Table, Operand Stack, 현재 메서드 실행에 필요한 정보 가짐

매개변수, 지역 변수, this 참조와 연산 중간값이 이 안에서 관리됨

Method Area

클래스 구조, 필드와 메서드 정보, Runtime Constant Pool 같은 타입 단위 정보를 관리하는 JVM 명세의 논리적 영역임

Metaspace

HotSpot JVM이 클래스 메타데이터를 네이티브 메모리에서 관리하는 구현 방식

Method Area와 완전히 같은 용어는 아님

PC Register

각 스레드가 현재 실행 중인 JVM 명령의 위치를 관리함

Native Method Stack

JNI 등을 통해 네이티브 메서드를 실행할 때 사용되는 영역임


4. 객체와 다형성

Parent p = new Child()

Heap에는 완전한 Child 객체 하나가 생성되고, 지역 변수 p에는 그 객체를 가리키는 참조가 저장됨

Parent 객체와 Child 객체가 따로 생성되는 것이 아님

선언 타입과 실제 타입

선언 타입은 Parent, 실제 객체 타입은 Child!

선언 타입은 접근 가능한 멤버 범위에 영향을 주고, 실제 타입은 오버라이딩 메서드 선택에 사용됨

Object Layout

Child 객체 안에는 Object Header와 부모에게서 상속된 인스턴스 필드, 자식이 선언한 인스턴스 필드가 함께 존재함

Klass Pointer

HotSpot 객체 헤더에는 객체의 런타임 클래스 메타데이터를 찾기 위한 정보가 포함됨

이를 통해 JVM은 실제 객체 타입과 메서드 정보를 확인할 수 있음


5. Field와 Method

Field

필드는 오버라이딩되지 않으며 같은 이름의 필드는 숨겨질 수 있음.

필드 접근은 변수의 선언 타입을 기준으로 결정됨

Overriding

부모의 인스턴스 메서드를 자식이 같은 시그니처로 다시 구현하는 것

호출 시 실제 객체 타입을 기준으로 최종 구현이 선택됨

Overloading

같은 이름의 메서드를 서로 다른 매개변수 목록으로 선언하는 것

호출 대상은 컴파일 시점의 정적 타입과 인자 정보를 기준으로 결정됨

Static Dispatch

컴파일 시점에 호출 대상이 결정되는 방식

오버로딩과 static 메서드 호출이 대표적

Dynamic Dispatch

런타임의 실제 객체 타입을 기준으로 오버라이딩된 메서드를 선택하는 방식

다형성이 실제 실행으로 이어지는 핵심 원리


6. 메서드 호출 바이트코드 (대표적인것만)

invokevirtual

일반적인 클래스 인스턴스 메서드 호출에 사용됨

오버라이딩된 메서드는 런타임에 실제 객체 타입을 기준으로 선택될 수 있음

invokeinterface

인터페이스 참조를 통한 인스턴스 메서드 호출에 사용됨

실제 구현 객체를 기준으로 최종 메서드 찾음

invokestatic

static 메서드 호출에 사용됨

receiver 객체 없이 호출 대상이 정적으로 결정됨

invokespecial

생성자, private, super 호출처럼 일반적인 동적 디스패치를 사용하지 않는 특별한 호출에 사용됨

invokedynamic

호출 연결 규칙을 런타임에 구성할 수 있도록 제공되는 명령어임

람다 표현식과 동적 언어 지원 등에 활용됨


7. Class Loading

ClassLoader

필요한 클래스 바이트를 찾아 JVM이 사용할 수 있는 런타임 타입으로 정의

바이트는 .class, JAR, 네트워크 또는 런타임 생성 데이터에서 올 수 있음

Loading

클래스 바이트를 읽어 JVM 내부의 런타임 타입과 연결된 java.lang.Class 객체를 만듬

Verification

바이트코드가 Class File Format과 JVM 안전 규칙을 만족하는지 검사함

Preparation

static 필드를 위한 공간을 준비하고 먼저 JVM 기본값 설정함

Resolution

Constant Pool의 심볼릭 참조를 실제 클래스, 필드, 메서드 같은 런타임 대상과 연결

Initialization

static 필드 초기화식과 static 블록 실행

클래스가 로딩되었다고 해서 반드시 초기화까지 완료된 것은 아님

Active Use

new, static 메서드 호출, 비상수 static 필드 접근처럼 클래스를 실제로 사용하는 시점에 초기화가 발생할 수 있음

<clinit>

static 필드 초기화식과 static 블록을 모아 실행하는 JVM 내부 클래스 초기화 메서드

필요한 초기화 코드가 없으면 생성되지 않을 수 있음

<init>

객체 생성자를 나타내는 JVM 내부 메서드 이름

객체가 생성될 때마다 실행되며 부모 생성자 호출과 인스턴스 초기화 포함됨


8. Runtime Constant Pool과 Linking

Constant Pool

클래스명, 필드명, 메서드명, 문자열과 숫자 상수 등 Class File에서 사용하는 정보를 모아 둔 테이블임

Runtime Constant Pool

클래스가 로딩되면 Class File의 Constant Pool을 바탕으로 JVM 내부에 만들어지는 런타임 자료구조

Symbolic Reference

컴파일 시점에는 실제 런타임 위치를 알 수 없으니께 클래스명, 메서드명, 타입 정보 같은 이름 기반 참조 기록함

Direct Reference

Resolution 이후 JVM이 실제 런타임 대상을 찾을 수 있도록 연결된 참조임

반드시 고정된 물리 메모리 주소만을 뜻하지는 않음

Dynamic Linking

필요한 클래스와 메서드 참조를 실행 과정에서 연결하는 특성

실제 구현을 선택하는 Dynamic Dispatch와는 다른 개념


9. 변수와 초기화

Local Variable

자동 기본값이 제공x

컴파일러가 값이 확실히 대입되었다고 판단한 이후에만 읽을 수 있음

Instance Field

객체 생성 시 JVM 기본값을 먼저 얻고, 이후 명시적 초기화식과 인스턴스 초기화 블록, 생성자가 순서대로 실행됨.

Static Field

Preparation에서 기본값을 얻고, Initialization에서 개발자가 작성한 static 초기화 코드가 실행됨

final Reference

다른 객체를 다시 가리키지 못하게 할 뿐, 참조 대상 객체의 내부 상태까지 불변으로 만드는 것은 아님

Compile-time Constant

컴파일 시 값이 확정되는 일부 static final 기본형·문자열 상수는 ConstantValue 등을 통해 일반 static 필드와 다르게 처리될 수 있음


10. Interface

Interface Object

인터페이스 자체의 구현 인스턴스는 생성할 수 없음.

Heap에는 인터페이스를 구현한 실제 클래스의 객체가 생성됨.

Abstract Method

메서드 선언 정보만 있고 구현 바이트코드인 Code 속성은 없습니다. 구현 코드는 구현 클래스의 .class에 존재함

Default Method

구현 바이트코드는 인터페이스의 .class에 저장됨

구현 클래스가 오버라이드할 수 있으므로 실제 호출은 Dynamic Dispatch 대상임

Interface Static Method

인터페이스 메타데이터에 저장되며 InterfaceName.method() 형태로 호출함

receiver 객체 없이 invokestatic으로 호출됨


11. Enum

Enum Class File

Enum도 JVM의 Class File Format으로 표현되므로 .class로 컴파일됨

Enum Constant

각 enum 상수는 Heap에 존재하는 실제 객체(Object)

같은 상수는 프로그램 안에서 동일한 인스턴스를 가리킴

Constant-specific Class Body

상수마다 서로 다른 메서드 구현을 작성하면 Operation$1.class처럼 추가 클래스 파일이 만들어질 수 있음


12. JIT Optimization

Hot Code

호출 횟수나 반복 횟수가 임계값을 넘어서 자주 실행된다고 판단된 코드임

JIT 컴파일의 주요 대상이 됨

Runtime Profiling

JVM은 실행 중 호출 횟수, 분기 결과, receiver의 실제 타입 등을 수집함

JIT는 이 정보를 바탕으로 최적화함

Devirtualization

호출 지점에서 실제 타입이 거의 고정되어 있다고 판단하면 동적 호출을 직접 호출에 가까운 형태로 최적화 함

Inlining

호출 대상 메서드의 본문을 호출 지점에 펼쳐 호출과 복귀 비용 줄임

이후 상수 전파와 분기 제거 같은 추가 최적화 가능해짐

Deoptimization

JIT가 세운 타입이나 실행 경로에 대한 가정이 깨지면 최적화된 코드를 폐기하고 안전한 실행 상태로 돌아감

Code Cache

JIT가 생성한 네이티브 코드가 저장되는 JVM 메모리 영역임

 


[Inside Java] 몰아보기

 

[Inside Java] 01. Java는 왜 바로 기계어로 컴파일하지 않을까? — 바이트코드부터 JVM 메모리까지...

Swift로 iOS 앱을 만들 때는 보통 컴파일 → 빌드 → 실행 파일 생성 → CPU 실행이라는 흐름이 비교적 자연스럽게 느껴졌습니다.C++도 비슷합니다. 소스 코드를 컴파일하면 특정 운영체제와 CPU가 이

dev-with-precious-dreams.tistory.com

 

[Inside Java] 02. 업캐스팅?! Java 컴파일러는 무엇을 알고, JVM은 무엇을 결정할까? — Dynamic Dispatch로

Parent p = new Child(); Java를 공부하다 보면 이 한 줄을 아주 자주 만나게 됩니다.보통은 이렇게 설명합니다.부모 타입 변수에 자식 객체를 담는 업캐스팅이다. 문법적으로는 맞는 설명입니다.그런데

dev-with-precious-dreams.tistory.com

 

[Inside Java] 03. Heap 객체에서 method Area까지! - Klass Pointer와 Dynamic vs Static Dispatch 살펴보기

이전 포스트(누르면 바로가져요) 에서는 다음 코드가 왜 문법적으로 가능한지 살펴봤습니다.Parent p = new Child(); 컴파일러는 표현식 p의 정적 타입을 Parent로 보고, 실제 생성되는 객체는 Child입니

dev-with-precious-dreams.tistory.com

 

[Inside Java] 04. JVM은 main()을 바로 실행하지 않는다?! #1 -ClassLoader, 클래스 생명주기 #Loading #Linking #In

1편에서는 Java 코드가 바이트코드로 변환되고 JVM 위에서 실행되는 전체 흐름을 살펴봤습니다. 2편에서는 컴파일러가 정적 타입을 기준으로 무엇을 검사하고, 런타임의 JVM이 실제 객체를 기준으

dev-with-precious-dreams.tistory.com

 

[Inside Java] 05. JVM은 main()을 바로 실행하지 않는다 #2 — class metadata는 언제 초기화 될까? #Active Use,

앞선 4장에서는 .class 파일이 JVM 내부에서 Loading, Linking, Initialization을 거쳐 실행 가능한 타입이 되는 과정을 살펴봤습니다.하지만 여기서 한 가지 궁금증이 생깁니다. “클래스가 메모리에 로드

dev-with-precious-dreams.tistory.com

 

[Inside Java] 06. 같은 Java 코드가 실행될수록 빨라지는 이유 — JIT 컴파일과 HotSpot JVM 최적화 #Inline #

[Inside Java] 1~5편 까지는 JVM이 메모리를 어떻게 관리하는지를 중심으로 살펴보았습니다. 그런데 지금까지 살펴본 것처럼 JVM이 런타임마다 바이트코드를 하나씩 해석하고, Operand Stack을 이용해 연

dev-with-precious-dreams.tistory.com

 

[Inside Java] 07. interface는 JVM 메모리에서 어떻게 관리될까? — default 메서드와 enum 객체까지!!

이전 [Inside Java] 시리즈에서는 class로 만든 객체와 메서드 호출이 JVM 안에서 어떻게 연결되는지 살펴봤습니다.그런데 Java에는 class만 있는 것이 아니죠.....interface Animal { void speak();}enum Direction { LEF

dev-with-precious-dreams.tistory.com