본문 바로가기

Programming/Java

Dump Java byte code from Java class file (Java byte code 보기)

이 포스팅의 일부는 IBM Developerworks의 Java bytecode 기사의 일부를 발췌하여 작성하였다.


Java compiler는 Java source code를 Java byte code로 해석을 한다.

Java byte code는 C/C++ 프로그램의 중간 표현식이라고 할 수 있는 assembler 에 해당한다.

유능한 C/C++ 프로그래머는 Debugging과 성능메모리 사용을 tuning하기 위해서 assembler instruction 정보를 사용한다. 이는 Java Programmer가 Java bytecode의 instruction의 의미를 이해하는 것과 같은 맥락이다.

그러므로, 좀더 고급 Java 프로그래머가 되기 위해서는 Java byte code에 대한 이해가 필요한다.



Java bytecode를 생성하는 방법은 2가지 방법이 있겠다.

1. 첫 번째로 JDK에서 제공하는 program인 javac, javap 를 사용하는 방법

2. 두 번째로 Eclipse plug-in (Java bytecode outline)혹은 다른 플러그인을 이용하여 생성하는 방법


1. javac, javap 를 사용하여 생성하려면 아래와 같은 단계를 거치면 된다.


A. Java source를 컴파일 한다.

$ javac StringCatTest.java


B. 컴파일 된 class 파일을 javap를 이용하여 bytecode를 생성한다.

$ javap -c StringCatTest > StringCatTest.bc

위에서 ">" 는 오른쪽 파일로 standard out을 redirecting 하겠다는 의미이다. redirecting에 대하여 궁금하다면, Linux의 redirect 를 참고 하면 되겠다.


이 과정을 거치면 아래와 같은 bytecode를 살펴 볼 수 있다.


Compiled from "StringCatTest.java"
public class test.StringCatTest extends java.lang.Object{
public test.StringCatTest();
  Code:
   0:    aload_0
   1:    invokespecial    #8; //Method java/lang/Object."<init>":()V
   4:    return

public java.lang.String concat(java.lang.String, java.lang.String);
  Code:
   0:    new    #16; //class java/lang/StringBuilder
   3:    dup
   4:    aload_1
   5:    invokestatic    #18; //Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
   8:    invokespecial    #24; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
   11:    aload_2
   12:    invokevirtual    #27; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   15:    invokevirtual    #31; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
   18:    areturn

}

여기서 나오는 instruction 중에서 "a" 혹은 "i" 라는 prefix는 instuction이 다루고 opcode의 type을 의미힌다.

"a" 는 reference를 의미하고, "i" 는 integer 를 의미한다.

bytecode를 보다 더 자세히 이해하고 싶으면, JVM이 bytecode를 어떻게 실행시키는 지를 살펴보면 된다.

하지만 이 주제는 이번 포스팅의 주제를 넘는 것이므로 더 알고 싶으면 아래 링크를 통해서 정보를 얻을 수 있다.

http://www.ibm.com/developerworks/ibm/library/it-haggar_bytecode/


2. 두 번째 방법인, Eclipse plug-in 인 bytecode outline 을 이용하여 bytecode의 대략적인 모습을 살펴보는 것이다.

아래 사이트에서 eclipse version 별로 bytecode outline 을 다운로드 받아서 설치 할 수 있는 안내를 찾을 수 있다.

http://andrei.gmxhome.de/bytecode/index.html


설치하여 아래와 같은 Java source code는 다음과 같이 변환되는 것을 볼 수 있다.

Java source code


package test;

public class StringCatTest {
    public String concat(String a, String b) {
        return a + b;
    }
}


Java bytecode


// class version 50.0 (50)
// access flags 0x21
public class StringCatTest {

  // compiled from: StringCatTest.java

  // access flags 0x1
  public <init>() : void
   L0
    LINENUMBER 3 L0
    ALOAD 0: this
    INVOKESPECIAL Object.<init>() : void
    RETURN
   L1
    LOCALVARIABLE this StringCatTest L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1
  public concat(String,String) : String
   L0
    LINENUMBER 5 L0
    NEW StringBuilder
    DUP
    ALOAD 1: a
    INVOKESTATIC String.valueOf(Object) : String
    INVOKESPECIAL StringBuilder.<init>(String) : void
    ALOAD 2: b
    INVOKEVIRTUAL StringBuilder.append(String) : StringBuilder
    INVOKEVIRTUAL StringBuilder.toString() : String
    ARETURN
   L1
    LOCALVARIABLE this StringCatTest L0 L1 0
    LOCALVARIABLE a String L0 L1 1
    LOCALVARIABLE b String L0 L1 2
    MAXSTACK = 3
    MAXLOCALS = 3
}


