사전 javap
인터페이스

javap

gabury1고친 사람 github-actions[bot]

javap 는 컴파일이 끝난 자바 파일을 열어 안에 무엇이 들었는지 글로 보여 주는 명령입니다. 소스 코드가 아니라 컴파일러가 만들어 낸 결과를 들여다볼 때 씁니다. 기본으로는 클래스의 필드와 메서드 목록을 찍습니다. 옵션에 따라 메서드 안의 명령어까지 찍습니다.

쉽고 빠른 이해

javap 는 컴파일된 자바 파일의 속을 글로 보여 주는 명령입니다. javap HelloWorldFrame.class 라고 치면 그 클래스에 든 필드와 메서드 목록이 나옵니다.

컴파일된 파일은 사람이 열어 읽을 수 없는 바이너리입니다. 컴파일러는 소스를 소스와 다른 모양으로 바꿉니다. app.kt 의 함수는 AppKt 라는 자바 클래스의 메서드가 됩니다. 무엇으로 바뀌었는지는 파일을 열어 봐야 압니다.

돌아가는 방식은 이렇습니다.

  1. 클래스 이름이나 파일 경로를 받습니다
  2. 정해진 경로에서 그 파일을 찾아 읽습니다
  3. 옵션이 정한 만큼 풀어서 찍습니다

한계도 있습니다. 찍히는 것은 파일에 남은 것뿐입니다. 컴파일할 때 안 남긴 지역 변수 이름은 돌아오지 않습니다. 소스 모양으로 되살려 주지도 않습니다.

상세

자바 소스를 컴파일하면 .class 확장자를 단 클래스 파일이 나옵니다. 클래스 파일은 JVM(Java Virtual Machine, 자바 가상 머신)이 읽어 실행하는 바이너리 파일입니다. javap 는 이 클래스 파일을 풀어 사람이 읽는 글로 찍는 명령입니다.

이 항목은 Java SE(Standard Edition) 21 판 javap 문서를 따릅니다. 그 문서는 맨 앞 이름 줄에서 javap 를 "disassemble one or more class files" 라고 소개합니다. 역어셈블(disassemble)은 기계가 읽는 명령어를 사람이 읽는 표기로 옮기는 일입니다. 출력은 어떤 옵션을 쓰느냐에 따라 달라집니다.

flowchart TD
    S["HelloWorldFrame.java · 소스"] --> C["javac · 자바 컴파일러"]
    C --> F["HelloWorldFrame.class · 클래스 파일"]
    F --> J["JVM · 읽어서 실행"]
    F --> P["javap · 풀어서 글로 찍음"]

javap 는 프로그램을 실행하는 길에 끼지 않습니다. 자바 컴파일러 javac 가 만든 클래스 파일을 JVM 과 따로 읽어 글로 옮길 뿐입니다.

이 명령이 필요한 까닭은 컴파일 결과가 소스와 모양이 다르기 때문입니다. 코틀린에서 org.example 패키지의 app.kt 에 선언한 함수는 org.example.AppKt 라는 자바 클래스의 정적 메서드로 컴파일됩니다. 소스 어디에도 AppKt 라는 이름은 없습니다. 컴파일러가 무엇을 만들었는지는 클래스 파일을 열어야 보입니다. javap 가 그 파일을 글로 여는 도구입니다.

javap 는 따로 설치하는 물건이 아닙니다. JDK(Java Development Kit, 자바 개발 도구 모음)에 딸려 옵니다. 모듈은 패키지 여럿을 이름 하나로 묶은 단위입니다. javap 를 정의하는 모듈은 jdk.jdeps 입니다. 같은 모듈이 jdeps 와 jdeprscan 도구도 정의합니다.

javap 는 명령줄 말고 자바 코드에서도 부를 수 있습니다. 그 방법은 이 모듈의 API(Application Programming Interface) 문서에 있습니다. ToolProvider 서비스 제공자 인터페이스(SPI, Service Provider Interface)를 쓰면 명령줄과 같은 기능을 코드에서 씁니다. 도구 인스턴스는 ToolProvider.findFirst 나 서비스 로더에 이름 "javap" 를 넘겨 얻습니다.

형태

javap 는 아래 꼴로 부릅니다. 두 인자 중 classes 는 무엇을 풀지 정합니다. options 는 무엇을 얼마나 찍고 어디서 찾을지 정합니다.

javap [options] classes...

