목표
자바 소스 파일(.java)을 JVM으로 실행하는 과정 이해하기.
학습할 것
- JVM이란 무엇인가
- 컴파일 하는 방법
- 실행하는 방법
- 바이트코드란 무엇인가
- JIT 컴파일러란 무엇이며 어떻게 동작하는지
- JVM 구성 요소
- JDK와 JRE의 차이
JVM이란 무엇인가
Java Virtual Machine
자바 코드는 완전한 기계어가 아닌 바이트코드이기 때문에 이를 해석하고 실행해 줄 가상의 운영체제가 필요하다.
이 역할을 수행하는 것이 JVM
TL:DR - 어느 환경에서든 ( 윈도우, 리눅스, Mac...) 자바 프로그램을 실행할 수 있도록 도와주는 프로그램
** JVM은 자바 바이트코드로 컴파일 된 다른 언어들도 실행 할 수 있다.
바이트코드 - 특정 하드웨어가 아닌 가상 컴퓨터에서 돌아가는 실행 프로그램을 위한 이진 표현법. 하드웨어가 아닌 소프트웨어에 의해 처리되기 때문에, 보통 기계어보다 더 추상적이다.
자바 바이트코드 - JVM에서 사용하는 명령어들. 각 명령어는 1바이트로 이루어져 있다.
** 자바 바이트코드를 공부하는 것은 C, C++개발자가 어셈블리 언어를 공부하는 것과 마찬가지로 자바 개발자들에게 도움이 된다고 한다. 하지만 이것에 왜 개발자들에게 도움이 주는가는 아직 더 고민하고 알아봐야 할 것 같다.
컴파일 하는 방법
컴파일 - 우리가 작성한 자바 프로그램을 바이트코드로 변환시켜주는 역할을 한다.
** 컴파일을 통하여 생성된 바이트코드를 jvm이 받아 이를 다시 기계어로 변환하게 된다.
컴파일은 javac 라는 명령어로 실행할 수 있으며 여러가지 옵션이 있어서 상황에 맞게 사용할 수 있다.
실행하는 방법
자바의 실행은 java 라는 명령어를 통해 실행 할 수 있다.
대략적인 실행 과정
확장자가 .java인 소스 파일을 컴파일러(javac.exe)로 컴파일하여 확장자가 .class인 바이트코드 파일을 생성한다.
생성된 바이트코드 파일은 jvm 구동 명령어(java.exe)를 통해 JVM에서 해석되고 운영체제에 맞게 기계어로 번역이 된다.
Class Loader가 컴파일 된 자바 바이트코드를 런타임 데이터 영역에 로드하고, 실행 엔진이 자바 바이트코드를 실행한다.
JIT 컴파일러
자바의 가장 큰 장점은 한 번 작성하면 어디에서나 실행이 된다(Write Once, Run Everywhere)는 점이지만
JVM을 한 번 거쳐 기계어로 번역되고 실행되기 때문에 C, C++과 같은 언어들보다 속도가 느리다는 단점이 있다.
다만 JVM 내부의 최적화된 JIT컴파일러를 통해 속도의 격차는 많이 줄어들고 있다.
JIT 컴파일러 - 실행 시점에서 인터프리트 방식으로 기계어 코드를 생성하면서 그 코드를 캐싱하여, 같은 함수가 여러 번 불릴 때 매번 기계어 코드를 생성하는 것을 방지한다.
JVM 구성요소
클래스 로더
- 자바는 런타임에 클래스를 처음 참조 시 해당 클래스를 로드하고 링크하는데(동적 로드) 이 동적로드를 담당하는 부분이 JVM의 Class Loader
- Bootstrap Class Loader: Object 클래스 혹은 기본 Java API등을 로드
- Extension Class Loader: 기본 java API등을 제외한 확장 클래스들을 로드
- System Class Loader: 사용자가 지정한 $CLASSPATH 내의 클래스들을 로드
- User Defined Class Loader: 사용자가 직접 코드상에서 생성해 사용하는 Class Loader
Class Loader가 아직 로드되지 않은 클래스를 찾으면 위와 같은 과정을 거쳐 클래스를 로드하고 링크한뒤 초기화 한다. - Loading – 클래스를 파일에서 가져와 JVM의 메모리에 업로드 한다.
- Verifying – 읽어 들인 클래스가 자바 언어 명세(Java Language Specification) 및 JVM명세에 명시된 대로 잘 구성되어 있는지 검사한다.
- Preparing – 클래스가 필요로 하는 메모리를 할당, 클래스에서 정의된 필드, 메서드, 인터페이스들을 나타내는 데이터 구조를 준비한다.
- Resolving – 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경한다.
- 심볼릭 레퍼런스 - 객체를 가져오기 위해 사용되는 문자열
- Initializing – 클래스 변수들을 적절한 값으로 초기화. Static Initializer들을 수행하고 static 필드들을 설정된 값으로 초기화 한다.
Runtime Data Area
-
Method Area
static 변수를 포함해서, 모든 클래스 레벨의 데이터가 저장된다. 중요한 점은 하나의 JVM 당 하나의 Method Area가 존재하며 자원을 공유한다. -
Heap Area
모든 객체 및 인스턴스와 배열이 여기에 저장된다. Method Area와 마찬가지로 JVM 당 하나의 영역이 존재하며, 자원을 공유하기 때문에 thread-safe 하지 않다. -
Stack Area
스택 영역은 런타임 시 모든 쓰레드에 생성이된다. 모든 지역 변수는 스택 영역에 저장되며, 쓰레드 당 영역이 존재하기때문에 thread-safe 하다. Stack Area는 다음 세가지 항목으로 나눌 수 있다.
- Local Variable Array
- Operand stack
- Frame data -
PC Registers
각 쓰레드는 PC 레지스터를 가지며, 명령어가 실행되면 현재 명령어의 주소를 가지기 위해서 PC 레지스터가 다음 명령어로 업데이트 된다. -
Native Method stacks
네이티브 메서드 스택은 네이티브 메서드 정보를 가진다. 모든 쓰레드에 대해 별도의 네이티브 메서드 스택이 된다.
Execution Engine
Runtime Data Area에 할당 된 바이트 코드는 Execution Engine에 의해 실행된다. 실행 엔진은 바이트 코드를 읽고 한줄씩 실행된다.
-
Interpreter
인터프리터는 바이트 코드를 더 빨리 해석하지만 느리게 실행된다. 인터프리터의 단점은 하나의 메서드가 여러 번 호출 될 때마다 새로운 해석이 필요하다는 점이다. -
JIT Compiler
JIT 컴파일러는 인터프리터의 단점을 보완한다. 실행 엔진은 바이트 코드를 변환하는 데 인터프리터의 도움을 사용하지만 반복 된 코드를 찾으면 전체 바이트 코드를 컴파일하고 네이티브 코드로 변경하는 JIT 컴파일러를 사용한다. 이 네이티브 코드는 반복되는 메서드 호출에 직접 사용되어 시스템 성능을 향상시킨다. -
Garbage Collector
Garbage Collector(GC)는 참조되지 않은 객체를 모아서 제거한다. GC는 “System.gc()”를 호출하여 수동으로 할 수 있지만 실행이 보장되지는 않는다. JVM의 GC는 객체가 생성되는 것을 확인한다.
JDK와 JRE의 차이
JDK: Java Development Kit
- 자바 프로그램 개발시 필요한 도구(javac, java등)들을 포함
- JDK를 설치하면 JRE도 같이 설치가 된다.
JRE: Java Runtime Environment
- 컴파일된 자바 프로그램을 실행시킬 수 있는 자바 환경
- JVM이 자바 프로그램을 동작시킬 때 필요한 라이브러리 파일들과 기타 파일들을 가지고 있다.
- 자바 프로그램을 실행시키기 위해서는 필수로 설치되어야 한다.