Live Study 1주차 - JVM은 무엇이며 자바 코드는 어떻게 실행하는 것인가


JVM이란 무엇인가

자바를 두 문장으로 설명하는 말이 있다.

Write once,
Run anywhere.

자바 언어로 개발된 프로그램은 , 운영체제가 리눅스이건, 윈도우이건, 맥OS이건 모두 실행가능한 특징을 비유하는 말이다. 이를 가능케하는 존재가 JVM(Java Virtual Machine) 이다.

컴퓨터에서 프로그램이 실행되려면, 컴퓨터가 읽을 수 있는 언어로 변환이 필요하다. 이렇게 변환된 언어를 기계어라고 한다. 자바에서는 기계어로 번역하는 역할을 JVM이라는 가상 머신에서 한다.

자바 프로그램을 실행하는 환경이 어떤 운영체제이든 JVM만 설치되어 있다면, 이 JVM에서 컴퓨터에 맞는 기계어로 번역하기 때문에 JDK가 설치된 환경이라면, 어디서나 자바 프로그램을 실행할 수 있게된다.

출처 : 남궁성 - Java의 정석

단, JVM은 java 코드를 직접 읽지 못하기 때문에 JVM이 읽을 수 있는 형태인 바이트코드로 컴파일해주어야 한다. 이 역할을 하는걸 자바 컴파일러라고 한다.


컴파일 및 실행하는 방법

개발자가 Java로 작성한 소스 코드가 컴퓨터에 의해 읽어들이기까지의 과정은 다음과 같다.

  1. Java 코드 작성하기
  2. 자바 컴파일러로 java 파일 컴파일하기 (= 바이트코드 생성)
  3. 바이트코드를 JVM에서 OS(운영체제)에 맞는 기계어(binary)로 변환하기
  4. 자바 애플리케이션 실행하기

Java 코드 작성하기

1
$ vim StudyHale.java
1
2
3
4
5
public class StudyHale {
public static void main(String[] args){
System.out.println("Hello World.");
}
}

컴파일하기 (bytecode 생성)

1
$ javac StudyHale.java

Java 프로그램 실행

1
2
$ java StudyHale
Hello World.

바이트코드란 무엇인가

컴파일 과정

JVM이 실행하는 명령어 형태이다. 컴퓨터에 의해 실행되려면 기계어로 번역되어야 하는데, JVM은 자바 코드를 읽지 못한다. 따라서 자바 컴파일러를 이용하여 JVM이 읽을 수 있는 형태인 바이트코드로 컴파일 해주어야한다.

자바 컴파일러에 의해 변환되는 코드의 명령어 크기가 1 byte라서 이를 바이트코드라한다.

그리고 JVM에서 이 바이트코드를 읽어들여 최종적으로 컴퓨터가 실행할 수 있는 기계어로 번역한다.

javap 명령어를 이용하면 바이트코드를 볼 수 있다. 아래 예제코드를 통해 확인해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;

public class StudyHalle01 {
public static void main(String[] args){
StudyHalle01 study = new StudyHalle01();

List<Integer> lotto = new ArrayList<Integer>();
for(int i=0; i<7; i++){
lotto.add(study.randomNumber());
}

System.out.println(getDate()+" 로또 번호는 입니다");
}

public int randomNumber(){
Random random = new Random();
int number = random.nextInt(45);
return number;
}

public static String getDate(){
Date time = new Date();
SimpleDateFormat date = new SimpleDateFormat("yyyy년 MM월 dd일");
return date.format(time);
}
}

위의 코드를 컴파일하고, javap 명령어로 컴파일하여 생성된 바이트코드를 확인해보겠다.

1
2
javac StudyHalle01.java
javap -c StudyHalle01

javap-c 옵션 명령어를 사용하면 위에서 보듯 역어셈블된 코드를 출력하는데, 이 때 보이는 한 줄 한 줄 명령어(Code)의 크기가 1 byte이다. 그래서 바이트코드라하는 것이다.