classes 는 풀어 볼 클래스입니다. 공백으로 갈라 하나 이상을 줍니다. 클래스 경로는 JVM 과 자바 도구가 클래스 파일을 찾으러 가는 디렉터리와 JAR(Java ARchive) 파일의 목록입니다. 클래스 경로에서 찾을 수 있는 클래스는 세 방식으로 적을 수 있습니다.

적는 방식 예
파일 이름 path/to/MyClass.class
URL(Uniform Resource Locator) jar:file:///path/to/MyJar.jar!/mypkg/MyClass.class
정규화된 클래스 이름 — 패키지까지 붙인 이름 java.lang.Object

셋을 어떤 순서로 가려 해석하는지는 아래 「동작」 절에 있습니다.

options 에 넣는 옵션은 javap 문서에 열아홉 항목이 실려 있습니다. 아래에서는 하는 일에 따라 넷으로 묶습니다. 찍을 멤버의 범위, 찍는 양, 찾는 곳, 그리고 이 셋에 안 드는 옵션입니다.

오류 없이 끝나면 결과를 글로 찍습니다. 결과 코드는 javap 가 끝날 때 돌려주는 정수입니다. 오류 없이 끝나면 0 입니다. OpenJDK 소스 JavapTask.java 에서는 이 값을 EXIT_OK 라는 이름으로 둡니다. 나머지 값은 「실패」 절에 있습니다.

찍을 멤버의 범위

접근 제한자를 붙이지 않은 멤버는 같은 패키지 안에서만 보입니다. 이것을 package 접근이라고 부릅니다.

옵션 원문 찍는 멤버
-public Shows only public classes and members. public 만
-protected Shows only protected and public classes and members. protected · public
-package Shows package/protected/public classes and members (default). package · protected · public. 기본값이라는 표시가 붙어 있습니다
-p · -private Shows all classes and members. private 까지 전부

네 옵션은 안쪽 고리를 품는 고리입니다. 바깥 고리로 갈수록 찍는 멤버가 늘어납니다.

flowchart TD
    subgraph P["-p · private 까지 전부"]
        subgraph K["-package · package 까지 · -package 항목이 기본값으로 표시"]
            subgraph R["-protected · protected 까지 · 설명 절이 옵션 없을 때로 적은 범위"]
                U["-public · public 만"]
            end
        end
    end

그런데 옵션을 안 줬을 때의 기본 범위가 명세 안에서 두 가지로 적혀 있습니다. 둘은 서로 다른 고리를 가리킵니다. 설명 절은 옵션 없이 부르면 protected 와 public 필드 · 메서드를 찍는다고 합니다. -package 항목은 package · protected · public 에 기본값 표시를 달았습니다. 같은 문서의 예제에서는 옵션 없이 부른 출력에 접근 제한자 없이 선언한 message 필드가 찍혀 있습니다. 예제 출력은 -package 항목의 표현과 맞습니다.

찍는 양

클래스 파일은 안의 정보를 속성(attribute) 단위로 나눠 담습니다. 이 구조는 JVMS(Java Virtual Machine Specification, 자바 가상 머신 명세)가 정합니다. 속성은 속성 표(attributes table)에 들어가는 항목입니다. 메서드마다 속성 표가 딸려 있습니다. 그 속성 표의 Code 속성이 메서드 하나의 JVM 명령어와 보조 정보를 담습니다. Code 속성에도 속성 표가 있습니다. 줄 번호 표 LineNumberTable 과 지역 변수 표 LocalVariableTable 은 그 표에 들어가는 속성입니다.

바이트코드는 JVM 이 실행하는 명령어의 모음입니다. -c 와 -l 이 찍는 것은 이 구조의 아래쪽입니다.

flowchart TD
    CF["클래스 파일"] --> M["메서드"]
    M --> CA["Code 속성"]
    CA --> I["JVM 명령어 · -c 가 찍음"]
    CA --> LN["줄 번호 표 LineNumberTable · -l 이 찍음"]
    CA --> LV["지역 변수 표 LocalVariableTable · -l 이 찍음"]
옵션 하는 일
-c 메서드마다 역어셈블한 코드를 찍습니다. 자바 바이트코드를 이루는 명령어가 그 예입니다
-l 줄 번호 표와 지역 변수 표를 찍습니다
-s 내부 타입 시그니처(internal type signatures)를 찍습니다
-constants static final 상수를 보여 줍니다
-v · -verbose 고른 클래스에 대한 추가 정보를 찍습니다
-sysinfo 처리하는 클래스의 시스템 정보를 보여 줍니다. 경로 · 크기 · 날짜 · SHA(Secure Hash Algorithm)-256 해시입니다

