본문 바로가기

Backend/Java

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

 

 

이전 포스트(누르면 바로가져요) 에서는 다음 코드가 왜 문법적으로 가능한지 살펴봤습니다.

Parent p = new Child();

 

컴파일러는 표현식 p의 정적 타입을 Parent로 보고, 실제 생성되는 객체는 Child입니다.

그런데 여기서 한 단계 더 들어가면 새로운 질문이 생깁니다.

p.introduce();

 

바이트코드에는 Parent.introduce()에 대한 참조가 남는데, 실행 결과는 왜 Child.introduce()일까요?

 

Heap의 객체에는 필드 값만 있을 텐데, JVM은 어떻게 이 객체가 Child라는 사실을 알고 실제 메서드 구현까지 찾아갈까요?

이번 글은 이 질문을 해결하는 데 집중합니다.

 

기준 코드

class Parent {
    int parentAge = 50;
    String name = "Parent";

    void introduce() {
        System.out.println("Parent introduce");
    }

    void greet() {
        System.out.println("Hello from Parent");
    }
}

class Child extends Parent {
    int childAge = 20;
    String name = "Child";

    @Override
    void introduce() {
        System.out.println("Child introduce");
    }
}

public class Main {
    public static void main(String[] args) {
        Parent p = new Child();

        System.out.println(p.name);
        p.introduce();
        p.greet();
    }
}

 

실행 결과는 다음과 같습니다.

Parent
Child introduce
Hello from Parent

 

필드는 Parent.name을 읽고, introduce()Child가 오버라이딩했기 때문에 런타임에 Child.introduce()가 선택됩니다.

 

반면 greet()Child가 따로 오버라이딩하지 않았습니다. 따라서 실제 객체가 Child이더라도 상속 관계를 따라 Parent.greet()를 그대로 실행합니다.

 

여기서 한 가지를 먼저 구분해야 합니다.

오버라이딩된 메서드에만 invokevirtual이 붙는 것은 아닙니다.

 

이 예제의 introduce()greet()는 모두 인스턴스 메서드 호출이므로 바이트코드에서는 invokevirtual로 호출될 수 있습니다. (차이는 명령어의 종류가 아니라, 런타임에 receiver의 실제 클래스를 기준으로 최종 구현을 찾았을 때 어디에서 메서드가 발견되느냐에 있습니다.)

 

p.introduce()
→ receiver는 Child Object
→ Child가 introduce()를 오버라이딩함
→ Child.introduce() 선택

p.greet()
→ receiver는 Child Object
→ Child에는 greet() 구현이 없음
→ 상속 관계를 따라 Parent.greet() 선택

 

3편에서는 이 두 흐름을 함께 비교하며, JVM이 실제 호출 대상을 어떻게 선택하는지 살펴봅니다.

먼저 전체 구조 부터

 

이 그림에서 가장 중요한 연결은 세 가지입니다.

main Frame의 p
→ Heap의 Child Object

Child Object의 Klass Pointer
→ Child.class Metadata

Child.class Metadata의 Superclass Pointer
→ Parent.class Metadata
→ Object.class Metadata

 

변수 p는 직접 메서드 정보를 가지고 있는 것도 아니고, Heap 객체 안에 메서드 코드가 복사되어 있는 것도 아닙니다.

 

Child.class Metadata는 자신이 직접 선언한 필드와 메서드 정보뿐 아니라, 부모 클래스인 Parent.class Metadata로 이어지는 상속 관계도 가지고 있습니다.

 

따라서 Childintroduce()를 오버라이딩했다면 Child.introduce()가 선택되고, greet()처럼 오버라이딩하지 않은 메서드는 상속 체인을 따라 Parent.greet()를 사용합니다.

 

 

Method Area의 영역을 조금 더 확대해서 보면 위 그림처럼 이해할 수 있습니다.

여기서 Superclass Pointer는 상속 관계를 이해하기 위한 개념적 표현입니다. 실제 HotSpot 내부 구조와 필드 이름은 구현에 따라 달라질 수 있습니다.

 

변수 p가 Heap의 Child Object를 참조하듯, 각 클래스의 Metadata는 자신이 직접 선언한 필드와 메서드의 구조 정보를 가지고 있습니다. 또한 Child 클래스처럼 상속 관계가 있다면 부모 클래스의 Metadata를 참조합니다.

 

