시작
JVM 내부로 파고들어 메서드 호출이 어떻게 이루어지는지를 정리하고자 합니다. Amazon Corretto 기준으로 작성하였습니다.
본문
invokevirtual
invokevirtual
은 가장 일반적인 메서드 호출 방식입니다. 이 방식은 바이트코드를 사용해서 특정 클래스의 객체(또는 하위 클래스)에서 인스턴스 메서드를 호출하는 것을 의미합니다. 이 방식을 또 가상 메서드 디스패치(virtual method dispatch
)라고도 합니다. 이 방식은 컴파일 시점에는 호출할 메서드가 정해지지 않고 런타임 시점에 호출할 메서드가 결정됩니다.
아래와 같은 코드가 있다고 가정해 봅시다.
abstract class Animal {
abstract void eat();
}
class Dog extends Animal {
void eat() {
System.out.println("Animal is eating");
}
}
public class Main {
public static void main(String[] args) {
Animal dog = new Dog();
dog.eat();
}
}
위 코드에서 dog.eat()
은 invokevirtual
로 호출됩니다. invokevirtual
은 가상 메서드 디스패치를 사용하여 메서드를 호출합니다. 즉 컴파일 시점에는 호출할 메서드가 정해지지 않고 런타임 시점에 호출할 메서드가 결정됩니다.
INVOKEVIRTUAL me/albert/Dog.eat ()V
즉 컴파일 시점에는 호출할 메서드가 정해지지 않고 런타임 시점에 호출할 메서드가 결정됩니다.
만약 Kclass의 메서드 테이블에 메서드가 없다면 JVM은 부모 클래스의 메서드 테이블을 참조합니다. 이러한 과정을 통해 메서드 호출이 이루어집니다.

vtable
내부적으로 위 작업을 수해하기 위해 JVM은 클래스별로 해당 클래스의 메서드 테이블을 가지고 있습니다. 이 테이블은 vtable
이라고 불리며 클래스의 메서드들을 가리키는 포인터들을 가지고 있습니다. 이 테이블은 JVM에 필요한 메타데이터를 보관하는 JVM 내부의 메타스페이스(namespace)라는 특수한 메모리 영역에 저장됩니다.
모든 자바 객체는 객체 헤더(object header)를 가지고 있습니다. 객체 헤더는 고유한 메타데이터(mark word), 공유하는 메타데이터(klass word)를 가지고 있습니다.
- mark word: 객체의 상태 정보를 가지고 있습니다. 객체의 상태 정보는 객체가 동기화되었는지, 객체가 가비지 컬렉션의 대상인지 등을 나타냅니다. 힙에 저장된 객체의 메모리 주소를 가지고 있습니다.
- klass word: 객체의 클래스 정보를 가지고 있습니다.
namespace
에 저장된 클래스의 메서드 테이블을 가리키는 포인터를 가지고 있습니다.
다시 정리해 봅니다.
- 스택: 객체의 참조를 가지고 있습니다.
- 힙: 객체의 인스턴스를 가지고 있습니다. 객체의 인스턴스는 객체 헤더와 인스턴스 데이터를 가지고 있습니다.
- 메타스페이스: 클래스의 메타데이터를 가지고 있습니다. 클래스의 메타데이터는 클래스의 메서드 테이블을 가지고 있습니다.

invokeInterface
invokeInterface
는 인터페이스의 메서드를 호출할 때 사용됩니다. invokeInterface
는 invokevirtual
과 비슷하지만 인터페이스의 메서드를 호출할 때 사용됩니다. 컴파일 시점에는 호출할 메서드가 정해지지 않고 런타임 시점에 호출할 메서드가 결정됩니다.
interface Animal {
void eat();
}
class Dog implements Animal {
@Override
public void eat() {
System.out.println("Dog is eating");
}
}
public class Main {
public static void main(String[] args) {
Animal dog = new Dog();
dog.eat();
}
}
위 코드에서 dog.eat()
은 invokeInterface
로 호출됩니다. invokeInterface
는 인터페이스의 메서드를 호출할 때 사용됩니다. 인터페이스의 메서드를 호출할 때는 인터페이스의 메서드 테이블을 사용합니다.
INVOKEINTERFACE me/albert/Animal.eat ()V (itf)
즉 dog 참조는 Animal
인터페이스를 가리키고 있기 때문에 Animal
인터페이스의 메서드 테이블을 사용합니다. Animal
인터페이스의 메서드 테이블은 Dog
클래스의 메서드 테이블을 가리키고 있습니다. 만약 dog 참조가 Dog
클래스를 가리키고 있다면 invokevirtual
로 호출됩니다.

invokeSpecial
invokeSpecial
은 init
메서드를 호출할 때 사용됩니다.
class Animal {
void eat() {
System.out.println("Animal is eating");
}
}
public class Main {
public static void main(String[] args) {
Animal animal = new Animal();
animal.eat();
}
}
위 코드에서 new Animal()
은 invokeSpecial
로 호출됩니다.
INVOKEVIRTUAL me/albert/Animal.<init> ()V
Java 8에서는 private 메서드를 호출할 때 invokeSpecial을 사용합니다. Java 11부터는 invokeSpecial 대신 invokeVirtual을 사용합니다.
invokeStatic
invokeStatic
은 정적 메서드를 호출할 때 사용됩니다.
class Animal {
static void eat() {
System.out.println("Animal is eating");
}
}
public class Main {
public static void main(String[] args) {
Animal.eat();
}
}
위 코드에서 Animal.eat()
은 invokeStatic
으로 호출됩니다.
INVOKESTATIC me/albert/Animal.eat ()V
invokeDynamic
invokeDynamic
은 런타임에 동적으로 호출할 메서드를 결정할 때 사용됩니다. invokeDynamic
은 Bootstrap
메서드를 사용하여 호출할 메서드를 결정합니다.
Bootstrap
메서드는 CallSite
를(unlaced
상태) 생성하고 CallSite
는 호출할 메서드를 가리키는 포인터를 가지고 있습니다. 표준 인수는 다음과 같은 타입을 가지고 있습니다.
MethodHandles.Lookup
: 호출할 메서드를 찾기 위한 메서드 핸들을 가지고 있습니다.String
: 호출할 메서드의 이름을 가지고 있습니다.MethodType
: 호출할 메서드의 타입을 가지고 있습니다.
public class Main {
public static void main(String[] args) {
Runnable r = () -> System.out.println("Hello, World!");
r.run();
}
}
위 코드에서 r.run()
은 invokeDynamic
으로 호출됩니다.
INVOKEDYNAMIC run()Ljava/lang/Runnable;
마무리
이 Post에서는 JVM 내부로 파고들어 메서드 호출이 어떻게 이루어지는지를 정리하고자 하였습니다. invokevirtual
, invokeInterface
, invokeSpecial
, invokeStatic
, invokeDynamic
등 다양한 메서드 호출 방식을 살펴보았습니다.
댓글