타입 시그니처는 필드나 메서드가 어떤 타입을 쓰는지 적은 표기입니다. -s 의 설명은 한 줄뿐입니다. 이 표기가 「예시」 절에서 푸는 디스크립터와 같은 꼴인지는 모릅니다(미확인). -v 가 더하는 「추가 정보」가 무엇무엇인지도 모릅니다(미확인). 찍는 항목이 하나하나 적혀 있는 것은 -sysinfo 쪽입니다.

찾는 곳

상세 절의 jdk.jdeps 처럼 JDK 에 딸려 오는 모듈이 있습니다. 모듈을 찾는 곳을 정하는 옵션은 애플리케이션 모듈용과 시스템 모듈용으로 나뉘어 있습니다. 두 갈래를 무엇으로 가르는지는 모릅니다(미확인).

옵션 하는 일
--module 모듈 · -m 모듈 풀어 볼 클래스가 든 모듈을 정합니다
--module-path 경로 애플리케이션 모듈을 찾는 곳을 정합니다
--system jdk 시스템 모듈을 찾는 곳을 정합니다
--class-path 경로 · -classpath 경로 · -cp 경로 javap 가 사용자 클래스 파일을 찾는 경로를 정합니다. 기본값을, 또는 CLASSPATH 환경 변수가 설정돼 있으면 그 값을 덮어씁니다
-bootclasspath 경로 부트스트랩 클래스 파일의 위치를 덮어씁니다
--multi-release 판 여러 판(release)용 클래스를 함께 담은 JAR 에서 어느 판을 고를지 정합니다

옵션 뒤에 받는 값은 원문에서 path · module · version 같은 낱말로 적혀 있습니다. --system jdk 의 jdk 도 이런 낱말입니다. 거기에 JDK 경로를 적는지, 글자 jdk 를 적는지는 모릅니다(미확인). 부트스트랩 클래스 파일이 무엇인지도 확인하지 못했습니다(미확인). --module 을 주면 클래스를 찾는 순서가 달라집니다. 「동작」 절에 있습니다.

그 밖의 옵션

옵션 하는 일
--help · -help · -h · -? javap 명령의 도움말을 찍습니다
-version 릴리스 정보를 찍습니다
-J옵션 적은 옵션을 JVM 에 넘깁니다. 예를 들면 javap -J-version 입니다

실패

javap 가 원하는 결과를 못 내는 경우는 둘로 나뉩니다. 하나는 오류 문구를 내며 멈추는 경우입니다. 다른 하나는 오류 없이 끝나도 찾던 내용이 안 보이는 경우입니다. 이 절의 오류 문구는 OpenJDK 저장소 jdk-21-ga 태그의 메시지 파일 javap.properties 에 있는 것입니다. 결과 코드와 분기 조건은 같은 태그의 소스 JavapTask.java 를 보고 적었습니다.

결과 코드

JavapTask.java 는 결과 코드 다섯 개를 상수로 둡니다. 이름이 모두 EXIT_ 로 시작합니다.

상수 값 소스 주석의 뜻
EXIT_OK 0 오류 없이 끝남
EXIT_ERROR 1 끝났지만 오류를 보고함
EXIT_CMDERR 2 명령줄 인자가 잘못됨
EXIT_SYSERR 3 시스템 오류나 자원 고갈
EXIT_ABNORMAL 4 비정상 종료

3 과 4 를 어느 조건에서 돌려주는지는 확인하지 못했습니다(미확인).

오류 문구

상수 풀(constant pool)은 클래스 파일이 쓰는 클래스 이름 · 메서드 이름 · 문자열 같은 상수를 모아 둔 표입니다.

조건 문구 메시지 키 결과 코드
풀어 볼 클래스를 하나도 안 줌 no classes specified err.no.classes.specified 2. 도움말이나 판 정보를 찍는 옵션을 줬으면 0
--module 로 준 모듈을 못 찾음 Cannot find module {0} err.cant.find.module 1
모듈을 찾는 도중 입출력 예외(IOException)가 남 Problem finding module {0}: {1} err.cant.find.module.ex 1
넘긴 이름으로 클래스를 못 찾음 class not found: {0} err.class.not.found 1
넘긴 파일이 없음 file not found: {0} err.file.not.found 미확인
파일에 기대한 클래스가 들어 있지 않음 File {0} does not contain class {1} warn.unexpected.class 미확인
상수 풀을 읽다가 오류가 남 error while reading constant pool for {0}: {1} err.bad.constant.pool 미확인
다 읽기 전에 파일이 끝남 unexpected end of file while reading {0} err.end.of.file 미확인

