1주차 과제: JVM은 무엇이며 자바 코드는 어떻게 실행하는 것인가
목표
자바 소스 파일(.java)을 JVM으로 실행하는 과정 이해하기
학습할 것
- JVM이란 무엇인가
- 컴파일 하는 방법
- 실행하는 방법
- 바이트코드란 무엇인가
- JIT 컴파일러란 무엇이며 어떻게 동작하는지
- JVM 구성 요소
- JDK와 JRE의 차이
JVM이란 무엇인가
자바 바이트코드는 JRE 위에서 동작합니다. JVM(Java Virtual Machine)이란, 자바 바이트코드를 해석하고 실행하는 요소입니다. JRE는 자바 API와 JVM으로 구성되며, JVM의 역할은 자바 애플리케이션을 클래스 로더(Class Loader)를 통해 읽어 들여서 자바 API와 함께 실행하는 것입니다.
자바 바이트코드란,
자바와 기계어 사이의 중간 언어라 할 수 있고,
자바 코드를 배포하는 가장 작은 단위.
JVM 특징
- JVM은 스택기반으로 동작합니다.
- 기본 자료형을 제외한 모든 타입(클래스와 인터페이스)을 명시적인 메모리 주소 기반의 레퍼런스가 아니라 심볼릭 레퍼런스를 통해 참조한다.
- 클래스 인스턴스는 사용자 코드에 의해 명시적으로 생성되고 가바지 컬레션(garbage collection)에 의해 자동으로 파괴된다.
- C/C++ 등의 전통적인 언어는 플랫폼에 따라 int 형의 크기가 변합니다.
하지만 JVM은 기본 자료형을 명확하게 정의하여 호환성을 유지하고 플랫폼 독립성을 보장합니다. - 자바 클래스 파일은 네트워크 바이트 오더를 사용합니다.
JVM의 가장 중요한 특징은 C/C++ 등의 컴파일러처럼 바로 기계어로 변환하는 것이 아닌 JVM이 이해하는 자바 바이트코드로 변역해줍니다. 즉, 자바 바이트코드는 플랫폼 의존적인 코드가 없기 때문에 JVM(정확히는같은 프로파일의 JRE)이 설치된 장비라면 CPU나 OS가 다르더라도 실행할 수 있고, 컴파일 결과물의 크기가 소스코드의 크기와 크게 다르지 않으므로 네트워크로 전송하여 실행하기가 쉽습니다.
다만, 자바 언어를 컴파일해서 바로 기계어로 변환해주는 것이 아닌 자바 바이트코드를 한번 거치기 때문에 C/C++ 등의 언어보다는 수행 속도가 느리다는 단점이 있습니다. 이러한 단점을 보완하기 위해 JIT 컴파일러를 사용하고 있습니다.
컴파일하는 방법 & 실행하는 방법
자바 소스(.java)
먼저 간단한 소스코드의 자바 소스파일을 생성합니다. 저는 notepad로 작성하였습니다.
public class javaByteCodeDisplay {
public static void main(String[] args) {
int a = 1;
int b = 2;
int sum = a + b;
System.out.println(sum);
}
}
컴파일(.class)
java 파일을 준비하고 javac로 컴파일합니다. 해당 파일의 class 파일이 생성됩니다. 컴파일 또는 실행하실 때는 CMD에서 해당 파일이 있는 경로에서 실행해주어야 합니다.
javac javaByteCodeDisplay.java
실행
java javaByteCodeDisplay
바이트코드 들춰보기
역어셈블러(javap)로 해당 클래스를 실행하면 바이트코드가 출력됩니다.
javap -v -p -s javaByteCodeDisplay.class
바이트코드
Classfile /D:/ChaeMinSu/notepad/javaByteCodeDisplay.class
Last modified 2021. 9. 15; size 428 bytes
MD5 checksum 87a960d121ddca9ccf2a6e90eb284070
Compiled from "javaByteCodeDisplay.java"
public class javaByteCodeDisplay
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#14 // java/lang/Object."<init>":()V
#2 = Fieldref #15.#16 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #17.#18 // java/io/PrintStream.println:(I)V
#4 = Class #19 // javaByteCodeDisplay
#5 = Class #20 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 main
#11 = Utf8 ([Ljava/lang/String;)V
#12 = Utf8 SourceFile
#13 = Utf8 javaByteCodeDisplay.java
#14 = NameAndType #6:#7 // "<init>":()V
#15 = Class #21 // java/lang/System
#16 = NameAndType #22:#23 // out:Ljava/io/PrintStream;
#17 = Class #24 // java/io/PrintStream
#18 = NameAndType #25:#26 // println:(I)V
#19 = Utf8 javaByteCodeDisplay
#20 = Utf8 java/lang/Object
#21 = Utf8 java/lang/System
#22 = Utf8 out
#23 = Utf8 Ljava/io/PrintStream;
#24 = Utf8 java/io/PrintStream
#25 = Utf8 println
#26 = Utf8 (I)V
{
public javaByteCodeDisplay();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: istore_3
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: iload_3
12: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
15: return
LineNumberTable:
line 3: 0
line 4: 2
line 5: 4
line 6: 8
line 7: 15
}
SourceFile: "javaByteCodeDisplay.java"
- Constant pool: 클래스 / 인스턴스의 상수, 메소드와 필드에 대한 레퍼런스가 저장됩니다.
- Stackframe에서 Constant pool을 참조합니다.
- Local Variable Array는 로컬 변수를 담고 있는 배열입니다.
- invokeinterface: 인터페이스 메소드 호출
- invokespecial: 생성자, private 메소드, 슈퍼 클래스의 메소드 호출
- invokestatic: static 메소드 호출
- invokevirtual: 인스턴스 메소드 호출
java/io/PrintStream.println:(I)V
이 코드에서 마지막에 'V'의 뜻은 "반환값이 없다" 입니다. ('V'에 대한 설명은 네이버 기술블로그를 참고하시면 됩니다.)
0: aload_0
1: invokespecial #1
- 코드 앞에 숫자는 바이트 번호를 의미합니다.
- aload_0, invokespecial 같은 바이트코드 명령어 OpCode들은 1바이트의 바이트 번호로 표현됩니다.
바이트코드란 무엇인가
앞에서 설명했지만 바이트코드란 자바 언어와 기계어 사이의 중간 언어이며, 자바 코드를 배포하는 가장 작은 단위입니다. (보통 바이트 코드라고 바이트하고 한 칸 띄어쓰기를 하는데 이건 올바르지 않다고 합니다. 올바른 단어: 바이트코드, 붙여서 씁니다.)
JIT 컴파일러란 무엇이며 어떻게 동작하는지
앞에서 JVM 단점의 보완으로 나온 것이 JIT 컴파일러가 있다고 했습니다. 자바 컴파일 방식은 바이트코드를 한 줄 한 줄씩 읽는 인터프리터 방식인데 이러면 단점이 속도가 느립니다. 그래서 이 인터프리터의 단점을 보완하기 위해 도입한 것이 JIT(Just-In-Time) 컴파일러입니다.
인터프리터 방식으로 실행하다가 적절한 시점에 바이트코드 전체를 컴파일하여 네이티브 코드로 변경하고, 이후에는 해당 메소드를 더 이상 인터프리팅하지 않고 네이티브 코드로 직접 실행하는 방식입니다. 네이티브 코드를 실행하는 것이 하나씩 인터프리팅하는 것보다 빠르고, 네이티브 코드는 캐시에 보관하기 때문에 한번 컴파일된 코드는 계속 빠르게 수행되게 됩니다.
JIT 컴파일러가 컴파일하는 과정은 바이트코드를 하나씩 인터프리팅하는 것보다 훨씬 오래 걸리므로, 만약 한 번만 실행되는 코드라면 컴파일하지 않고 인터프리팅하는 것이 훨씬 유리합니다. 따라서 JIT 컴파일러를 사용하는 JVM들은 내부적으로 해당 메소드가 얼마나 자주 수행되는지 체크하고, 일정 정도를 넘을 때에만 컴파일을 수행한다.
그러면 어떻게 동작할까.
JIT 컴파일러는 바이트코드를 일단 중간 단계의 표현인 IR로 변환하여 최적화를 수행하고 그 다음에 네이티브 코드를 생성합니다.
바이트코드와 같은 중간 언어를 도입하고, VM이 바이트코드를 실행하며, JIT 컴파일러 등으로 성능을 향상시키는 기법입니다.
JVM 구성 요소
클래스 로더(Class Loader)가 컴파일된 자바 바이트코드를 런타임 데이터 영역(Runtime Data Areas)에 로드하고, 실행 엔진(Execution Engine)이 자바 바이트코드를 실행한다.
더 자세한 설명은 네이버 기술블로그를 참고하세요! 설명이 참 잘되어있습니다.
클래스 로더
자바는 동적 로드, 즉 컴파일타임이 아니라 런타임에 클래스를 처음으로 참조할 때 해당 클래스를 로드하고 링크하는 특징이 있습니다. 그래서 이 동적 로드를 담당하는 부분이 JVM의 클래스 로더입니다. 클래스 로더의 특징은 다음과 같습니다.
- 계층 구조: 클래스 로더끼리 부모-자식 관계를 이루어 계층 구조로 생성된다. 최상위 클래스는 부트스트랩 클래스 로더(Bootstrap Class Loader)입니다.
- 위임 모델: 계층 구조를 바탕으로 클래스 로더끼리 위임하는 구조로 동작합니다.
- 가시성(visibility) 제한: 하위 클래스 로더는 상위 클래스 로더의 클래스를 찾을 수 있지만, 그 반대는 찾을 수 없습니다.
- 언로드 불가: 클래스 로더는 로드할 수는 있지만 언로드할 수는 없습니다.
각 클래스 로더는 로드된 클래스들을 보관하는 네임스페이스(namespace)를 갖습니다. 클래스를 로드할 때 이미 로드된 클래스인지 확인하기 위해서 네임스페이스에 보관된 FQCN(Fully Qualified Class Name)을 기준으로 클래스를 찾습니다. 비록 FQCN이 같더라도 네임스페이스가 다르면, 즉 다른 클래스 로더가 로드한 클래스이면 다른 클래스로 간주됩니다.
FQCN(Fully Qualified Class Name)이란,
클래스가 속한 패키지명을 모두 포함한 이름을 말합니다.
ex) java.lang.String
런타임 데이터 영역
런타임 데이터 영역은 JVM이라는 프로그램이 OS 위에서 실행되면서 할당받는 메모리 영역입니다. 총 6개의 영역으로 나눌 수 있고 PC 레지스터, JVM 스택, 네이티브 메소드 스택은 쓰레드마다 하나씩 생성되며 힙, 메소드 영역, 런타임 상수 풀은 모든 스레드가 공유해서 사용합니다.
- 메소드 영역: 메소드 영역은 모든 스레드가 공유하는 영역으로 JVM이 시작될 때 생성됩니다. JVM이 읽어들인 각각의 클래스와 인터페이스에 대한 런타임 상수 풀, 필드와 메소드, Static 변수, 메소드의 바이트코드 등을 보관합니다.
- 런타임 상수 풀: 클래스 파일 포맷에서 constant_pool 테이블에 해당하는 영역입니다. 어떤 메소드나 필드를 참조할 때 JVM은 런타임 상수 풀을 통해 해당 메소드나 필드의 실제 메모리상 주소를 찾아서 참조합니다.
- 힙: 인스턴스 또는 객체를 저장하는 공간으로 가비지 컬렉션의 대상입니다.
각각의 클래스 인스턴스들이 힙에 할당되고,
User, UserAdmin, String 등의 클래스 정보가 메소드 영역에 보관될 것입니다.
실행 엔진
클래스 로더를 통해 JVM 내의 런타임 데이터 영역에 배치된 바이트코드는 실행 엔진에 의해 실행됩니다. 실행 엔진은 자바 바이트코드를 명령어 단위로 읽어서 실행합니다. 바이트코드의 각 명령어는 1바이트짜리 OpCode와 추가 피연산자로 이루어져 있으며, 실행 엔진은 하나의 OpCode를 가져와서 피연산자와 함께 작업을 수행한 다음, 다음 OpCode를 수행하는 식으로 동작합니다.
실행 엔진은 바이트코드를 실제로 JVM 내부에서 기계가 실행할 수 있는 형태로 변경하며, 그 방식은 두 가지가 있습니다.
- 인터프리터 방식: 바이트코드 명령어를 하나씩 읽어서 해석하고 실행합니다. 기본적으로 바이트코드라는 '언어'는 인터프리터 방식으로 동작하며, 하나하나씩 해석하기 때문에 실행 속도가 느리다는 단점이 있습니다.
- JIT 컴파일러 방식: 앞서 설명했지만 인터프리터 방식의 실행 속도가 느리다는 점을 보완해서 나온 것이 JIT 컴파일러입니다. JIT 컴파일러는 바이트코드를 컴파일하여 네이티브 코드로 변경하고 한번 컴파일된 코드는 캐시에 보관하기 때문에 실행 속도가 빠릅니다.
JDK와 JRE의 차이
JRE(Java Runtime Environment)는 자바 가상 머신, 자바 클래스 라이브러리, 자바 명령 및 기타 인프라를 포함한 컴파일된 Java 프로그램을 실행하는데 필요한 패키지입니다.
JDK(Java Development Kit)는 Java를 사용하기 위한 필요한 모든 기능을 갖춘 Java용 SDK(Software Development Kit)입니다. JRE에 있는 모든 것뿐만 아니라 컴파일러(javac), jdb, javadoc과 같은 도구도 있습니다. 즉 JDK는 프로그램을 생성하고 컴파일할 수 있습니다.
정리하면, JDK는 JRE를 포함하고 있습니다.
결국엔 우리는 자바 프로그래밍을 해야하기 때문에 JDK를 설치하면 됩니다!
하지만 자바 프로그래밍을 안해도 JRE가 아닌 JDK를 설치해야 하는 경우가 있다면 그것은 JSP를 이용하여 웹 어플리케이션을 배포하는 경우에는 내부적으로 JSP는 자바 servlet기반으로 돌기 때문에 이런 경우에는 JDK를 설치해야 합니다.
UnsupportedClassVersionError
사용하고 있는 자바 버전에서 컴파일한 후에 사용하고 있는 자바 버전보다 더 낮은 버전으로 바이트코드를 실행시 발생하는 에러 메세지입니다.
javac 옵션
- classpath(cp): 컴파일 시 필요로 하는 참조할 클래스 파일의 파일 경로를 지정
javac -classpath [참조파일 경로] [소스파일.java]
javac -cp [참조파일 경로] [소스파일.java]
- d: 클래스 파일을 생성할 루트 디렉토리 설정
javac -d [루트 디렉토리] [소스파일.java]
- encoding: 소스 파일에 사용된 문자열 설정
javac -encoding [인코딩셋] [소스파일.java]
- g: 디버깅 정보 옵션
javac -g:none //디버깅 정보를 생성하지 않음
javac -g:line(라인정보),var(지역변수),source(소스파일정보)
- nowarn: 경고 메세지 관련 옵션, 이 옵션을 사용할 경우 경고 메세지를 생성하지 않음
- sourcepath: 소스파일의 위치를 지정
References
'Java' 카테고리의 다른 글
[자바 라이브 스터디] 03. 연산자 (0) | 2021.09.16 |
---|---|
[자바 라이브 스터디] 02. 자바 데이터 타입, 변수와 배열 (0) | 2021.09.16 |
[자바 라이브 스터디] 00. 자바 기본 공부 계획 (0) | 2021.09.13 |
간략한 JVM에 대한 개념.. (0) | 2021.05.17 |
클래스 활용 (0) | 2020.09.09 |
댓글