자바는 write once, run anywhere 라는 철학을 가진 컴파일 언어입니다. 자바가 일반적인 컴파일 언어인 C, C++과 구분되는 가장 큰 특징은 바로 플랫폼 독립성입니다. 즉, 한 번 컴파일된 자바 코드는 다시 컴파일하지 않고도 자바 가상 머신이 설치된 모든 플랫폼에서 실행될 수 있습니다.
자바 가상 머신(Java Virtual Machine, 이하 JVM)은 자바 프로그램을 실행하기 위한 가상 머신입니다. JVM 덕분에, 우리가 작성한 자바 프로그램은 호스트 운영체제와의 직접적인 상호작용 없이 실행될 수 있습니다. 이는 자바 프로그램을 바이트코드(bytecode)로 컴파일하고, 해당 바이트코드를 JVM에서 실행함으로써 가능합니다. 여기서 바이트코드란 JVM의 명령어 집합을 의미합니다.
Note
바이트코드로 표현할 수 있는 모든 프로그래밍 언어는 JVM에서 실행될 수 있습니다.
자바 프로그램을 javac 컴파일러를 사용해 바이트코드로 컴파일해 봅시다.
public class JavaComplieExample {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
자바 프로그램은 javac
명령어를 사용해 컴파일할 수 있습니다.
$ javac JavaComplieExample.java
해당 프로그램을 컴파일하면, 자바 파일과 같은 경로에 JavaComplieExample.class
라는 클래스 파일이 생성된 것을 확인할 수 있습니다. 클래스 파일은 JVM에서 실행될 수 있는 바이트코드와 기타 보조 정보를 포함하는 이진 데이터 파일입니다.
java
명령어를 사용하면 컴파일된 자바 프로그램을 실행할 수 있습니다.
$ java JavaComplieExample
Hello, World!
컴파일된 JavaComplieExample.class
파일을 javap 역어셈블러로 역어셈블해 직접 확인해 봅시다.
$ javap -s -v JavaComplieExample.class
javap
명령어를 사용하면 다음과 같은 결과물을 확인할 수 있습니다.
Classfile /Users/junghoyun/Desktop/JavaComplieExample.class
Last modified 2024. 2. 14.; size 443 bytes
SHA-256 checksum 9d5b2d9cbd2f87508cfa354ad668608b5d7fca8c9b8717d740abec3efc0ed744
Compiled from "JavaComplieExample.java"
public class JavaComplieExample
minor version: 0
major version: 65
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #21 // JavaComplieExample
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Fieldref #8.#9 // java/lang/System.out:Ljava/io/PrintStream;
#8 = Class #10 // java/lang/System
#9 = NameAndType #11:#12 // out:Ljava/io/PrintStream;
#10 = Utf8 java/lang/System
#11 = Utf8 out
#12 = Utf8 Ljava/io/PrintStream;
#13 = String #14 // Hello, World!
#14 = Utf8 Hello, World!
#15 = Methodref #16.#17 // java/io/PrintStream.println:(Ljava/lang/String;)V
#16 = Class #18 // java/io/PrintStream
#17 = NameAndType #19:#20 // println:(Ljava/lang/String;)V
#18 = Utf8 java/io/PrintStream
#19 = Utf8 println
#20 = Utf8 (Ljava/lang/String;)V
#21 = Class #22 // JavaComplieExample
#22 = Utf8 JavaComplieExample
#23 = Utf8 Code
#24 = Utf8 LineNumberTable
#25 = Utf8 main
#26 = Utf8 ([Ljava/lang/String;)V
#27 = Utf8 SourceFile
#28 = Utf8 JavaComplieExample.java
{
public JavaComplieExample();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String Hello, World!
5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 3: 0
line 4: 8
}
SourceFile: "JavaComplieExample.java"
클래스 파일에서 가장 주목해야할 부분은 상수 풀(constant pool)입니다. 상수 풀은 클래스와 관련된 다양한 데이터를 저장하는 테이블 형태의 자료구조이며, 클래스 파일 내에서 중복을 피하고 값을 공유하여 메모리를 절약하는 데 사용됩니다. 상수 풀에는 리터럴이나 클래스 및 메서드에 대한 심볼릭 참조 등이 저장됩니다. JVM은 클래스 파일을 로드할 때 상수 풀에 저장된 값들을 참고하여 클래스 및 메서드의 정보를 효율적으로 읽어들일 수 있습니다.
Note
심볼릭 참조란 참조하는 대상의 실제 메모리 주소가 아닌 이름이나 식별자를 참조하는 것입니다.