{0} · {1} 은 실행 때 채워지는 값입니다. 문구로 보아 {0} 은 대상 파일이나 클래스, 모듈의 이름입니다. 표의 위 네 줄은 조건과 결과 코드를 JavapTask.java 의 분기로 확인했습니다. 같은 이름의 모듈이 한 위치 묶음에 둘 있으면 모듈을 찾는 메서드가 IOException 을 던집니다. 그래서 이 경우도 셋째 줄로 갑니다. 아래 네 줄의 조건은 문구만 보고 적은 것입니다. 메시지 파일에는 조건이 따로 적혀 있지 않습니다. warn.unexpected.class 만 키가 경고(warning)를 뜻하는 warn. 으로 시작합니다. 이때 javap 가 멈추는지 계속 가는지는 모릅니다(미확인).

성공했는데 원하는 게 안 보이는 경우

private 멤버가 안 나옵니다. 기본 범위 밖이기 때문입니다. 기본 범위는 「형태」 절에서 본 대로 두 가지로 적혀 있습니다. 그래도 둘 다 private 을 넣지 않습니다. -p 를 줘야 private 까지 찍힙니다.

-l 을 줬는데 지역 변수 표가 없습니다. 클래스 파일에 그 정보가 안 들어 있을 수 있습니다. javac 의 -g 는 지역 변수를 포함한 모든 디버깅 정보를 만드는 옵션입니다. -g 계열 옵션을 안 주면 javac 은 줄 번호와 소스 파일 정보만 만듭니다. 그래서 옵션 없이 컴파일한 클래스에는 지역 변수 정보가 안 들어갑니다.

javac 은 디버깅 정보를 고르는 꼴을 셋 둡니다.

javac 옵션 만드는 디버깅 정보
-g 지역 변수까지 모든 디버깅 정보
-g:none 아무것도 안 만듭니다
-g:[lines,vars,source] 쉼표로 적은 종류만 만듭니다. lines 는 줄 번호, vars 는 지역 변수, source 는 소스 파일 정보입니다

지역 변수 표를 넣으려면 -g 로 컴파일하거나 -g: 뒤 목록에 vars 를 넣습니다. -g:vars 하나만 주면 적은 종류만 만들기 때문에 줄 번호 정보가 빠집니다. LineNumberTable 과 LocalVariableTable 은 선택(optional) 속성입니다. 클래스 파일에 없어도 됩니다.

메서드의 파라미터 이름이 안 보입니다. 옵션 없이 찍은 메서드 줄에는 public static void main(java.lang.String[]); 처럼 타입만 나옵니다. javac 의 -parameters 는 생성자와 메서드의 형식 파라미터 이름을 클래스 파일에 저장합니다. 리플렉션은 실행 중인 코드가 클래스의 메서드 · 필드 정보를 읽는 기능입니다. 이름을 저장하는 목적은 리플렉션 API 의 java.lang.reflect.Executable.getParameters 가 그 이름을 꺼내게 하는 것입니다. -parameters 로 컴파일한 클래스에서 javap 가 어느 옵션으로 파라미터 이름을 찍는지는 모릅니다(미확인).

예시

javap 문서에 실린 예제 한 벌을 봅니다. 스윙(Swing) 창 클래스 HelloWorldFrame 의 클래스 파일을 옵션 없이 한 번, -c 를 붙여 한 번 찍은 결과입니다. 두 출력을 소스와 맞대어 어느 줄이 어디서 왔는지 읽습니다.

Java
import java.awt.Graphics;

import javax.swing.JFrame;
import javax.swing.JPanel;

public class HelloWorldFrame extends JFrame {

   String message = "Hello World!";

   public HelloWorldFrame(){
        setContentPane(new JPanel(){
            @Override
            protected void paintComponent(Graphics g) {
                g.drawString(message, 15, 30);
            }
        });
        setSize(100, 100);
    }
    public static void main(String[] args) {
        HelloWorldFrame frame = new HelloWorldFrame();
        frame.setVisible(true);

    }

}

HelloWorldFrame 을 옵션 없이

