오늘은 JVM에 대해서 공부해봅시다.👻
Java Virtual Machine의 약자로, 자바로 작성된 프로그램을 실행시켜주기 위한 가상 컴퓨터를 말합니다.
일반적으로 프로그램은 OS에 종속적으로 개발이 되어있습니다.
그와 달리, 이 JVM이라고 하는 녀석은 OS와 프로그램 사이에 위치해 OS에 종속받지 않고 CPU가 Java를 인식, 실행할 수 있게 해줍니다.
그렇기 때문에, Java 언어는 JVM에 의해 "운영체제에 독립적"이라는 장점을 가질 수 있게 됩니다.
일반적인 컴파일 과정을 거친다고 생각했을 때 Java 소스 코드, 즉 원시코드(.java)는 CPU가 인식을 하지 못하기 때문에 기계어로 컴파일을 해주어야 합니다.
하지만, Java는 이 JVM이라는 가상 머신을 거쳐서 OS에 도달하기 때문에 OS가 인식할 수 있는 기계어로 바로 컴파일 되는 것이 아니라, JVM이 인식할 수 있는 bytecode(.class)로 먼저 변환됩니다.
💡 그럼 이 과정을 간단한 예시로 한 번 볼까요?
JAVA 컴파일러는 설치한 JDK의 하위 폴더
중 bin
폴더 안에 위치해 있는 javac.exe
라는 파일입니다.
우선 이 javac.exe
라는 컴파일러를 통해 Java 언어가 중간어 코드라고도 부르는 bytecode(.class)로 변환됩니다.
"Hello World"라는 문자열을 출력하는 Java코드인 testcode.java
라는 파일을 예시로 들어보겠습니다.
아래의 커맨드를 입력하면 javac.exe
가 Java 언어를 bytecode로 바꾸어 줄겁니다.
$ javac testcode.java
이제 JVM을 통해 변환된 bytecode를 OS에게 전달해 주어야겠죠?
다음과 같이 입력해줍니다.
$ java testcode
Hello world
이렇게 Java 언어로 작성한 소스 파일은 OS로 바로 전달되는 것이 아닌, JVM을 거쳐서 OS와 상호작용을 하게 됩니다.
이 때문에, 개발자의 입장에서는 Java 소스 코드를 작성함에 있어서 운영체제로부터 독립적이라는 장점을 가지게 되는 것입니다.😎
그렇다면 이제 이 JVM이라는 것이 어떻게 구성되어 있는 지 살펴봅시다.
JVM은 크게 3가지 구성 요소를 가지고 있습니다.
📍클래스 로더 (Class Loader)
📍실행 엔진 (Execution Engine)
📍런타임 데이터 영역 (Runtime Data Area)
그럼 클래스 로더부터 한 번 알아볼까요?
클래스 로더는, 런타임 시 동적으로 클래스 파일(.class)을 JVM으로 로드하고, jar 파일 내 저장된 클래스들을 JVM에 배치하는 작업을 수행하는 모듈입니다.
이름 그대로 클래스 파일을 적재하는 역할을 한다고 보시면 됩니다.
예를 들어, hello.java
파일을 컴파일하면 hello.class
파일 즉, 앞서 살펴본 것과 마찬가지로 bytecode가 생성되고, 이런 파일들을 모아 클래스 로더가 메모리에 적재시킵니다.
이 때, 클래스 파일이 적재되는 장소가 바로 마지막에 살펴볼 Runtime Data Area입니다.
실행 엔진은 클래스 로더에 의해 메모리에 적재된 클래스들을 컴퓨터가 이해할 수 있는 기계어로 변환시켜 명령어 단위로 실행하는 역할을 합니다.
💡이 때, 명령어를 한 줄씩 번역하여 실행하는 인터프리터 방식이 있고,
bytecode를 native code로 변환하는 JIT 컴파일 방식이 있습니다.
인터프리터 방식
실행 엔진이 bytecode를 명령어 단위로 읽어서 실행하는 방식이다.
코드 변경이 바로바로 반영되기 때문에 개발 과정에서의 테스트와 디버깅이 편리하다는 장점이 있지만, 매번 실행할 때마다 코드를 해석해야 하기 때문에 실행 속도가 비교적 느린 편입니다.
JIT(Just In Time) 컴파일 방식
프로그램을 실행하는 시점에서 필요한 코드를 기계어로 변환하는 방식이다.
이 방식은 인터프리터 방식과 컴파일 방식의 중간 쯤 되는데, 실행 시간에 소스 코드르 분석하고, 자주 사용되는 부분을 기계어로 컴파일하기 때문에 실행 속도를 향싱시킬 수 있습니다.
하지만, 초기 실행 시에 컴파일 과정이 필요하기 때문에 시작 속도가 느릴 수 있다는 단점이 있습니다.
두 방식에는 이러한 장단점이 있습니다.
결국 선택은 개발 환경과 실행 속도 등 여러 요소를 고려해야겠죠?
간단한 스크립트나 소규모 프로그램을 빠르게 개발하고 테스트하는 경우 인터프리터 방식이 유리할 수 있고, 대규모 애플리케이션에서는 실행 속도의 최적화가 중요하기 때문에 JIT 컴파일 방식이 더 적합하겠다는 생각이 드네요.🤔
클래스 파일이 적재되는 장소인 Runtime Data Area에는 5가지 영역이 존재합니다.
- 메서드 영역
- 힙 영역
- 스택 영역
- PC Register
- Native Method Stack
메서드 영역은 JVM이 시작될 때 생성되고, 모든 스레드가 공유하는 영역입니다.
이 영역에는 코드에서 사용되는 클래스들을 클래스 로더로 읽어 클래스 별로 정적 필드와 상수, 메서드 코드, 생성자 코드 등을 분류해서 저장합니다.
JVM이 동작하고 클래스가 로딩될 때 생성되어 JVM이 종료될 때까지 유지됩니다.
힙 영역은 인스턴스 객체와 배열이 생성되는 영역입니다.
여기에 생성된 객체와 배열은 스택 영역의 변수나 다른 객체의 필드에서 참조합니다.
Instance instance = new Instance();
💡예를 들어, 이렇게 객체를 생성한다고 가정해봅시다.
여기서 new Instance();
부분이 실제로 객체를 생성하는 부분이며, 이 객체는 힙 영역에 저장됩니다.
그리고 참조 주소인 instance
변수는 스택 영역에 저장되지만, 실제로는 힙 영역의 주소값을 가지고 있습니다.
즉, 힙의 참조 주소는 스택에 저장되고, 해당 객체를 통해서만 힙 영역에 있는 인스턴스를 핸들링 할 수 있다는 것입니다.
만약 참조하는 변수나 필드가 없다면, 의미 없는 객체이므로 JVM이 이를 쓰레기로 취급해 가비지 컬렉터를 실행시켜 자동으로 제거합니다.
따라서 개발자는 객체를 제거하기 위해 별도의 코드를 작성할 필요가 없게 됩니다. (원래 자바는 코드로 객체를 제거할 수도 없다.)
스택 영역은 아래와 같은 역할을 수행합니다.
함수 또는 메서드 내에서 선언된 지역 변수들은 함수가 호출될 때 생성되고, 함수가 종료되면 소멸합니다.
자바 프로그램에서 메소드가 호출될 때마다, 각 호출에 대한 스택 프레임이 스택 영역에 생성됩니다.
이 스택 프레임 내에는 지역 변수, 메소드에 대한 정보, 연산 중간 결과 등이 저장되며, 메소드가 종료되면 해당 스택 프레임은 스택에서 제거됩니다.
스택 영역은 메소드의 호출 정보를 순서대로 저장하기 때문에, 프로그램의 실행 경로를 추적하는 데 사용될 수 있습니다. 예를 들어, 예외가 발생했을 시 스택 트레이스를 통해 어디서 문제가 발생했는지 확인할 수 있죠👍
PC Register는 아래와 같은 역할을 수행합니다.
현재 실행 중인 명령어의 주소 저장 : PC Register는 현재 실행 중인 JVM 명령어의 주소를 저장합니다. 이를 통해 JVM은 어떤 명령어를 실행해야 할 지를 알 수 있습니다.
다음에 실행할 명령어의 위치 파악 : 명령어가 실행된 후, PC Register는 다음에 실행할 명령어의 주소로 업데이트됩니다. 덕분에 JVM은 순차적으로 프로그램 코드를 실행할 수 있습니다.
스레드 별 독립 실행 : JVM에서는 각 스레드가 독립적으로 실행됩니다. 이를 위해 각 스레드는 자신만의 PC Register를 가지고 있으며, 이를 통해 각 스레드가 현재 어떤 명령어를 실행하고 있는지 독립적으로 관리할 수 있습니다.
이와 같이 PC Register는 JVM이 프로그램의 실행 흐름을 정확하게 관리하고, 멀티 스레딩 환경에서 각 스레드가 독립적으로 실행될 수 있도록 하는 데 핵심적인 역할을 합니다.
이 영역은 자바 외 언어로 작성된 코드를 저장하는 메모리 영역입니다.
주로 native
라는 키워드가 붙은 언어들이 저장되죠.
오늘 살펴본 내용들을 바탕으로 JVM의 프로세스 과정을 정리해봅시다.
javac.exe
컴파일러가 클래스 파일(bytecode)로 변환이렇게 오늘은 Java Virtual Machine에 대해서 공부를 해보았습니다.
읽어주셔서 감사합니다.👻
📚 Reference
How Java Program Works?
자바가상머신 JVM 이란?
JVM이란?