본문 바로가기
아키텍쳐를 고민하기

JVM 이해하기 - 2 (자바의 바이트 코드)

by simplify-len 2020. 11. 24.

Write Once, Run anywhere(WORA) 

 

자바의 바이트 코드

WORA를 구현하기 위해 JVM은 사용자 언어인 자바와 기계어 사이의 중간 언어인 자바 바이트코드를 사용합니다. 이 자바 바이트코드가 자바 코드를 배포하는 가장 작은 단위입니다.

JVM을 이야기 할 때에는 자바 바이트코드를 빼놓을 수 없다. JVM은 자바 바이트코드를 실행하는 실행기이다. 자바 컴파일러는 C/C++등의 컴파일러처럼 고수준 언어를 기계어, 즉 직접적인 CPU명령으로 변환하는 것이 아니라, 개발자가 이해하는 자바 언어를 JVM이 이해하는 자바 바이트코드로 번역한다. 따라서 자바 바이트코드는 플랫폼 의존적인 코드가 없기 때문에 JVM이 설치된 장비라면 CPU나 운영체제가 다르더라도 실행할 수 있고, 컴파일 결과물의 크기가 소스코드의 크기와 크게 다르지 않으므로 네트워크로 전송하여 실행하기가 쉽다.

 클래스 파일 자체는 바이너리 파일이므로 사람이 이해하기 쉽지 않다. 이 점을 보완하기 위해 JVM 벤더들은 javap라는 역어셈블러(disassembler)를 제공한다. javap를 이용한 결과물을 흔히 자바 어셈블리라고 부른다. 앞의 사례에서 애플리케이션 코드 UserService.add() 메서드를 javap -c 옵션으로 역어셈블한 결과물은 다음과 같다.

public void add(java.lang.String);  
Code:  
0: aload_0  
1: getfield #15; //Field admin:Lcom/nhn/user/UserAdmin;  
4: aload_1  
5: invokevirtual #23; //Method com/nhn/user/UserAdmin.addUser:(Ljava/lang/String;)V  
8: return  

이 결과물에서 addUser() 메서드를 호출하는 부분은 4번째 줄인 "5: invokevirtual #23;"이다. 이는 23번 인덱스에 해당하는 메서드를 호출하라는 의미이며, 23번 인덱스의 메서드를 javap 프로그램이 친절하게 주석으로 달아주었다. invokevirtual은 자바 바이트코드에서 메서드를 호출하는 가장 기본적인 명령어의 OpCode(operation code)이다. 참고로, 자바 바이트코드에서 메서드를 호출하는 명령어 OpCode는 invokeinterface, invokespecial, invokestatic, invokevirtual의 4가지가 있으며 각각의 의미는 다음과 같다.

  • invokeinterface: 인터페이스 메서드 호출
  • invokespecial: 생성자, private 메서드, 슈퍼 클래스의 메서드 호출
  • invokestatic: static 메서드 호출
  • invokevirtual: 인스턴스 메서드 호출

자바 바이트코드의 명령어는 OpCode와 피연산자(Operand)로 분리할 수 있으며, invokevirtual과 같은 OpCode는 2바이트의 피연산자를 필요로 한다.

위의 애플리케이션 코드를 업데이트된 라이브러리로 다시 컴파일하여 다시 역어셈블하면 다음 결과를 얻을 수 있다.

public void add(java.lang.String);  
Code:  
0: aload_0  
1: getfield #15; //Field admin:Lcom/nhn/user/UserAdmin;  
4: aload_1  
5: invokevirtual #23; //Method com/nhn/user/UserAdmin.addUser:(Ljava/lang/String;)Lcom/nhn/user/User;  
8: pop  
9: return  

23번에 해당하는 메서드가 "Lcom/nhn/user/User;"를 반환하는 메서드로 변환된 것을 확인할 수 있다.

위의 역어셈블 결과물에서 코드 앞의 숫자는 무엇을 의미할까? 바로 바이트 번호이다. JVM이 실행하는 코드를 굳이 자바 "바이트"코드라고 하는 이유가 바로 이것일 것이다. 즉, 위의 aload_0, getfield, invokevirtual 같은 바이트코드 명령어 OpCode들은 1바이트의 바이트 번호로 표현된다. aload_0 = 0x2a, getfield = 0xb4, invokevirtual = 0xb6 등이다. 따라서, 자바 바이트코드 명령어 OpCode는 최대 256개라는 점을 알 수 있다.

aload_0, aload_1과 같은 OpCode는 피연산자가 필요 없다. 따라서 aload_0 바로 다음 바이트가 다음 명령어의 OpCode가 된다. 그러나 getfield, invokevirtual은 2바이트의 피연산자가 필요하다. 따라서 첫 번째 바이트에 있는 getfield의 다음 명령어는 2바이트를 건너뛴 네 번째 바이트에 기록된다. 위의 바이트코드를 Hex Editor로 보면 다음과 같다.

자바 바이트코드에서 클래스 인스턴스는 "L;", void는 "V"로 표시되는 것처럼 다른 타입들도 고유의 표현이 있다. 이 표현을 정리하면 다음과 같다.

 

[참조자료]

d2.naver.com/helloworld/1230

 

댓글