출처 : 위키백과 - 자바 바이트코드

인텔리제이에서 바이트코드를 확인할 수 있다. [View] - [Show Bytecode] 를 클릭하면, 바이트코드를 보여준다. 단, 프로그램이 빌드한 이력이 있어야 한다. 개발자가 코딩한 직후엔 아직 바이트코드를 생성하지 않았기 때문에 이 땐 바이트코드를 보여줄 수 없다.

위에서 작성한 클래스의 바이트코드.

출처 : 햄과함께IT - java bytecode 확인


JVM 구성요소

JVM

출처 : platformengineer.com - Understanding JVM Architecture

JVM은 크게 아래의 모듈들로 구성되어 있다.

클래스 로더 (Class Loader)

자바 컴파일러에 의해 컴파일된 자바 바이트코드(.class)들이 JVM에 의해 읽어들여지면, 클래스로더가 이 바이트코드를 운영체제(OS)로부터 적재받은 메모리 영역인 런타임 데이터 에어리어로 적재(Loading) 하는 일을 한다.

여기서 말하는 운영체제로부터 적재받은 메모리란, RAM 을 이야기한다. 이 RAM에 클래스 파일들이 올라가야만(Loading), 프로세스로서 실행될 수 있다.

또 하나, 런타임이란 실행시점을 의미한다. 이와 비슷하지만 다른 시점이 컴파일 시점이 있다.

  • 컴파일 타임
    • 자바 컴파일러에 의해 바이트코드가 생성되는 시점
  • 런타임
    • 자바 애플리케이션(기계어)을 실행하는 시점

에러가 없다면 좋겠지만, 에러를 확인해야 한다면 에러는 런타임이 아니라 컴파일 타임에서 확인되는게 가장 좋다. 에러를 누가 확인하는지를 생각하면 이해할 수 있다. 컴파일 타임은 개발자가 확인하지만, 런타임은 사용자도 볼 수 있기 때문이다. 따라서 배포되기 전에 개발자에게 발견되려면 아예 컴파일 시점에 발견되어 컴파일에 실패하는게 좋다.

Loading 단계에서는 클래스 파일의 main() 부터 메모리에 적재되는데, main() 이 실행되면서 필요한 객체들을 동적으로 로딩하게 된다.

자바 바이트코드는 JVM에서 기계어(바이너리 코드)로 변환된다고 하였는데, 이 때 코드가 통째로 기계어로 번역되는게 아니라 애플리케이션에 필요한 객체들이 먼저 JIT에 의해서 번역(Interpretion)된다.
이를 동적인 클래스 로딩(Java’s dynamic class loading) 이라고 한다.

클래스 로더에서는 메모리 영역에 객체 정보를 적재하는 역할 외에 적재된 클래스 파일의 에러를 검증하고 레퍼런스들을 연결하는 Linking 이라는 작업을 거친다.

마지막으로 초기화(Initialization)를 거침으로 클래스 로더의 역할이 끝이난다.

출처 : 당근케잌 - [Java] JVM Architecture란?


메모리 영역 (Runtime Data Area)

JVM이 운영체제(OS)로부터 적재받은 메모리 영역이다. 이곳에 클래스가 적재되어야지 자바 애플리케이션이 프로세스로써 컴퓨터에서 실행될 수 있다. 지금부터는 그냥 ‘메모리’라고 표현하겠다.

JVM의 메모리 영역은 다음으로 구성된다.

메서드 영역(Method Area)

JVM 내 하나만 존재하는 공유자원 이며, 메모리에 항상 상주하는 영역이기 때문에 JVM 내 스레드들이 메서드 영역의 자원을 공유하여 사용한다.

클래스에서 필요한 패키지 클래스, 런타임 상수풀, 인터페이스, 상수, static 변수, final 변수, 필드 데이터, 생성자 등의 모든 메서드 정보가 한 번 적재되면, 메모리 영역에서 항상 상주하게 된다.