메서드 Metadata에는 메서드 이름만 저장되는 것이 아닙니다. 일반적인 Java 메서드라면 컴파일된 바이트코드와 max locals, max stack, 예외 처리 정보처럼 JVM이 메서드를 실행하는 데 필요한 정보도 함께 관리됩니다. 즉, 우리가 작성한 소스코드가 그대로 저장되는 것은 아니지만, 소스코드의 실행 로직은 컴파일된 바이트코드 형태로 이 영역과 연결되어 있습니다.

 

즉, Heap의 객체에는 실행 코드가 저장되는 것이 아니라, 어떤 클래스의 인스턴스인지를 나타내는 연결 정보만 존재합니다.

실제 메서드의 실행 로직은 Method Metadata와 연결된 바이트코드를 통해 관리되며, JVM은 이를 Interpreter가 해석하거나 이후 JIT가 컴파일한 기계어를 실행합니다.

 

 

위 그림은 이해를 돕기 위해 단순화한 구조입니다.

실제 HotSpot JVM에서는 InstanceKlass, Method 같은 C++ 객체들이 서로 포인터로 연결되어 동작합니다.

 

[ Method Area / Metascpace ] (살짝 유사하게,, )

  ┌────────────────────────────────────────────────────────┐
  │ InstanceKlass (C++ 객체 / Child.class 메타데이터)          │
  ├────────────────────────────────────────────────────────┤
  │ - _name         : "Child"                              │
  │ - _super        │ (포인터) ──> [ Parent InstanceKlass ]  │
  │ - _fields       │ (포인터) ──> [ 필드 정보 배열 ]           │
  │ - _methods      │ (포인터) ──> [ 메서드 포인터 배열 ]        │
  ├────────────────────────────────────────────────────────┤
  │ [EMBEDDED vtable] (가상 메서드 테이블 공간)                  │
  │   - introduce() → [ Child.introduce()의 Method* ]       │
  │   - greet()     → [ Parent.greet()의 Method*    ]       │
  └────────────────────────────────────────────────────────┘
                                              │
                                              ▼ (가리킴)
                                    ┌──────────────────────┐
                                    │ Method (C++ 구조체)    │
                                    │ - access_flags       │
                                    │ - max_stack / locals │
                                    │ - _code (바이트코드)    │
                                    └──────────────────────┘

 

 

클래스와 관련된 모든 정보가 하나의 InstanceKlass 내부에 그대로 차곡차곡 저장되는 것은 아닙니다. InstanceKlass는 클래스 이름, 부모 클래스, 필드 정보, 메서드 목록 등 클래스 단위의 핵심 정보를 가지고 있으며, 세부 메서드 정보와 바이트코드는 별도의 Method 및 관련 내부 구조를 참조합니다.

 

따라서 실제 구조는 그림처럼 독립된 박스들이 단순히 이어지는 형태라기보다, 여러 JVM 내부 객체가 필요한 정보를 직접 보관하거나 다른 구조를 포인터로 참조하며 연결되는 형태에 가깝습니다.

 

 

jdk/src/hotspot/share/oops/instanceKlass.hpp at master · openjdk/jdk

JDK main-line development https://openjdk.org/projects/jdk - openjdk/jdk

github.com

method area의 instanceKlass 내부 구현체를 보고 싶다면 위 링크를 클릭하세요 !

Stack Frame 내부에는 무엇이 들어 있을까

main()이 호출되면 main Thread의 JVM Stack에 main Stack Frame이 생성됩니다.

 

하나의 Stack Frame에는 지역 변수와 연산 값만 들어 있는 것은 아닙니다.

현재 실행 중인 메서드가 동작하고, 다른 메서드를 호출한 뒤 다시 원래 위치로 돌아오기 위해 필요한 정보들이 함께 관리됩니다.

main Stack Frame
├── Local Variable Table
│   ├── 메서드 매개변수
│   ├── 지역 변수
│   └── 객체 참조값
│
├── Operand Stack
│   ├── 연산 중간값
│   ├── 메서드 호출 인자
│   └── 호출 대상 객체의 receiver 참조
│
├── 현재 메서드의 Runtime Constant Pool 연결
├── Dynamic Linking에 필요한 정보
├── 메서드 반환 후 돌아갈 위치
├── 반환값 전달에 필요한 정보
└── 예외 처리에 필요한 정보

 

