개발 일지

[Java] 바이트 코드(2) :: 바이트 코드 예제 본문

Java

[Java] 바이트 코드(2) :: 바이트 코드 예제

junjun_ 2022. 11. 13. 00:22

[Java] 바이트 코드(1) :: 바이트 코드란?

 

[Java] 바이트 코드(1) :: 바이트 코드란?

자바 스터디 3주차 발표 준비 자료 입니다 https://github.com/WanOnPreStudy/JavaQuestionStudy GitHub - WanOnPreStudy/JavaQuestionStudy: 백엔드 자바 질문 스터디 백엔드 자바 질문 스터디. Contribute to WanOnPreStudy/JavaQuesti

jueun275.tistory.com

앞에서 바이트 코트란 무엇이고 어떻게 확인하는지, 그리고 간단한 동작을 살펴보았습니다.

이번에는 좀 더 많은 예제들을 자세히 정리하며 바이트 코드의 동작을 알아볼까 합니다.


JVM 스펙에서는 class영역에 실행코드를 갖고 있으며, method 호출 시 각 method 별로 별도의 stack frame이 생성되고 frame안에는 operand stack, local variable array, constant pool의 reference 포함되어 있습니다. (너무 자세히 들어가면 바이트 코드 글이 아니라 JVM 글이 되어버리기 때문에 여기서는 이런 게 있다 정도만 정리하겠습니다.) 

먼저 operand stack, local variable array에 대해서 알아보도록 하겠습니다. 

 

local variable array

  • 0부터 시작하는 인덱스를 가진 지역변수 배열 입니다.
  • 0번 인덱스는 항상 메서드가 속한 클래스 인스턴스의 this 레퍼런스입니다 (static 메서드 제외)
  • 1부터는 메서드에 전달된 파라미터들이 저장되며, 메서드 파라미터 이후에는 메서드의 지역 변수들이 저장됩니다.

operand stack

  • 메서드의 실제 작업 공간입니다.
  • 각 메서드는 operand stack과 local variable array사이에서 데이터를 교환하고, 다른 메서드 호출 결과를 추가하거나(push) 꺼냅니다(pop).
  • operand stack공간이 얼마나 필요한지는 컴파일할 때 결정할 수 있으므로, operand stack의 크기도 컴파일 시에 결정됩니다.

 

예제를 보면서 operand stack와 local variable array살펴보겠습니다.

spin함수

    void spin(){
        int i;
        for(i=0; i<100; i++){
            // empty
        }
    }

 

spin함수 bytecode

아래와 같이 locals variable array과 operand stack사이에서 데이터의 교환이 일어나는 것을 볼 수 있습니다.

코드 옆에 그림에서 local은 locals variable array, stack은 operand stack을 의미합니다. 

spin() bytecode 동작

명령어 Stack
( before → after)
설명
iconst_0 → value int 0을 스택(Operand stack)에 push
istore_1 value → stack에서 pop해서 local variable array 인덱스 1번에 저장 ( 0번은 this)
iload_1 → value  local variable array 인덱스 1에서  int 값을 가져와서 스택에  push
bipush   100 → value
byte를 int형으로스택에  push (100)
if_icmpge   14 value1, value2 → value1이 value2보다 크거나 같으면 해당 명령어로 이동합니다. (14번)
iinc    1, 1 [No change] 지역 변수 #index를 부호 있는 바이트로 증가 (지역변수 1을 1증가)
goto   2 [No change] 해당 명령어로 이동 (2번)
return → [empty] 메서드에서 void 반환

 

 

이번에는 int를 double로만 바꿔서 어떤 차이가 있는지 확인해 보겠습니다.

dspin함수

    void dspin(){
        double i;
        for(i=0.0; i<100.0; i++){
            //empty
        }
    }

 

dspin함수 bytecode와 spin함수 bytecode

int형에서 double형으로 자료형만 바뀌었지 만 바이트 코드는 바뀐 부분이 많아 보입니다. 하나하나 보겠습니다.

1. stack, locals 공간 차이

stack=2, locals=2, args_size=1	//int

stack=4, locals=3, args_size=1	//double

가장 먼저 보이는 차이는 stack, locals의 공간 차이입니다. 이는 int가 하나의 공간을 사용한 것에 비해 double은 두 개의 공간을 사용하기 때문입니다.

