자바 소스 파일(.java)을 JVM으로 실행하는 과정 이해하기
[ 핵심 내용 ]
JVM이란? JVM 구성 요소
바이트코드란, 바이너리 코드
컴파일 & 실행하는 방법
JIT 컴파일러란? JIT의 동작
JDK와 JRE의 차이
1. JVM이란?
Java Virtual Machine 즉, 자바(j)를 실행하기 위한 가상 기계(vm).
자바 가상 머신으로 자바 바이트코드(.class 파일)를 컴퓨터가 이해할수 있는 바이너리 코드로 변환하여 실행한다.
1-2. 바이트 코드, 바이너리 코드
- 바이트코드
바이트 코드 0과 1로 구성되어 있는 이진 코드이지만 바이너리 코드와 다르게 가상 머신이 이해할 수 있는 코드 사람에게 더 친숙한 고급언어보다는 덜 추상적이지만 기계어보다는 추상적이다. 컴퓨터가 이해 할 수 있는 기계어로 만들기 위해서는 인터프리터의 도움이 필요하다.
- 바이너리 코드
0과 1로 구성되어 있는 코드 C언어로 작성 된 .c 파일을 컴파일한 .obj 파일이 바이너리 코드
하지만 .obj 파일만으로는 실제 컴퓨터가 이해하여 실행할 수 없는데, 이때 필요한 것이 링커다. 링커는 여러 개의 코드와 데이터를 모아서 연결하여 메모리에서 실행 가능 한 파일로 만드는 역할을 한다. 위 과정이 모두 진행된 후 생성되는 실행 파일이 컴퓨터가 이해 할 수 있는 기계어로 구성되어 있다.
1-3. 왜 변환해야 하지?
일반 애플리케이션의 코드는 OS만 거치고 하드웨어로 전달되는데, Java 애플리케이션은 JVM을 한 번 더 거칠뿐 아니라 하드웨어에 맞게 완전히 컴파일 된 상태가 아니라 실행 시에 해석(interpret)되기 때문에 속도가 느리다는 단점이 있다.
하지만 요즘에는 바이트코드(컴파일된 자바코드)를 하드웨어의 기계어로 바로 변환해주는 JIT 컴파일러와 향상된 최적화 기술이 적용 되어 속도의 격차를 많이 줄였다. 또한, JVM이 없을 경우 애플리케이션은 OS와 바로 맞붙어 있기 때문에 OS에 종속적이게 된다. 그래서 다른 OS에 실행시키기 위해서는 애플리케이션을 그 OS에 맞게 변경해야 한다. 하지만 이처럼 JVM을 사용하는 Java 애플리케이션은 JVM하고만 상호작용을 하기 때문에 OS가 달라지더라도 프로그램의 변경 없이 실행이 가능하다.
1-4. JVM의 특징
(1) 스택 기반의 가상 머신:
인텔x86 아키텍처나 ARM 아키텍처와 같은 하드웨어가 레지스터 기반으로 동작하는데 비해 JVM은 스택 기반으로 동작한다.
(2) 심볼릭 레퍼런스
참고하는 클래스의 특정 메모리 주소를 참조 관계로 구성하지 않고 이름만 가지고 있는 것.
기본 자료형(primitive data type)을 제외한 모든 타입(클래스와 인터페이스)을 명시적인 메모리 주소 기반의 레퍼런스가 아니라 심볼릭 레퍼런스를 통해 참조한다.
(3) 가비지 컬렉션 (garbage collection)
클래스 인스턴스는 사용자 코드에 의해 명시적으로 생성되고 가비지 컬렉션에 의해 자동으로 파괴된다.
(4) 기본 자료형을 명확하게 정의하여 플랫폼 독립성 보장
C/C++등의 전통적인 언어는 플랫폼에 따라 int형의 크기가 변하지만 JVM은 기본 자료형을 명확하게 정의하여 호환성을 유지하고 플랫폼 독립성을 보장한다.
(5) 네트워크 바이트 오더 (network byte order)
자바 클래스 파일은 네트워크 바이트 오더를 사용한다. 인텔 x86 아키텍처가 사용하는 리틀 엔디안이나, RISC계열 아키텍처가 주로 사용하는 빅 엔디안 사이에서 플랫폼 독립성을 유지하려면 고정된 바이트 오더를 유지해야 하기에 네트워크 전송 시에 사용하는 바이트 오더인 네트워크 바이트 오더를 사용한다. 네트워크 바이트 오더는 빅 엔디안이다.
2. JVM 구성 요소
JVM은 크게 클래스 로더 시스템, 메모리, 실행 엔진, 그리고 네이티브 메서드로 나뉘어져 있다.
2-1. 클래스 로더 시스템
자바는 런타임에 클래스를 처음으로 참조할 때 해당 클래스를 로드하고 링크하는 특징이 있다. 이 동적 로드를 담당하는 부분이 JVM의 클래스 로더이다.클래스 로더 시스템은 컴파일된 바이트코드들을 읽어 연결한 뒤 메모리에 저장하는 역할을 수행한다.
내부적으로는 로딩, 링크, 초기화의 단계가 존재 클래스 로더 시스템 초기화 단계에서 전역 변수를 메모리에 할당하기 때문에 필요 이상으로 전역 변수를 남용할 경우 메모리 이슈를 겪을 수 있다.
2-2. 메모리
JVM의 메모리 영역으로 자바 애플리케이션을 실행할 때 사용되는 데이터들을 적재하는 영역.
- 스택 영역 : 지역 변수, 파라미터, 리턴 값, 연산에 사용되는 임시 값등이 생성되는 영역으로 클래스 수준의 정보를 저장하고 공유자원이다.
FILO의 특징을 갖고 있고 특정 영역에서 사용되는 메모리 프레임을 만들 때 사용된다. 특정 스레드가 생성될 때마다 특정 함수가 호출 될 때 마다 각각의 스택 프레임을 만들고, 함수 파라미터와 같은 데이터를 저장할때는 데이터를 역순으로 저장한다.
- PC Register : 쓰레드가 생성될 때마다 생성되는 영역으로 현재 쓰레드가 실행되는 부분의 주소와 명령을 저장하고 있는 영역이다. 이것을 이용해 쓰레드를 돌아가며 수행할 수 있게 한다. 현재 실행 중인 명령어의 주소와 Return Address를 저장하는 register로써 하나의 스레드가 생성될 때마다 독립적으로 존재한다.
- 네이티브 메소드 스택: 자바 외 언어로 작성된 네이티브 코드를 위한 메모리영역으로 보통 C/C++등의 코드가 저장되는 메모리 공간.
- 힙(heap): new 키워드로 생성된 객체와 배열이 생성되는 영역. 대표적으로 객체(메모리 할당된 인스턴스)가 저장된다.
메소드 영역에 로드된 클래스만 생성이 가능하고 Garbage Collector가 참조되지 않는 메모리를 확인/제거하는 영역이기 때문에 Application Context 전반적으로 공유되는 자원이다. 클래스 수준의 정보는 스택에 저장되지만 객체의 인스턴스는 힙 영역에 생성된다. 스택 영역에 이 힙 영역의 주소값을 저장한다.
- 메소드 : 모든 스레드가 공유하는 영역으로 JVM이 시작될 때 생성된다. 클래스의 정보나 static 변수가 저장되어 있으며 Application Context 전반적으로 공유되는 영역. 저장하는 내용은 JVM이 읽어들인 각각의 클래스, 인터페이스에 대한 런타임 상수 풀, 필드와 메서드 정보, static 변수, 메서드의 바이트코드 등을 저장한다.
2-3. 실행엔진
컴파일러된 .class의 바이트 코드를 실행하는 역할. 클래스 로더 시스템 에 의하여 실행에 필요한 준비 과정이 완료되었다면, 이제 인터프리터를 사용하여 바이트 코드를 번역하여 실행할 차례다. (컴파일에 대한 자세한 이야기는 다음 챕터에서 다루어지지만, 자바에서는 두가지 컴파일러가 있다. java compiler(javac)와 JIT 컴파일러이다.) 실행 엔진 내부적으로는 인터프리터, JIT 컴파일러, GC 가 있다.
(1) 인터프리터
컴파일된 .class의 바이트 코드를 실행하는 역할
(2) JIT 컴파일러
JIT 컴파일러란? 어떻게 동작하는지?
프로그램이 실행될 때 인터프리터가 바이트 코드를 읽어 기계어로 번역하지만, 프로그램에는 반복적으로 사용하는 코드가 존재한다. 반복적으로 사용되는 코드를 매 번 번역하는 것보다는 최초 1회만 번역하여 특정 저장소(캐시)에 저장 한 뒤 추가 참조가 필요할 때마다 불러온다면 성능 향상을 이끌어 낼 수 있을 것이다. 이 역할을 수행하는 것이 JIT 컴파일러이다.
일반적으로 인터프리터 언어(python)보다는 정적 컴파일 언어(C)가 실행 속도가 더 빠르다고들 하는데, 그 이유는 JIT 컴파일러의 목적을 보면 알 수 있다. 정적 컴파일 언어의 경우 컴파일에 시간이 오래 걸리는 반면 컴파일 후 결과물(.exe)은 CPU가 이해할 수 있는 기계어이기 때문에 실시간으로 번역하여 실행하는 인터프리터 언어보다 빠를 수 밖에 없다. Java는 컴파일 언어이지만 동적 컴파일 언어이다. CPU가 이해할 수 있는 기계언어가 아닌 JVM이 이해 할 수 있는 바이트 언어로 컴파일한 뒤 인터프리터에 의해 실시간으로 번역되어 실행된다. JIT 컴파일러는 반복되어 사용되는 코드나 기계어로 변환 시 많은 리소스가 필요한 부분을 코드가 실행되는 과정(Just-In-Time)에 실시간으로 변환(기계어)하여 캐싱한다. Java는 이러한 최적화 과정이 존재하기에 인터프리터 언어(Python) 보다 좋은 실행 성능을 낼 수 있다.
JIT는 이런 인터프리터의 단점을 보완하기 위해 도입되었다. 인터프리터 방식으로 실행하다가 적절한 시점에 바이트코드 전체를 네이티브 코드로 바꾼다. 그 다음부터 인터프리터는 네이티브 코드로 컴파일된 코드를 바로 사용한다. 네이티브 코드를 실행하는 것이 하나씩 인터프리팅 하는 것보다 빠르고, 네이티브 코드는 캐시에 보관하기 때문에 한 번 컴파일된 코드는 계속 빠르게 수행된다. JIT 컴파일러가 컴파일하는 과정은 바이트코드를 하나씩 인터프리팅하는 것보다 훨씬 오래걸리기에 만약 한 번만 실행되는 코드라면 컴파일 하지 않고 인터프리팅하는게 유리하다. 따라서 컴파일러를 사용하는 JVM들은 내부에서 해당 메소드의 수행빈도를 확인 후 일정 정도를 넘을 때 컴파일을 수행한다.
JIT 컴파일러는 바이트코드를 우선 중간 단계의 표현인 IR(Intermediate Representation)로 변환하여 최적화를 수행하고 그 다음 네이티브 코드를 생성한다.
이렇게 실행 엔진(Execution Engine)이 클래스 로더에 의해 메모리에 적재된 클래스(Bytecodes)들을 기계어로 변경하여,
명령어 단위로 실행하는 역할을 했다. 이 명령어를 명령어를 하나 하나 실행하는 방식은 다음과 같이 두 가지가 있다.
1. 인터프리터(Interpreter)
2. JIT(Just-In-Time) 컴파일러
3. 컴파일 하는 방법
플랫폼으로부터 독립적인 언어인 Java는 바로 컴파일 할 수는 없다. 2단계를 거쳐 실행할 수 있는데
1. 자바 소스 파일 작성 .java
2. javac.exe 사용하여 .java 파일 컴파일 (모든 운영체제에 맞게 만들어진 JVM으로 컴파일)
$ javac 소스파일명.java
컴파일이 정상적으로 완료되면 해당 경로에 위와 같이 소스파일명.class 생성된다.
전 챕터에서 말했다시피 자바에서는 두가지 컴파일러가 있다. java compiler(javac), 그리고 JIT 컴파일러이다.
(1) java compiler
흔히 javac라는 명령어로 사용하는 자바 컴파일러가 우리가 자바 소스코드를 빌드하면 생기는 .class 파일을 만들어준다. 하지만 사람이 읽을 수 있는 코드를 기계가 읽을 수 있는 코드로 바꿔주는 일반적인 컴파일러와는 달리, .class 파일(바이트코드)은 아직 기계가 읽을 수 없다. 실제로 jvm을 통해서 바이트코드가 메모리상에 올라갈때 기계어로 바뀌게 된다.
(2) jit compiler
jit(just in time) 컴파일러는 jvm 의 구성 요소로, 바이트코드를 바이너리로 컴파일하는 역할을 한다.
3-2. 컴파일 과정
소스파일에 포함 된 각 클래스의 내용은 별도의 .class 파일에 저장된다. 소스코드를 바이트 코드로 변환하는 동안 컴파일러는 다음 단계를 따른다.
[ 자바 소스 프로그램이 컴파일되어 실행되기까지의 순서 ]
(1) 개발자가 자바 언어를 사용하여 프로그램 작성 -> .java
*.java 파일이 컴파일러를 통해 전달 된 다음 소스코드를 Bytecode로 인코딩..
소스파일에 포함 된 각 클래스의 내용은 별도의 .class 파일에 저장.
소스코드를 바이트 코드로 변환하는 동안 컴파일러는 다음 단계를 따른다.
- Parse: *.java 소스파일을 읽은 뒤 결과 토큰을 AST(Abastract Syntax Tree) 노드에 매핑.
- Enter: 정의된 심볼들을 심볼테이블(Symbol table) 에 저장.
- Process annotations: 요청된 경우 지정된 컴파일 위치에서 찾은 애너테이션을 처리.
- Attribute: 구문 트리에 속성을 부여하며, 이름 확인, 유형 검사 및 상수 정의가 포함.
- Flow: 이전 단계의 트리에 대한 데이터 흐름을 분석합니다. 여기에 할당 및 도달 가능성에 대한 검사도 포함.
- Desugar: AST를 다시 작성하고 몇몇 syntactic sugar 들을 번역.
- Generate: *.class 파일을 생성.
(2) 작성한 프로그램을 컴파일 .class -> .class
(3) 프로그램 실행에 필요한 파일들을 모두 읽어 연결
(4) 프로그램 실행 전 메모리 초기화 작업 진행
(5) 바이트 코드로 작성된 프로그램을 인터프리터가 기계어로 번역하여 실행
3-3. 실행하는 방법
java.exe 파일을 사용하여 바이트코드로 컴파일 된 .class 실행 (소스 파일명의 확장자 붙이지 않음)
$ java 파일명
4. JDK와 JRE의 차이
( 다음 글을 참고해 주세요.)
JRE(Java Runtime Enviroment):
JVM + 라이브러리
자바 애플리케이션을 실행할 수 있도록 구성된 배포판. JVM과 핵심 라이브러리 및 자바 런타임 환경에서 사용하는 프로퍼티 세팅이나 리소스 파일을 가지고 있다. 개발 관련 도구는 포함하지 않는다.(JDK에서 제공) (JDK 11 이후로 Jlink가 추가되어 JRE가 따로 배포되지 않는다.)
JDK(Java Development Kit):
JRE + 개발 툴
소스 코드를 작성할 때 사용하는 자바 언어는 플랫폼에 독립적. 오라클은 자바 11부터는 JDK만 제공하며 JRE를 따로 제공하지 않는다.
한 줄 정리
1. 자바의 클래스로더는 자바 클래스를 '런타임'(프로그램 실행 시점)에 메모리에 로드
2. 바이트코드베리파이어가 해당 바이트코드가 실행가능한지 확인
3. 유효하다면 JIT 컴파일러를 통해 최종적으로 기계어로 변환 후, 컴퓨터가 실행.
JIT 컴파일러는 로드되는 자바 바이트 코드를 '바로바로' 컴파일해서 실행가능한 기계어로 변환. 클래스가 로딩될 때마다 JIT 컴파일러로 기계어가 생성되고 그 기계어를 실행하는 것. 이 특징 때문에 사전에 클래스 파일만 뽑아내면 된다. (운영체제 상관 x) 이 때문에 시간이 조금 더 소모되긴 한다.
참고: https://sowhat4.tistory.com/61
https://catsbi.oopy.io/df0df290-9188-45c1-b056-b8fe032d88ca#0af471f2-69b3-45a0-8d32-5dff9a21310c
'☕️자바 𝗝𝗔𝗩𝗔' 카테고리의 다른 글
[java] 자바 별찍기 모래시계 찍기 (0) | 2022.11.08 |
---|---|
String 과 String Pool (0) | 2022.11.04 |
[Java] 가변인자(varargs, 쩜쩜쩜) 사용법 (매개변수 동적 사용) (0) | 2022.10.25 |
[JAVA] Optional 클래스 (0) | 2022.10.06 |
도메인 모델 패턴, 트랜잭션 스크립트 패턴 (Domain Model Pattern, Transaction Script Pattern) (1) | 2022.10.03 |
댓글