..

Java 소스 코드에서 class 파일까지: javac와 bytecode

TOC


  1. Overview
  2. Java는 어떻게 실행되는가
  3. javac는 무엇을 하는가
  4. class 파일에는 무엇이 들어가는가
  5. java 명령은 무엇을 시작하는가
  6. javap로 bytecode를 확인하는 법
  7. 흔한 오해와 정리 포인트
  8. Conclusion
  9. Next Step

Overview


Java는 보통 이렇게 움직인다.

  1. .java 파일을 작성한다.
  2. javac가 이를 class 파일로 바꾼다.
  3. java가 JVM을 통해 실행한다.

핵심은 소스와 실행이 분리된다는 점이다.

예를 들면 아래 같은 파일이 있다고 하자.

public class HelloWorld {
    public static void main(String[] args) {
        int count = 3;
        for (int i = 0; i < count; i++) {
            System.out.println("Hello, Java " + i);
        }
    }
}

이 코드는 사람이 읽기 쉽지만, JVM이 그대로 읽는 형태는 아니다.

Java는 어떻게 실행되는가


작은 예제 하나만 보자.

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, Java");
    }
}

이 파일은 javac를 거쳐 HelloWorld.class가 되고, java HelloWorld가 JVM을 시작한다.

즉, Java 실행은 두 단계다.

  • 컴파일 단계: 소스 코드를 bytecode로 바꾼다.
  • 실행 단계: JVM이 bytecode를 읽고 실행한다.

흐름만 적으면 이렇다.

  1. javac HelloWorld.java
  2. HelloWorld.class 생성
  3. java HelloWorld
  4. JVM이 main 호출

javac는 무엇을 하는가


javac는 문법과 타입을 확인하고 class 파일을 만든다.

컴파일 오류와 실행 오류는 다르다. 문법/타입 오류는 javac 단계에서 막히고, null 참조나 배열 범위 초과는 실행 중에 드러난다.

예를 들면 아래 코드는 컴파일 단계에서 막힌다.

int value = "3";

반대로 아래 코드는 컴파일은 되지만 실행 중에 터질 수 있다.

String name = null;
System.out.println(name.length());

즉, javac는 문법과 타입이 맞는지 먼저 확인하는 관문이다.

class 파일에는 무엇이 들어가는가


class 파일은 소스 코드가 아니라 JVM이 읽을 바이너리 정보다. 클래스 이름, 상속 관계, 필드, 메서드, bytecode, constant pool, 부가 정보가 들어간다.

특히 constant pool은 문자열과 메서드/필드 참조를 담아 실행 중 심볼 해석을 가능하게 한다.

예를 들어 아래 코드를 생각해보자.

public class Sample {
    public static void main(String[] args) {
        String message = "hello";
        System.out.println(message);
    }
}

이 코드가 class 파일로 바뀌면, "hello" 같은 문자열과 System.out.println 참조가 constant pool에 들어간다.

java 명령은 무엇을 시작하는가


java는 소스 파일을 직접 해석하지 않는다. class 파일을 로드해 main 메서드부터 실행한다.

public static void main(String[] args)

그 전에 class loading, linking, static 초기화가 일어날 수 있다.

javap로 bytecode를 확인하는 법


javapclass 파일을 확인할 때 유용하다.

예를 들면 아래처럼 컴파일한 뒤:

javac HelloWorld.java
javap -c -v HelloWorld

bytecode 수준의 명령을 볼 수 있다. println 호출도 여러 bytecode로 바뀐다.

예를 들면 if는 이런 흐름으로 읽을 수 있다.

if (count > 0) {
    System.out.println("ok");
}

bytecode 관점에서는 조건을 평가하고, 맞지 않으면 분기 점프를 한다.

for도 같은 방식이다.

for (int i = 0; i < 3; i++) {
    System.out.println(i);
}

이 코드는 초기화, 조건 검사, 본문 실행, 증가식이 bytecode 명령으로 나뉜다.

흔한 오해와 정리 포인트


자주 생기는 오해만 정리하면 된다.

  1. Java는 소스 그대로 실행되지 않는다.
  2. class 파일은 소스 코드 복사본이 아니다.
  3. javac가 하는 일과 JVM이 하는 일을 구분해야 한다.
  4. java 명령은 실행을 시작할 뿐, 모든 단계를 다 직접 처리하지 않는다.
  5. if, for, 메서드 호출도 결국 bytecode 명령들의 조합으로 바뀐다.

한 줄로 정리하면 이렇다.

  • 소스 코드는 사람이 읽는 형태다.
  • class 파일은 JVM이 읽는 형태다.
  • JVM은 class 파일을 바탕으로 실행을 맡는다.

Conclusion


Java는 소스 코드와 실행 코드가 분리되어 있다.

javac는 소스를 bytecode로 바꾸고, java는 그 결과물을 JVM에서 실행한다.

Next Step


다음 글에서는 class 파일 내부 구조를 본다. constant pool, field, method, attribute를 정리해보자.