javap HelloWorldFrame.class
Compiled from "HelloWorldFrame.java"
public class HelloWorldFrame extends javax.swing.JFrame {
  java.lang.String message;
  public HelloWorldFrame();
  public static void main(java.lang.String[]);
}
  • 첫 줄은 이 클래스 파일이 어느 소스 파일에서 컴파일됐는지를 보여 줍니다
  • 타입은 javax.swing.JFrame · java.lang.String 처럼 패키지까지 붙은 이름으로 나옵니다. 소스의 import 줄은 출력에 없습니다
  • 접근 제한자 없이 선언한 필드 message 가 찍혀 있습니다. 옵션 없이 부른 출력에 package 멤버가 들어간다는 뜻입니다
  • 생성자와 main 은 선언 줄만 나옵니다. 메서드 본문도, 파라미터 이름 args 도 없습니다

HelloWorldFrame 에 -c 를 붙여

javap -c HelloWorldFrame.class
Compiled from "HelloWorldFrame.java"
public class HelloWorldFrame extends javax.swing.JFrame {
  java.lang.String message;

  public HelloWorldFrame();
    Code:
       0: aload_0
       1: invokespecial #1        // Method javax/swing/JFrame."<init>":()V
       4: aload_0
       5: ldc           #2        // String Hello World!
       7: putfield      #3        // Field message:Ljava/lang/String;
      10: aload_0
      11: new           #4        // class HelloWorldFrame$1
      14: dup
      15: aload_0
      16: invokespecial #5        // Method HelloWorldFrame$1."<init>":(LHelloWorldFrame;)V
      19: invokevirtual #6        // Method setContentPane:(Ljava/awt/Container;)V
      22: aload_0
      23: bipush        100
      25: bipush        100
      27: invokevirtual #7        // Method setSize:(II)V
      30: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #8        // class HelloWorldFrame
       3: dup
       4: invokespecial #9        // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: iconst_1
      10: invokevirtual #10       // Method setVisible:(Z)V
      13: return
}

메서드마다 Code: 아래로 명령어가 붙었습니다. 「형태」 절에서 본 Code 속성의 내용입니다. 생성자 HelloWorldFrame() 아래에도 Code: 가 있습니다. 명령어 줄에서 생성자는 "<init>" 이라는 이름의 메서드로 나옵니다. main 의 0:~4: 가 그 모습입니다. 소스의 new HelloWorldFrame() 이 new · dup · invokespecial ... "<init>":()V 세 줄로 찍혔습니다. invokespecial 이 부르는 것 가운데 하나가 인스턴스 초기화 메서드(instance initialization method)입니다. 인스턴스 초기화 메서드의 명령어도 Code 속성에 담깁니다. 이 편은 생성자 · "<init>" · 인스턴스 초기화 메서드를 같은 것으로 부릅니다.

명령어 줄은 네 칸으로 읽습니다.

칸 예 읽는 법
맨 앞 숫자 5: 메서드 안에서 명령어마다 붙은 번호입니다. 0: 에서 시작해 커집니다. 1씩 오르지는 않습니다
명령어 ldc JVM 명령어 이름입니다. 뜻은 아래 표에 있습니다
# 번호 #2 명령어 뒤에 붙어 다른 항목을 가리키는 번호입니다
// 주석 // String Hello World! 그 번호가 가리키는 항목을 javap 가 사람이 읽는 이름으로 풀어 적은 것입니다

맨 앞 번호가 오르는 폭은 줄마다 1 · 2 · 3 으로 다릅니다. aload_0 · dup 처럼 명령어 이름만 있는 줄 다음은 1 이 오릅니다. bipush 100 · ldc #2 · putfield #3 처럼 뒤에 값이 붙은 줄 다음은 2 나 3 이 오릅니다. 이 번호가 무엇을 세는 값인지는 모릅니다(미확인).

아래 명령어 표의 설명에는 값을 두는 곳이 둘 나옵니다. 지역 변수와 피연산자 스택입니다. 지역 변수는 메서드 안에서 선언한 변수입니다. 스택은 마지막에 넣은 값을 먼저 꺼내는 자료구조입니다. 피연산자 스택(operand stack)은 명령어가 다룰 값을 쌓아 두는 스택입니다. 아래 표에서 스택 이름이 나오는 설명 줄은 dup 의 "the top operand stack value" 하나입니다. 이 편은 Push 를 이 스택 맨 위에 넣는 일로 옮겼습니다.