2. constant pool 참조

bipush 100	//int

ldc2_w  #2	//double

stack에 값을 push 할 때 int는 bipush을 사용하고 double은 ldc 명령어(ldc, ldc_w, ldc2_w)를 사용하는 것을 볼 수 있습니다.  bipush는 값을 바로 스택에 push 하는 반면에 ldc는 인덱스 값으로 constant pool의 값을 참조합니다.

double값을 ldc를 사용하여 참조하는 이유는  0.0과 1.0을 제외하고 실수를 스택에  즉시를 배치(push)하는 바이트 코드 명령이 없기 때문입니다

물론 모든 int형을 바로 stack에 push 할 수 있는 것은 아닙니다. 일정 수 이상의 큰 수는 마찬가지로 constant pool에서 참조하는데 여기에 대해서는 밑에서 constant pool에 대해서 설명할 때 다시 언급하겠습니다.

 

3. 비교 연산,  4. 증감 연산

명령어 개수의 차이가 나지만 둘은 같은 동작입니다.

double의 명령어의 개수가 많은 이유는 또한 실수를 바로 처리할 수 있는 명령어가 없기 때문입니다.

 

dspin() bytecode 동작

명령어 Stack
( before → after)
설명
dconst_0  → value double 0.0을 스택(Operand stack)에 push
dstore_1 value → stack에서 pop해서 local variable array 인덱스 1에 저장
dload_1  → value  local variable array 인덱스 1에서  int 값을 가져와서 스택에  push
ldc2_w  #2  → value constant pool 에서부터 #index 에 해당하는 constant 를 가져와 스택에 push
dcmpg value1, value2 → result 두 double값을 비교합니다.
value1이 value2보다 크면 int 값 1이 스택에 push
value1과 value2이 값 0이 스택에 push
value1이 value2보다 작으면 int 값 -1이 스택에 push
ifge   17 value → 값이 0보다 크거나 같은 경우 해당 명령어로 이동 (17번)
dload_1  → value  local variable array 인덱스 1에서  int 값을 가져와서 스택에  push
dconst_1  → value double 1.0을 스택(Operand stack)에 push
dadd value1, value2 → result 스택의 double값 두개를 더해서 결과를 다시 스택에 저장
dstore_1 value → stack에서 pop해서 local variable array 인덱스 1에 저장
goto   2 [No change] 해당 명령어로 이동 (2번)
return → [empty] 메서드에서 void 반환

 

 

Constant pool

Constant pool는 클래스의 코드를 실행하는 데 필요한 상수들을 포함하는 메모리 영역입니다.

위에서 숫자가 큰 정수(int)와 실수(double)는 operand stack에 바로 값을 push하지 않고 constant pool의 값을 올려놓고 인덱스 값으로 참조한다고 했습니다. 관련 예제를 살펴보겠습니다.

 

void useManyNumberic()

    void useManyNumberic(){
        int a = 10;
        int b = 100000;
        long l1 = 1;
        long l2 = 0xffffff;
        double d = 2.2;
    }

 

void useManyNumberic() byte코드와  constant pool(constant pool은 관련 부분 외에는 생략)

 

위 바이트 코드를 보면 a, b는 둘 다 int임에도 불구하고 a는 stack에 바로 push(bipush), b는 constant pool에서 가져오는 것을(ldc) 확인할 수 있는데 이유는 아래와 같습니다.

 

ORACLE The Structure of the Java Virtual Machine 2.3.1

 

bipush는 byte를 int형으로 push 한다는 명령어입니다. 위에 글을 보면 byte는 -128 ~ 127 범위의 숫자를 가질 수 있습니다. byte범위 이상의 숫자는 bipush가 아닌 sipush(sort를 int형으로 push)를 사용합니다. 하지만 sort의 범위인  -32,768 ~ 32,767 도 벗어나는 수는 어떤 명령어를 사용할까요? 이 범위 이상의 수 를 스택에 push 하는 명령어는 없습니다.