그러나 JVM 명세가 Stack Frame 내부의 모든 정보를 위와 같은 물리적 레이아웃으로 배치하도록 강제하는 것은 아닙니다.

위 구조는 하나의 메서드가 실행되기 위해 어떤 정보들이 필요한지를 개념적으로 정리한 것입니다.

참고로 메서드의 매개변수는 별도의 공간에 저장되는 것이 아니라 Local Variable Table의 슬롯에 들어갑니다.
현재 예제의 main()static 메서드이므로 slot 0에는 첫 번째 매개변수인 String[] args가 저장되고, 이후 선언된 지역 변수 pslot 1에 저장됩니다.
인스턴스 메서드에서는 slot 0this가 들어가는데, 이 흐름은 뒤에서 Child.introduce()의 새 Stack Frame이 생성되는 과정에서 다시 살펴보겠습니다.

Local Variable Table

Local Variable Table
────────────────────────────────
Slot    Variable    Type        Value
────────────────────────────────
0       args        String[]    ref 0x4000
1       p           Parent      ref 0x1010

 

 

p 슬롯에 저장되는 것은 Child 객체 자체가 아닙니다.

Heap의 객체를 가리키는 참조값입니다.

Operand Stack

p.introduce()를 호출할 때 JVM은 Local Variable Table의 참조값을 Operand Stack으로 옮깁니다.

 

 

피연산자 스택(Operand Stack) 최상단에 위치한 ref 0x1010은 해당 메서드를 실제로 실행할 대상 객체, 즉 수신 객체(Receiver) 역할을 담당하게 됩니다.

따라서 동적 디스패치(Dynamic Dispatch)는 소스 코드 상의 변수 이름인 p를 기준으로 작동하지 않습니다.

프로그램이 실행되는 런타임 시점에 피연산자 스택으로 적재된 수신 객체의 참조값으로부터 본격적인 디스패치 과정이 시작됩니다.

Object Header는 왜 필요할까요..

HotSpot JVM 관점에서 일반 객체의 헤더는 흔히 다음과 같이 설명합니다.

Object Header
────────────────────────────────
Mark Word
Klass Pointer

(cf.) Mark Word

Mark Word는 객체의 런타임 상태와 관련된 정보를 담는 영역입니다.

Mark Word
────────────────────────────────
잠금 상태
해시 코드 관련 정보
GC age
기타 런타임 상태 비트

 

정확한 비트 구성은 JVM 버전과 객체 상태에 따라 달라질 수 있습니다.

 

여러 스레드 동시 접근 시 락 걸고 푸는 상태 기록 GC Age는 객체가 Young Generation의 Survivor 영역을 오갈 때마다 살아남은 횟수를 기록해서 Old Generation으로의 승격 여부를 결정하는 나이정보 담김..

 

Klass Pointer

Klass Pointer는 객체가 어떤 클래스의 인스턴스인지 연결하는 정보입니다.

Child Object @0x1010
────────────────────────────────
Klass Pointer
        ↓
Child.class Metadata
  • Klass Pointer는 HotSpot 구현을 설명할 때 사용하는 표현.
  • JVM 명세가 모든 구현체에 같은 이름과 물리적 레이아웃을 강제하는 것은 아님 명세서 느낌,,
    • 참고로 JVM 제조사, 버전에 따라 내부 구조 달라질 수 있습니다.

객체에는 값만 저장!

Heap 객체에는 실제 인스턴스 상태가 저장됩니다.

parentAge = 50
childAge  = 20
name      → String 객체 참조

 

반면 필드 이름, 타입, 접근 제어자, 어느 클래스에서 선언되었는지와 같은 구조 정보는 Class Metadata가 관리합니다.

 

Heap Object
→ 객체마다 달라지는 실제 데이터

Class Metadata
→ 그 데이터를 해석하기 위한 구조 정보

 

객체 자체가 parentAgeChild.name 같은 선언 정보를 들고 있는 것은 아니고 JVM은 객체가 연결된 Class Metadata를 바탕으로 객체 내부의 값을 어떤 필드로 해석할지 판단합니다.

 

JVM 명세는 이런 클래스 단위 정보가 Method Area라는 논리적 영역에서 관리된다고 설명합니다.