# 번호가 가리키는 항목은 주석에서 보입니다. Hello World! 같은 문자열, HelloWorldFrame$1 같은 클래스 이름, setSize 같은 메서드 이름입니다. 「실패」 절에서 푼 상수 풀이 모아 두는 상수가 이런 것들입니다. ldc 는 런타임 상수 풀(run-time constant pool)의 항목을 스택에 넣는 명령어입니다. # 번호가 상수 풀 안의 순번인지는 확인하지 못했습니다(미확인). 런타임 상수 풀과 클래스 파일의 상수 풀이 어떤 관계인지도 모릅니다(미확인).

이 출력에 나온 명령어는 열한 가지입니다. 영어 칸은 JVMS 가 명령어마다 붙인 한 줄 설명(Operation)입니다.

명령어 Operation 우리말
aload_0 · aload_1 Load reference from local variable 지역 변수에서 참조를 불러옵니다
astore_1 Store reference into local variable 참조를 지역 변수에 저장합니다
iconst_1 Push int constant int 상수를 스택에 넣습니다
bipush Push byte byte 값을 스택에 넣습니다
ldc Push item from run-time constant pool 런타임 상수 풀의 항목을 스택에 넣습니다
new Create new object 새 객체를 만듭니다
dup Duplicate the top operand stack value 피연산자 스택 맨 위 값을 복제합니다
putfield Set field in object 객체의 필드에 값을 넣습니다
invokespecial Invoke instance method; direct invocation of instance initialization methods and methods of the current class and its supertypes 인스턴스 초기화 메서드와 현재 클래스 · 상위 타입의 메서드를 직접 부릅니다
invokevirtual Invoke instance method; dispatch based on class 클래스에 따라 골라 인스턴스 메서드를 부릅니다
return Return void from method 값 없이 메서드에서 돌아옵니다

aload_0 · aload_1 · astore_1 · iconst_1 끝의 숫자가 무엇을 가리키는지는 이 설명만으로는 모릅니다(미확인). 출력 안에서는 짝이 보입니다. main 에서 7: astore_1 이 저장한 참조를 8: aload_1 이 다시 불러옵니다. 소스에서 그 참조는 지역 변수 frame 입니다.

주석 끝의 ()V · (II)V · Ljava/lang/String; 는 디스크립터입니다. 디스크립터는 JVMS 가 정한 타입 표기입니다. 필드 디스크립터는 필드나 변수의 타입을 나타냅니다. 메서드 디스크립터는 괄호 안의 파라미터 디스크립터들과 괄호 뒤의 반환 디스크립터로 이루어집니다.

flowchart TD
    D["(II)V · 메서드 디스크립터"] --> PA["괄호 안 · 파라미터 디스크립터"]
    D --> RE["괄호 뒤 · 반환 디스크립터"]
    PA --> I1["I · int"]
    PA --> I2["I · int"]
    RE --> V["V · 반환값 없음"]
이 출력의 표기 읽는 법 표기 규칙
Ljava/lang/String; java.lang.String 클래스의 인스턴스 L ClassName ; 는 클래스 ClassName 의 인스턴스
()V 파라미터 없음 · 반환값 없음 V 는 메서드가 값을 안 돌려준다(void)는 뜻
(II)V int 둘을 받고 반환값 없음 I 는 int
(Z)V boolean 하나를 받고 반환값 없음 Z 는 boolean
(Ljava/awt/Container;)V java.awt.Container 하나를 받고 반환값 없음 L ClassName ;

이 표로 읽으면 명령어 몇 줄이 소스의 어느 줄에서 왔는지 보입니다.

  • 생성자 1: 의 JFrame."<init>":()V 는 상위 클래스 JFrame 의 파라미터 없는 초기화 메서드를 부르는 줄입니다
  • 생성자 5:~7: 의 ldc · putfield 는 소스에서 필드 선언에 붙인 = "Hello World!" 입니다. 초기값이 생성자 코드 안에 들어가 있습니다
  • 생성자 11: 의 new 와 16: 의 invokespecial 이 가리키는 HelloWorldFrame$1 은 소스의 new JPanel(){ ... } 입니다. 이름 없이 식 안에서 선언한 클래스를 익명 클래스라고 부릅니다. 이 익명 클래스가 출력에서는 HelloWorldFrame$1 이라는 클래스로 나옵니다
  • 생성자 23:~27: 의 bipush 100 두 번과 setSize:(II)V 는 소스의 setSize(100, 100) 입니다
  • main 의 9: iconst_1 과 setVisible:(Z)V 는 frame.setVisible(true) 입니다

동작