JIT 컴파일러가 프로그램의 실제 실행시점에 실행에 필요한 바이트코드를 기계어로 번역하며, 다른 클래스에서 사용되는 클래스 정보를 메서드 영역에 적재해두고 공유하는 것이다.

메서드 영역에 Constant Pool이라는 영역이 존재하는데, 이곳에 상수값을 저장한다. 자바 애플리케이션 런타임시에 필요한 모든 종류의 숫자, 문자열, 식별자 이름, 클래스 파일, 메서드 정보들이 Constant Pool에 저장되는 데이터들이다.

출처 : 김석진 - JVM(Java Virtual Machine)이란


힙 영역(Heap Area)

메서드 영역과 마찬가지로 힙 영역도 JVM 내에 하나만 존재하는 공유자원이다. new 키워드로 생성된 객체의 데이터가 힙 영역에 적재된다.

메모리 영역(Method Area)에도 new 키워드로 생성한 클래스의 정보가 저장되는데, 힙 영역과 차이점이 있다면, 메모리 영역은 클래스 정보가 저장되는 곳이라면, 힙 영역은 실제 데이터가 저장 되는 곳이다.

실행 엔진(Execution Engine)에서 다시 설명하겠지만, 자바는 가비지 콜렉터(GC)가 존재하기 때문에 더 이상 사용되지 않는 인스턴스는 GC에 의해 제거되어 자동으로 메모리 관리가 이뤄지는데, 이 힙 영역에서 더 이상 참조되지 않는 메모리를 GC가 찾아서 제거해준다.

메모리 영역과 힙 영역은 다중 스레드에 의해 공유되는 자원이기 때문에 여기에 적재된 데이터들은 동기화 문제를 유발할 수 있다. 즉 Thread-safe 하지 않게된다는 이야기이다.

동기화 문제를 해결하려면, 임계구역을 만들어서 한 번에 한 스레드만 진입하도록 해야한다고 한다. 한 스레드가 자원을 사용하면, 다른 스레드들이 접근하지 못하도록 Lock을 걸고, 사용하던 스레드가 더 이상 사용하지 않으면 Lock을 해제하는 원리라고 한다.

출처


스택 영역(Stack Area)

스레드가 할당되는 영역이며, 스레드가 시작될때 스택 영역에 적재된다.

1
2
3
4
5
6
7
8
9
public class StackArea {
public static void main(String[] args){
hello();
}

public static void hello(){
System.out.println("Hello World");
}
}

위와 같은 코드의 애플리케이션을 실행한다고 하면, main() 이 먼저 실행되며, hello() 가 실행될 것이다. 그럼 스택 영역에 main() 스레드가 할당되고, main() 위로 hello() 가 할당되게 된다.

메서드 호출이 종료되면, 스택 영역에서 제거된다.


PC Register

Program Counter의 약자이다.

스레드당 하나의 PC 레지스터가 존재하며, 스레드가 생성될때마다 생성되는 영역이다.

스레드가 실행되면, 현재 스레드가 실행되는 부분의 주소와 명령을 저장하고, 이 명령이 끝나면 다음 실행될 명령의 주소를 가리킨다. 이 덕분에 여러 스레드들이 명령의 흐름을 잃지않고 순차적으로 실행 될 수 있다.

출처 : 김석진 - JVM(Java Virtual Machine)이란

Native Method Stack

자바 외에 다른 언어로 작성된 메서드 정보가 저장되는 영역이다.

출처 : 김석진 - JVM(Java Virtual Machine)이란


실행 엔진 (Execution Engine)

컴파일러가 변환한 바이트코드가 실제로 실행되는 영역이다. 실행 엔진은 다음으로 구성되어 있다.

  • 인터프리터
  • JIT 컴파일러
  • 가비지 콜렉터(GC)

인터프리터는 바이트코드를 한 줄씩 읽고 실행하는 역할을 수행한다.