JVM(e.g. HotSpot)에서는 클래스 메타데이터가 주로 Metaspace에 저장됩니다.

 

Method Area
→ JVM 명세가 정의한 논리적 런타임 데이터 영역

Metaspace
→ HotSpot이 클래스 메타데이터를 저장하는 구현 공간

 

따라서 Method Area = Metaspace라고 완전히 동일시하면 명세와 구현이 섞입니다.

  • Method Area: "이런 역할을 하는 공간이 있어야 해!" 라는 이름표(개념)
  • Metaspace: "그 역할은 우리 HotSpot에서는 이 영역(OS 메모리)을 써서 이렇게 구현할 거야!" 라는 실제 구현체

Method Metadata 에서는 메서드 이름만 저장할까요?

노노!

Method Metadata
────────────────────────────────
Name               : introduce
Descriptor         : ()V
Access Flags       : package
Bytecode           : ...
Exception Table    : ...
Max Locals         : ...
Max Operand Stack  : ...
Constant Pool Link : ...

 

예를 들어 void introduce() { ... } 메서드는 JVM 수준에서 introduce: ()V 이렇게 이름과 descriptor로 표현할 수 있습니다.

() : 매개변수 없음
V : 반환값 void

 

그래서 JVM은 단순히 메서드 이름만 찾는 것이 아니라, 이름과 descriptor를 함께 사용해 정확한 메서드를 식별합니다.

한가지 퀴즈!
힙 영역의  child Object안에 메서드 구현부? 선언부를 저장할까요?

 

→ 힙영역에 클래스에 대한 메서드를 저장 할까요 ?

 

Child c1 = new Child();
Child c2 = new Child();
Child c3 = new Child();

 

Heap에는 서로 다른 객체 세 개가 생성됩니다.

 

introduce()의 바이트코드(쉽게말해.. introduce 함수 내부 구현 로직들)가 각 객체 안에 세 번 복사되는 것은 아닙니다.

 

 

객체마다 달라지는 것은 인스턴스 상태입니다.

메서드 정보와 바이트코드는 클래스 단위로 공유됩니다.

그렇다면.. 왜 힙(Heap) 영역에 함수 구현 로직(명령어)을 직접 저장하지 않을까?

만약 클래스에 500줄짜리 메서드가 10개 있고, 이 클래스로 객체(인스턴스)를 100개 만든다고 가정해 보것습니다.

1. 만약 힙 영역에 메서드 코드까지 통째로 저장한다면? (가상의 상황)

  • 객체 1개당 500줄짜리 메서드 10개의 바이트코드(총 5,000줄의 명령어)가 고스란히 복사되어 들어갑니다.
  • 이걸 객체 100개로 늘리면? 5,000줄 × 100개 = 무려 50만 줄의 똑같은 명령어 코드가 힙 메모리에 사본으로 도배되게 됨
  • 결과: 메모리는 엄청난 양의 중복 데이터로 순식간에 거덜나구, 똑같은 코드를 CPU에 올렸다가 내리느라 캐시 효율 최악!

2. 실제 JVM의 방식: 코드는 메서드 영역에 '단 1개'만 두고 공유하기

  • 500줄짜리 메서드 10개의 실제 명령어(바이트코드)는 오직 메서드 영역(Metaspace)에 딱 1개만 !!
  • 힙 영역에 만들어지는 100개의 객체는 그 무거운 코드 덩어리를 들고 다닐 필요가 전혀 없음. 대신 각 객체는 "내 진짜 코드는 저기 메서드 영역의 어느 주소에 있어!"라는 얇고 가벼운 주소값(포인터)만 쥐고 있습니다.
  • 그래서 ..? : 객체가 100개든 1,000개든 늘어나는 것은 오직 객체의 상태값(힙에 할당된 인스턴스 object 변수 데이터)뿐이며, 메서드 코드는 단 하나만 존재하므로 메모리를 극단적으로 아꼇다~

한 줄 요약

"데이터(상태)는 객체마다 다르지만, 행동(코드)은 모든 객체가 똑같기 때문에, 무거운 코드를 객체마다 복사해 두지 말자. 한 곳에 모아두고 같이 쓰는 것!"

 

마치 싱글톤 패턴이랑 똑 닮았네요 ㅎㅎ