인자로 받은 문자열 하나가 어떤 순서로 클래스 파일이 되는지 봅니다. 명세에는 이 순서가 없습니다. 아래 순서는 OpenJDK 저장소 jdk-21-ga 태그 JavapTask.java 의 run · open · getClassFileObject · findModule 메서드를 따릅니다. 인자를 어떻게 적든 먼저 클래스 이름으로 찾아 봅니다. 파일 경로와 URL 로 여는 것은 그다음입니다. 그래서 .class 로 안 끝나는 인자는 클래스 경로나 모듈에서 찾아져야만 읽힙니다.

flowchart TD
    B{"클래스 이름으로 찾아지나 · 안 되면 점을 $ 로 바꿔 다시"}
    B -->|찾음| Z["그 클래스 파일을 읽음"]
    B -->|못 찾음| D{"이름이 .class 로 끝나나"}
    D -->|아니다| N["class not found 문구 · 결과 코드 1"]
    D -->|그렇다| E{"그 경로에 파일이 있나"}
    E -->|있다| Z
    E -->|없다| F{"영문자와 콜론으로 시작하나"}
    F -->|그렇다| U["URL 로 열어 읽음"]
    F -->|아니다| X["어떻게 끝나는지 옮긴 코드에 없음"]
  1. --module 을 줬으면 모듈부터 찾습니다. --module(-m)에 적은 이름은 옵션 값 moduleName 에 들어갑니다. 클래스를 찾기 전에 findModule 메서드가 UPGRADE_MODULE_PATH · SYSTEM_MODULES · MODULE_PATH 순서로 그 이름의 모듈을 찾습니다. 찾은 모듈의 위치가 모듈 위치가 됩니다. 못 찾으면 Cannot find module {0} 을 내고 결과 코드 1 로 끝납니다
  2. 클래스 이름으로 찾습니다. 모듈 위치가 있으면 그 모듈 위치에서만 찾습니다. 없으면 플랫폼 클래스 경로를 먼저 보고, 거기 없으면 클래스 경로를 봅니다. 플랫폼 클래스 경로는 코드가 PLATFORM_CLASS_PATH 로 부르는 위치입니다. 클래스 경로는 CLASS_PATH 로 부르는 위치입니다. 소스 주석은 이 단계를 맨 앞에 둔 까닭을 호환성("for compatibility")이라고 밝힙니다
  3. 점을 $ 로 바꿔 다시 찾습니다. 오른쪽 점부터 하나씩 바꿉니다. java.lang.Object 라면 java.lang$Object 를, 그다음 java$lang$Object 를 찾아 봅니다. 소스 주석은 이 단계를 이너 클래스인지 보는 단계라고 부릅니다. 이너 클래스는 다른 클래스 안에 선언한 클래스입니다
  4. 이름이 .class 로 안 끝나면 여기서 멈춥니다. 못 찾은 것으로 돌아갑니다. 그러면 class not found: {0} 문구를 내고 결과 코드 1 로 끝납니다
  5. .class 로 끝나면 파일로 열어 봅니다. 그 경로의 파일을 받아 마지막 수정 시각이 0 이 아니면 그 파일을 씁니다
  6. 이름이 영문자와 콜론으로 시작하면 URL 로 엽니다. 「형태」 절의 예 jar:file:///path/to/MyJar.jar!/mypkg/MyClass.class 가 이 꼴입니다

2단계에서 어느 위치를 보는지는 --module 을 줬느냐로 갈립니다.

flowchart TD
    M{"--module 로 모듈 이름을 줬나"}
    M -->|줬다| FM{"그 모듈을 찾았나"}
    FM -->|못 찾음| E1["Cannot find module 문구 · 결과 코드 1"]
    FM -->|찾음| ML["그 모듈 위치에서만 찾음"]
    M -->|안 줬다| PC["플랫폼 클래스 경로에서 찾음"]
    PC -->|없다| CP["클래스 경로에서 찾음"]

이 순서에서 따라 나오는 것이 셋입니다.

  • org.example.AppKt 처럼 .class 로 안 끝나는 인자는 파일로 열지 않습니다. 클래스 경로나 모듈 위치에 그 클래스가 들어 있어야 합니다. 사용자 클래스 파일을 찾는 경로는 -cp 로 정합니다
  • .class 로 끝나는 파일 경로도 2 · 3단계를 먼저 거칩니다. 클래스 이름으로 찾아지지 않을 때 5단계에서 파일로 엽니다
  • 같은 이름의 클래스가 플랫폼 클래스 경로와 클래스 경로에 둘 다 있으면 플랫폼 클래스 경로 쪽을 읽습니다. --module 을 줬으면 두 경로를 안 보고 그 모듈 위치만 봅니다