자바는 바이트코드로 컴파일 후, 다시 기계어로 변환하는 작업을 거치기 때문에 네이티브 언어(C언어)보다 느리다고 평가받았다. 이를 극복하기 위해 등장한 것이 JIT인데, JIT는 아래에서 더 자세하게 설명하겠다.

가비지 콜렉터 는 메모리를 자동으로 관리해주는 영역이다. 힙 영역에 적재된 객체들 중 더 이상 참조되지 않는 객체들을 탐색 후 제거해주는 역할을 수행한다. 주로 힙 영역에 적재된 객체들이 GC의 타겟이 되지만, 메서드 영역과 스택 영역도 GC의 타겟이 된다.

사용되지 않으면서 메모리만 사용하고있던 불필요한 자원을 프로그래머 대신 가비지 콜렉터가 자동으로 해주는 것이다.

출처


자바 네이티브 인터페이스 (Java Native Interface, JNI)

네이티브 코드(C, C++)로 작성된 라이브러리를 제공하는 Native Method Libraries와 상호작용하기 위해 존재하는 영역이다.

이 영역은 아직 이해가 잘 안되는 부분이다. 더 공부가 필요할것 같다…


출처 : 당근케잌 - [Java] JVM Architecture란?

JIT 컴파일러란 무엇이며 어떻게 동작하는지

JIT 컴파일러란, Just In Time 컴파일러를 정의한다. 프로그램을 실행하는 시점에 전체 바이트코드가 아닌 필요한 함수만 기계어로 번역하여 실행 하는 컴파일러를 가리킨다.

JIT 컴파일러를 설명하기 앞서 프로그래밍 언어를 실행하는 방식에 대해 먼저 짚고넘어가자. 프로그래밍 언어를 실행하는 방식은 크게 두가지가 있다. 인터프리터와 컴파일러 방식.

인터프리터는 말 코드를 한 줄 씩 번역하여 실행을 하며, 컴파일은 실행하기 전에 프로그램 코드 전체를 기계어로 번역해서 번역된 기계어를 한 번에 실행한다.

이런 특징 때문에 인터프리터는 컴파일러 대비 생산속도는 빠르지만, 실행 속도는 느리다는 특징으로 정리될 수 있다.

반면 컴파일러는 코드 전체를 기계어로 번역후, 번역된 기계어를 실행한다. 이 때문에 컴파일 과정에서 에러를 발견할 수 있어서 실행할 때의 오류를 미리 알 수 있다는 특징도 있다.

컴파일러는 인터프리터 대비 생산 속도는 느리지만, 실행 속도는 빠르다고 할 수 있다.

JIT 컴파일러는 자바라는 언어가 인터프리터와 컴파일러의 특징 모두를 가질 수 있게한 컴파일러이다.
자바 컴파일러로 변환한 바이트코드를 한 번에 기계어로 번역하는게 아니라 실행 시점에 필요한 함수(메서드)만 기계어로 변환하기 때문에 기존보다 속도에서 더 향상 되었다고 할 수 있다.

출처 : 당근케잌 - [Java] JVM Architecture란?


JDK와 JRE차이

JRE (Java Runtime Environment)

자바 애플리케이션을 실행하기 위한 최소한의 배포 단위이다. JVM과 라이브러리가 포함된다. 개발이 아닌 실행만을 위한 도구이다.

JDK (Java Development Kit)

자바 애플리케이션 개발에 필요한 도구가 모두 포함된다. 원래 JDK를 설치하면 JRE까지 함께 설치되었으나 Oracle JDK11부터는 JRE가 기본으로 제공되지 않는다고 한다.

이유는 JRE의 구성요소가 JDK에 이미 포함되기 때문에 굳이 JRE를 따로 배포할 필요가 없어졌기 때문이라고 한다. 따라서 JRE를 따로 설치안하더라도 JDK만 설치하면 자바 애플리케이션을 실행할 수 있다.

출처 : Oracle - JDK 11 Release Notes



더 읽어볼 글