자바 바이트코드 명령어 OpCode는 최대 256개로 제한되어 있기 때문에(바이트코드 1에서 설명) stack에 push 하는 명령어는 bipush, sipush만 존재합니다. 명령어가 없기 때문에 sort범위 이상의 수는 constant pool에 값을 올려놓고 참조하는 것입니다. 또한 long, double도 0, 1( double은 0.0, 1.0)을 제외하고는 stack에 push 하는 명령어가 없기 때문에 0,1을 제외한 모든 수는 constant pool로 넣고 인덱스 값으로 참조하는 것입니다.

 

위 이유를 적용하면 왜 어떤 수는 바로 stack에 push 하고, 어떤 수는 constant pool에서 가져오는지 알 수 있습니다.

 

상수 풀에서 스택으로 상수를 push 하는 명령어는 3가지입니다

  • ldc: constant pool(cp)에서 String, int, float, Class 등등의 을 가져옵니다. (constant pool #index < 1byte)
  • ldc_w: constant pool에서 String, int, float, Class 등등의 을 가져옵니다. (1byte < cp #index <2byte)
  • ldc2_w: constant pool에서 double, long값을 가져옵니다.

 

args_size

stack=2, locals=2, args_size=1에서 stack과 locals를 알아봤으니 이번에는 args_size을 알아보겠습니다.

지금 까지 예제에서 stack과 locals는 변했지만 args_size는 계속 1로 변하지 않았습니다.  args_size가 나타내는 것은 함수로 전달된 인자의 수입니다. 위 예제의 함수 모두 인자를 받지 않았기 때문에 계속 args_size=1이었던 것입니다.

왜 인자를 받지 않은데 0이 아니라 1인 걸까요? 위에서 locals에 0번 인덱스는 항상 this라고 했었는데 args_size=1은 바로 이 this 레퍼런스인 것입니다 ( 인스턴스 메서드는 항상 this레퍼런스가 전달됩니다. static메서드는 인스턴스가 생성되지 않기 때문에 this가 전달되지 않습니다.) 

 

이 부분에서는 

1. 인자를 받는 함수

2. 인스턴스가 필요 없는 static 함수 

 

두 개의 함수를 확인하면 더 이해하기 쉬울 것입니다.

 

1. 인자를 받는 함수

int addTwo(int, int)

   int addTwo(int i, int j){
        return i + j;
   }

int addTwo(int, int) bytecode

두 개의 인자를 받는 함수는 args_size=3 인 것을 확인할 수 있습니다 (this, int i, int j)

 

2. static 함수

Static getRandomNumber()

    static int getRandomNumber() {
        return (int)(Math.random() * 100);
    }

Static getRandomNumber() bytecode

인자를 받지 않는 Static함수는 args_size=0 인 것을 확인할 수  있습니다.

 

 

 

메서드 호출

지금까지는 해당 함수 안에서만 살펴보았습니다. 함수를 호출하는 명령어는 다음과 같습니다.

  • invokeinterface: 인터페이스 메서드 호출
  • invokespecial: 생성자, private 메서드, 슈퍼 클래스의 메서드 호출
  • invokestatic: static 메서드 호출
  • invokevirtual: 인스턴스 메서드 호출
public class Test {
    public static void main(String[] args) {
        String str = "hello word";
        System.out.print(str);
    }
 }

 

정리

바이트 코드 예제를 통해서 바이트 코드가 어떻게 동작하는지 정리해 보았습니다

정리하면서 코드의 동작을 좀 더 내부적으로 살펴볼 수 있어서 좋았습니다.

더 많은 예제들을 보고 싶으면 아래 글을 참고해 봐도 좋을 것 같습니다.

https://docs.oracle.com/javase/specs/jvms/se10/html/jvms-3.html#jvms-3

 

Chapter 3. Compiling for the Java Virtual Machine

The Java Virtual Machine machine is designed to support the Java programming language. Oracle's JDK software contains a compiler from source code written in the Java programming language to the instruction set of the Java Virtual Machine, and a run-time sy

docs.oracle.com

 

 

 

reference

'Java' 카테고리의 다른 글

[Java] Annotation  (0) 2022.11.23
[Java] try-with-resources  (0) 2022.11.16
[Java] 바이트 코드(1) :: 바이트 코드란?  (1) 2022.11.10
[Java] Checked Exception, Unchecked Exception, Error  (0) 2022.11.07
[Java] final 키워드  (0) 2022.11.04