첫 번째 방법으로 생성한 Java bytecode와 두 번째 방법으로 생성한 Java bytecode 는 약간 다르다는 사실을 알 수 있다. 개인적으로는 첫 번째 방법이 좋아보이지만, 매번 javap 프로그램을 사용하여 살펴보는 방법은 다소 귀찮을 수 있으므로, Eclipse plug-in으로 제공되는 Java bytecode outline을 이용하는 것이 생산성 향상에 더 좋은 것 같다.



마지막으로 포스트를 마치기 전에 코드를 어떻게 작성하느냐에 따라 성능의 차이를 알 수 있는 간단한 예를 들어보겠다.

아래 코드는 동일한 일을 하는 두가지 method (top1, top2) 이다.


public synchronized int top1()
{
  return intArr[0];
}

public int top2()
{
 synchronized (this) {
  return intArr[0];
 }
}

하지만 두 버전의 bytecode 를 살펴보면 엄청난 차이를 알 수 있다.

Method int top1()
   0 aload_0           //Push the object reference(this) at index
                       //0 of the local variable table.
   1 getfield #6 <Field int intArr[]>
                       //Pop the object reference(this) and push
                       //the object reference for intArr accessed
                       //from the constant pool.
   4 iconst_0          //Push 0.
   5 iaload            //Pop the top two values and push the
                       //value at index 0 of intArr.
   6 ireturn           //Pop top value and push it on the operand
                       //stack of the invoking method. Exit.

Method int top2()
   0 aload_0           //Push the object reference(this) at index
                       //0 of the local variable table.
   1 astore_2          //Pop the object reference(this) and store
                       //at index 2 of the local variable table.
   2 aload_2           //Push the object reference(this).
   3 monitorenter      //Pop the object reference(this) and
                       //acquire the object's monitor.
   4 aload_0           //Beginning of the synchronized block.
                       //Push the object reference(this) at index
                       //0 of the local variable table.
   5 getfield #6 <Field int intArr[]>
                       //Pop the object reference(this) and push
                       //the object reference for intArr accessed
                       //from the constant pool.
   8 iconst_0          //Push 0.
   9 iaload            //Pop the top two values and push the
                       //value at index 0 of intArr.
  10 istore_1          //Pop the value and store it at index 1 of
                       //the local variable table.
  11 jsr 19            //Push the address of the next opcode(14)
                       //and jump to location 19.
  14 iload_1           //Push the value at index 1 of the local
                       //variable table.
  15 ireturn           //Pop top value and push it on the operand
                       //stack of the invoking method. Exit.
  16 aload_2           //End of the synchronized block. Push the
                       //object reference(this) at index 2 of the
                       //local variable table.
  17 monitorexit       //Pop the object reference(this) and exit
                       //the monitor.
  18 athrow            //Pop the object reference(this) and throw
                       //an exception.
  19 astore_3          //Pop the return address(14) and store it
                       //at index 3 of the local variable table.
  20 aload_2           //Push the object reference(this) at
                       //index 2 of the local variable table.
  21 monitorexit       //Pop the object reference(this) and exit
                       //the monitor.
  22 ret 3             //Return to the location indicated by
                       //index 3 of the local variable table(14).
Exception table:       //If any exception occurs between
from to target type    //location 4 (inclusive) and location
 4   16   16   any     //16 (exclusive) jump to location 16.


위의 두 코드를 살펴 보면 top1 이 약 13% 정도의 성능향상이 기대 된다. 또한 코드의 양도 훨씬 적다. 이를 살펴보아도 성능이 중요한 Mobile (Android), Server 프로그램으로 작성이 되어야 한다면, bytecode를 분석함으로써 성능 향상을 꾀할 수 있겠다. Android 는 사실 dalvik 이라는 VM에서 Dex 라는 bytecode로 컴파일된 중간언어를 실행시킨다. 그러므로 이 포스트의 내용이 약간 상이할 수도 있을 거라고 생각한다. 하지만 큰 차이는 없을 거라고 생각하기 때문에, Java bytecode 의 대한 이해가 Java 고급 프로그래머가 되기 위한 Requirement 라고 생각한다.