p.introduce()는 컴파일 시점에 무엇을 확인할까

Parent p = new Child();
p.introduce();

 

컴파일러는 우선 변수 p의 정적(static) 타입을 확인합니다.

p의 정적 타입
    ↓
Parent
    ↓
Parent에 introduce()가 선언되어 있는가
    ↓
호출 가능 여부 결정

 

따라서 Child에만 선언된 메서드는 바로 호출할 수 없습니다.

class Childddd extends Parent {
    void study() {}
}

Parent p = new Childddd();
p.study(); // compile error

실제 객체가 Child여도 컴파일러가 보는 표현식의 타입은 Parent이기 때문입니다. 에러가 나는 것은? 컴파일러가.. 그것은 포스트 2장에서 간단히 소개 했었습니다!


참고 바이트코드에는 왜 Parent.introduce가 보일까

개념적으로 다음과 같은 바이트코드가 만들어질 수 있습니다.

aload_1
invokevirtual Parent.introduce:()V

 

 

invokevirtual Parent.introduce:()V는 다음 뜻에 가깝습니다.

Parent 타입을 기준으로 호출 가능 여부와 메서드 참조를 확인했지만, 실제 실행할 구현은 receiver의 런타임 클래스를 기준으로 선택합니다.

 

따라서 바이트코드에 Parent.introduce가 보인다고 해서 부모 구현을 무조건 직접 호출하는 것은 아닙니다.

그래서 Dynamic Dispatch는 어떻게 진행될까요

컴파일 타임에는 선언된 타입(Parent)을 믿고 바이트코드(invokevirtual)를 발행하지만,
런타임에는 힙에 생성된 실제 객체(Child)의 메타데이터를 추적해 가장 구체화된 메서드를 실행하는 메커니즘

 

단계별로 풀면 다음과 같습니다. ( 이거는 raw 버전 그 아래 스크롤하면 좀 순화버전 !)

확대.. 해서.. 보셔야 합니닷

 

1. aload_1
   p에 저장된 참조값을 Operand Stack에 올린다.

2. invokevirtual
   Operand Stack 위의 참조값을 receiver로 사용한다.

3. receiver 객체 확인
   ref 0x1010이 가리키는 객체는 Child Object다.

4. 실제 클래스 확인
   객체의 Klass Pointer를 통해 Child.class Metadata에 도달한다.

5. 메서드 선택
   Child가 introduce()를 오버라이딩했으므로 Child.introduce()를 선택한다.

6. 메서드 호출
   Child.introduce()를 위한 새 Stack Frame을 생성한다. 
   (왜냐면 introduce는 함수이기에, JVM Stack의 Main StackFrame위에 introduce StackFrame의 
       스택 프레임이 푸시되며 함수 안에 내용들이 실행 되겠습니다!)

이 과정이 Dynamic Dispatch입니다.

 

 

  1. Parent p = new Child();가 실행되면 힙(Heap)에 실제 Child 객체가 생성됩니다.
  2. 이 객체 안의 Klass Pointer는 자신이 Child 객체임을 가리킵니다.
  3. 메서드가 오버라이딩 되었다는 구체적인 정보는 개별 객체가 아니라, Method Area에 있는 Child 클래스의 메타데이터(vtable 등)에 기록됩니다.
  4. 따라서 런타임에 메서드를 호출할 때 선언된 타입(Parent)만 보는 것이 아닙니다.
  5. 힙에 있는 실제 객체의 메타데이터를 참조하여 재정의된 메서드를 찾아냅니다.
    - 왜냐하면 new Child를 했기 때문에 p는 Child object를 가리키고,,, 이때 오버라이딩 여부를 vtable로 파악하게 됩니당
  6. 이처럼 런타임에 실제 객체 기반으로 실행할 메서드를 결정하는 메커니즘이 Dynamic Dispatch!

참고로 Dynamic 이 있다면? static Dispatch도 있겠죠?

 

구분 Static Dispatch (정적 디스패치) Dynamic Dispatch (동적 디스패치)
결정 시점 컴파일 타임 (Compile-time) 런타임 (Runtime)
판단 기준 변수의 선언된 타입
(Parent p)
힙(Heap)에 생성된 실제 객체
(new Child())
대상
  • 필드(멤버 변수) (p.name)
  • static 메서드
  • private 메서드
  • super 호출
  • 오버라이딩된 인스턴스 메서드 (p.introduce())
  • 상속받은 인스턴스 메서드 (p.greet())