플랫폼 클래스 경로에 무엇이 드는지, UPGRADE_MODULE_PATH · SYSTEM_MODULES · MODULE_PATH 를 각각 어느 옵션이 정하는지는 확인하지 못했습니다(미확인).

경계

javap 출력은 소스 코드를 되살린 것인가. 아닙니다.

javap 가 하는 일은 역어셈블("disassemble")입니다. 클래스 파일에 든 명령어와 선언을 사람이 읽는 표기로 옮기는 일입니다. 클래스 파일에 안 남은 것은 javap 도 보여 줄 수 없습니다. javac 은 기본으로 줄 번호와 소스 파일 정보만 만듭니다. 지역 변수 정보는 -g 를 줘야 만듭니다. 파라미터 이름은 -parameters 를 줄 때 클래스 파일에 저장합니다. LineNumberTable 과 LocalVariableTable 은 선택 속성입니다. 두 속성은 디버거가 쓸 수 있는 정보입니다.

예시 절의 HelloWorldFrame 을 소스와 맞대면 차이가 보입니다.

소스에 있던 것 javap -c 출력
import javax.swing.JFrame; 없습니다. 타입이 전부 javax.swing.JFrame 처럼 패키지를 붙인 이름으로 나옵니다
필드 선언의 초기값 = "Hello World!" 필드 줄에는 선언만 있습니다. 값은 생성자 코드의 ldc · putfield 로 나옵니다
new JPanel(){ ... } 익명 클래스의 본문 생성자의 11: new #4 와 16: invokespecial #5 두 줄에 HelloWorldFrame$1 이라는 이름으로만 나옵니다. 본문은 이 파일의 출력에 없습니다
지역 변수 이름 frame 이름 없이 astore_1 · aload_1 로 나옵니다

코틀린 소스에서 온 클래스도 마찬가지입니다. javap 가 받는 것은 클래스 파일입니다. 코틀린 최상위 함수는 그 클래스 파일에 org.example.AppKt 같은 클래스의 정적 메서드로 들어 있습니다.

flowchart TD
    K["app.kt · 코틀린 소스"] --> KC["코틀린 컴파일러"]
    KC --> A["org.example.AppKt 클래스 파일 · 정적 메서드 · kotlin.Metadata 애노테이션"]
    A --> J["javap 에 넘기는 것은 이 클래스"]

그래서 javap 에 넘기는 것은 app.kt 가 아니라 컴파일로 나온 클래스입니다. 「형태」 절의 세 방식대로 정규화된 클래스 이름 org.example.AppKt 로 넘기거나, 그 클래스 파일의 파일 경로나 URL 로 넘깁니다. 코틀린 컴파일러가 만든 클래스 파일에는 kotlin.Metadata 애노테이션이 붙습니다. 코틀린 문서는 이 애노테이션을 읽는 쪽으로 컴파일러와 리플렉션을 듭니다. javap 는 거기에 없습니다. javap 명세에도 출력을 코틀린 선언 모양으로 되돌린다는 내용이 없습니다. 코틀린 클래스를 javap 로 찍으면 출력이 어떻게 나오는지는 확인하지 못했습니다(미확인). 소스 모양을 되살리려는 도구는 디컴파일러라고 따로 부릅니다.

관련 항목

javap 가 풀어 보이는 클래스 파일의 구성 요소

클래스 파일 · 바이트코드 · 상수 풀 · 디스크립터 · 속성 (클래스 파일) · Code 속성 · LineNumberTable · LocalVariableTable · 디버깅 정보 · 인스턴스 초기화 메서드 · kotlin.Metadata

javap 를 싣고 오는 JDK 모듈과 도구

JDK · jdk.jdeps · jdeps · jdeprscan · ToolProvider

javap 에 넘길 클래스 파일을 만드는 컴파일러

javac · 컴파일러 · 코틀린 · 코틀린 컴파일러 · 익명 클래스

javap 가 클래스를 찾으러 가는 경로

클래스 경로 · CLASSPATH · 모듈 · 모듈 경로 · 시스템 모듈 · JAR · 멀티 릴리스 JAR

javap 출력의 명령어를 정의하고 실행하는 가상 머신

JVM · JVMS · 런타임 상수 풀 · 피연산자 스택 · invokespecial · invokevirtual

javap 처럼 클래스 파일 정보를 읽는 도구와 API

역어셈블러 · 디컴파일러 · 디버거 · 리플렉션

다른 이름: javap 명령