작동 원리 컴파일러가 코드에 명시된 타입 정보를 보고 호출할 대상을 컴파일 시점에 고정(Static Binding)한다. 바이트코드(invokevirtual)가 런타임에 객체 헤더의 Klass Pointervtable을 조회하여 실행할 메서드를 동적으로 결정한다.
코드 예시 System.out.println(p.name);

→ Parent의 "Parent" 출력
p.introduce(); → Child 실행

p.greet(); → Parent 실행

새 Stack Frame의 slot 0에는 this가 들어간다

Child.introduce()가 선택되면 해당 메서드를 위한 새 Stack Frame이 생성됩니다.

 

인스턴스 메서드의 Local Variable Table에서 slot 0은 일반적으로 this를 저장합니다.

 

pthis는 서로 다른 Stack Frame의 서로 다른 슬롯에 있습니다.

하지만 두 슬롯에는 같은 참조값 ref 0x1010이 저장됩니다.

그래서 Child.introduce() 내부의 thisp가 가리키던 바로 그 Child 객체를 가리킵니다.


참고! vtable은 어디까지 이해해야 할까

Dynamic Dispatch를 매 호출마다 상속 계층 전체 탐색으로 처리하면 비용이 커질 수 있습니다.

HotSpot과 같은 JVM 구현체는 가상 메서드 호출을 빠르게 처리하기 위해 vtable과 유사한 디스패치 구조를 사용할 수 있습니다.

 

Parent Dispatch Table
────────────────────────────────
toString()    → Object.toString()
introduce()   → Parent.introduce()
greet()       → Parent.greet()

Child Dispatch Table
────────────────────────────────
toString()    → Object.toString()
introduce()   → Child.introduce()
greet()       → Parent.greet()

 

Child가 introduce()를 오버라이딩하면 같은 논리적 슬롯이 Child.introduce()를 가리키도록 구성될 수 있습니다.

정확히 알면 좋을것!

  • Dynamic Dispatch는 Java와 JVM이 보장하는 실행 의미
  • vtable은 그 실행 의미를 효율적으로 구현하는 대표적인 전략!
  • 참고로 JVM 명세가 모든 구현체에 같은 vtable 구조를 강제하지는 않음

정리

Parent p = new Child()에서 p의 정적 타입은 Parent이지만, 실제 Heap에 생성되는 객체는 Child입니다.

  • pmain Stack Frame의 Local Variable Table에 저장!
  • p의 슬롯에는 객체 자체가 아니라 Heap의 Child 객체를 가리키는 참조값이 저장
  • Heap의 Child 객체에는 부모에게 상속받은 인스턴스 필드와 자식이 선언한 인스턴스 필드의 실제 값이 저장됨
  • 객체 내부에는 메서드 바이트코드나 필드 이름 같은 클래스 구조 정보가 복사되어 저장x
  • HotSpot JVM의 객체 헤더에는 객체 상태를 관리하는 Mark Word와 클래스 메타데이터를 연결하는 Klass Pointer가 존재가능
  • Klass Pointer를 따라가면 해당 객체의 실제 클래스인 Child의 Class Metadata에 도달 가능
  • Class Metadata에는 클래스 이름, 부모 클래스, 필드 정보, 메서드 정보, Runtime Constant Pool과 같은 클래스 단위 정보가 관리됨
  • 각 메서드의 이름, descriptor, 접근 정보, 바이트코드, 최대 Local Variable 수와 Operand Stack 크기 등의 정보는 Method Metadata와 관련 내부 구조에서 관리됨
  • 따라서 객체가 여러 개 생성되어도 메서드 바이트코드가 객체마다 반복해서 복사되지는 않음

p.introduce()를 컴파일할 때 컴파일러는 p의 정적 타입인 Parentintroduce()가 존재하는지 확인합니다.

 

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] 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

 

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

앞선 1장부터 7장까지 다룬 내용을 다시 찾기 쉽도록 핵심 개념만 두줄로 압축했습니다.자세한 동작 원리와 예시는 각 챕터에서 확인하고, 이 글에서는 개념의 연결 관계만 빠르게 정리합니다.

dev-with-precious-dreams.tistory.com