학습 목차
1. 자바 가상 머신(JVM)이란 무엇인가
2. 컴파일 하는 방법
3. 실행하는 방법
4. 바이트코드란 무엇인가
5. JIT 컴파일러란 무엇이며 어떻게 동작하는지
6. JVM 구성 요소
7. JDK와 JRE의 차이
1. 자바 가상 머신(JVM)이란 무엇인가
Java Virtual Machine(JVM)은 Java를 실행하기 위한 자바 가상머신입니다. 자바와 운영체제 사이에서 중계자 역할을 하며, 자바가 운영체제 종류에 종속받지 않고 실행 가능하도록 합니다.
운영체제 위에서 동작하는 프로세스로, 자바 코드(.java)를 컴파일 해서 얻은 바이트 코드(.class)를 해당 운영체제가 이해할 수 있는 기계어로 바꿔 실행 시켜줍니다.
Garbage Collection(GC)을 이용하여 자동으로 메모리 관리를 해줍니다.
Java 언어의 특징
- Java의 특징 중 하나는 운영체제에 독립적이라는 것입니다.
어떻게 운영체제에 독립적일 수 있을까?
- Java 애플리케이션은 운영체제나 하드웨어가 아닌 JVM하고만 통신하고, JVM이 Java 애플리케이션으로부터 전달받은 명령을 해당 운영체제가 이해할 수 있도록 변환하여 전달합니다. 따라서 다른 OS에서도 프로그램의 변경없이 실행이 가능합니다.
- 단, JVM은 OS에 종속적이기 때문에, 해당 OS에서 실행가능한 JVM이 필요합니다. (일반적으로 많이 사용되는 주요 OS용 JVM을 제공하고 있습니다.)
Java 애플리케이션 vs 일반 애플리케이션
- 일반 애플리케이션의 코드는 OS만 거치고 하드웨어로 전달되는 데 비해, Java 애플리케이션은 JVM을 한 번 더 거칩니다. 또한, 하드웨어에 맞게 완전히 컴파일 된 상태가 아니라, 실행 시에 해석(interpret)됩니다.
- Java 애플리케이션은 JVM을 한 번 더 거치고, 실행 시에 해석되는 특징때문에, 속도가 느리다는 단점을 가지고 있습니다. (요즘은 바이트코드를 하드웨어의 기계어로 바로 변환해주는 JIT컴파일러와 향상된 최적화 기술이 적용되어 속도의 격차를 많이 줄였습니다.)
Java 소스 파일(.java)를 JVM으로 실행하는 과정
- Java 애플리케이션이 실행되면 JVM은 운영체제로부터 이 프로그램이 필요로 하는 메모리를 할당 받습니다.
- 자바 컴파일러(javac)가 자바 소스코드(.java)를 읽어들여, 자바 바이트코드(.class)로 변환합니다.
- Class Loader를 통해 class 파일을 JVM 메모리에 적재합니다.
- JVM 메모리 영역에 적재된 class 파일을 Execution engine을 통해 해석합니다.
2. 컴파일 하는 방법
javac.exe 프로그램을 통해 컴파일합니다. ($ javac 소스파일명.java)
Java 소스코드(*.java)가 컴파일되면 Java 바이트코드(*.class)라는 중간 형태로 변환됩니다.
Java 소스코드를 컴파일할 때 사용하는 프로그램
- javac.exe
- 컴파일 결과로 자바 소스코드(.java) → 자바 바이트코드(.class)로 변환됩니다.
$ javac 소스파일명.java
3. 실행하는 방법
java.exe 프로그램을 통해 실행합니다. ($ java 소스파일명)
JVM은 컴파일 된 Java 바이트코드를 읽고, OS가 인식할 수 있는 기계어로 변환하여 프로그램을 실행합니다.
컴파일 된 Java 바이트 코드를 실행할 때 사용하는 프로그램
- java.exe
- 컴파일 된 바이트코드(.class)를 실행합니다. (단, 실행시에는 소스 파일명 확장자는 붙이지 않습니다.)
$ java 클래스파일명
4. 바이트코드란 무엇인가
컴퓨터가 이해할 수 언어가 바이너리코드라면, 바이트코드는 가상 머신(JVM)이 이해할 수 있는 언어입니다.
자바 소스코드(.java)를 가상 머신(JVM)이 이해할 수 있는 중간 코드(.class)로 컴파일 한 것을 말합니다.
기계어와 바이너리 코드
- 기계어는 컴퓨터(CPU)가 이해하고 실행할 수 있는 명령어로, 이진수(0과 1)로 이루어져 있습니다. 즉, 바이너리 코드의 일부분으로 볼 수 있습니다.
- 바이너리 코드는 이진수(0과 1) 형태로, 기계어 명령어뿐만 아니라 모든 데이터(ex. 텍스트, 이미지, 오디오 등)를 포함합니다. 즉, 기계어를 포함한 더 넓은 범위입니다.
즉, 기계어 ⊂ 바이너리 코드라 할 수 있습니다.
- C언어는 컴파일러에 의해 소스파일(.c)이 목적파일(.obj)로 변환합니다. 목적파일(.obj)은 기본적으로 컴퓨터가 이해할 수 있는 바이너리코드의 형태이지만, 완전한 기계어가 아니기 때문에 실행될 수는 없습니다. (링커에 의해 실행 가능한 실행파일(.ex)로 변환될 때 완전한 기계어가 됩니다.)
바이트 코드(.class)
- 이진수(0과 1)로 구성되어 있는 코드입니다.
- 가상 머신(JVM)이 이해할 수 있는 코드로, 컴퓨터가 이해할 수는 없습니다.
5. JIT 컴파일러란 무엇이며 어떻게 동작하는지
JVM이 프로그램을 처음 실행할 때는 인터프리터 방식을 사용하여, 바이트코드를 한 줄씩 기계어로 변환하여 실행하여 빠른 시작 시간이 가능합니다.
만약, 특정 기준(자주 실행되는 코드 등)을 충족하면, JIT 컴파일러가 해당 바이트코드를 기계어로 컴파일하여 캐시에 저장합니다.
이후 동일한 코드가 다시 실행되면, 인터프리팅하지 않고 캐시에 저장된 기계어 코드를 사용하여 실행 속도를 높일 수 있습니다. 또한, 코드의 일부가 변경되면, 변경된 부분만 재컴파일하여 전체 성능을 최적화할 수 있습니다.
JIT 컴파일러의 등장 배경
- 기존의 Interpreter 방식은 바이트코드를 명령어 단위로 읽어 기계어로 변환하여 실행하는 과정에서, 반복적인 번역 작업으로 인해 성능이 저하되었습니다.
- 이를 해결하기 위해 JIT(Just-In-Time) 컴파일러가 등장했습니다. JIT 컴파일러는 실행 시점에 코드를 컴파일하여 기계어로 변환함으로써 실행 속도를 개선하였습니다.
JIT(Just-In-Time)의 동작 방식
- JVM 이 프로그램을 처음 실행할 때는 인터프리터 방식을 사용하여 바이트코드를 한 줄씩 기계어로 변환하여 실행합니다. 이는 빠른 시작 시간을 가능하게 합니다.
- 만약, 특정 기준(자주 실행되는 코드 등)을 충족하면, JIT 컴파일러가 해당 바이트코드를 기계어로 컴파일하여 캐시에 저장합니다.
- 이후 동일한 코드가 다시 실행되면, 인터프리팅하지 않고 캐시에 저장된 기계어 코드를 사용하여 실행 속도를 높일 수 있습니다. 또한, 코드의 일부가 변경되면, 변경된 부분만 재컴파일하여 전체 성능을 최적화할 수 있습니다.
6. JVM 구성요소
JVM의 구성은 크게 클래스 로더(Class Loader), 런타임 데이터 영역(Runtime Data Area), 실행 엔진(Execution Engine), 네이티브 메서드 인터페이스(JNI; Java Native Interface), 네이티브 메서드 라이브러리(Native Method Library) 5가지로 나뉩니다.
클래스 로더는 클래스 파일들을 읽어들인 후, 메모리에 로드하는 역할을 수행합니다.
런타임 데이터 영역은 자바 애플리케이션을 실행할 때 사용되는 데이터들이 저장되는 메모리 공간으로, 크게 5가지 영역으로 나뉩니다. Thread가 공유하는 영역에는 힙 영역, 메서드 영역이 존재하며, JVM 시작 시 생성됩니다. Thread가 공유하지 않는 영역에는 스택 영역, PC 레지스터 영역, 네이티브 메서드 스택이 존재하며, Thread마다 생성됩니다.
실행 엔진은 클래스 로더를 통해 런타임 메모리(JVM 메모리)에 배치된 바이트코드(.class)들을 읽어 기계어로 번역하여 실행해 줍니다. 이때 실행 엔진은 인터프리터와 JIT 컴파일러 두 방식을 혼합하여 실행합니다.
네이티브 메서드 인터페이스는 자바 애플리케이션이 네이티브(자바 이외의 언어) 메서드를 호출할 수 있게 해주며, 필요 시 네이티브 라이브러리를 로드하여 사용할 수 있습니다. 이를 통해 자바 코드와 네이티브 코드 간 상호 작용을 할 수 있습니다.
더보기
- Java 애플리케이션이 실행되면 JVM은 운영체제로부터 이 프로그램이 필요로 하는 메모리를 할당 받습니다.
- 자바 컴파일러(javac)가 자바 소스코드(.java)를 읽어들여, 자바 바이트코드(.class)로 변환합니다.
- Class Loader를 통해 class 파일을 JVM 메모리에 적재합니다.
- JVM 메모리 영역에 적재된 class 파일을 Execution engine을 통해 해석합니다.
클래스 로더 (Class Loader)
- 컴파일된 바이트코드(.class)들을 읽어, OS로 부터 할당 받은 메모리에 적재하는 역할을 수행합니다.
- 내부적으로는 로딩, 링크 및 초기화의 단계가 존재합니다.
- 로딩(Loading) : 클래스 파일을 메모리에 읽어들이는 작업이 수행됨. 이 단계에서 클래스 로더는 클래스 파일을 찾고, 읽고, JVM의 메모리 구조에 해당 클래스를 나타내는 객체를 생성함
- 링크(Linking) : 로드된 클래스를 JVM이 실행할 수 있는 상태로 만듦. 이때, 바이트코드가 유효한지 검증, 클래스의 정적 변수와 기본 값 준비, 심볼릭 레퍼런스(클래스의 이름으로 된)를 메모리 레퍼런스(실제 클래스의 메모리 상 주소)로 변환하는 작업이 일어남
- 초기화(Initialization) : 클래스 변수(정적 변수)에 정의한 초기 값을 설정하고(ex. static int x = 5), 클래스의 static 초기화 블록(static { // 초기화 코드 })을 실행함.이때 필요 이상으로 전역 변수를 남용할 경우, 메모리 부족이 발생할 수 있음
런타임 데이터 영역 (Runtime Data Area)
- JVM이 자바 애플리케이션을 실행할 때 사용되는 데이터들이 저장되는 메모리 공간으로, 크게 5가지 영역으로 나뉩니다.
- Thread가 공유하는 영역 (JVM 시작 시 생성) : 힙 영역, 메서드 영역
- Thread가 공유하지 않는 영역 (Thread마다 생성) : 스택 영역, PC 레지스터 영역, 네티이브 메서드 스택
- PC 레지스터 (Program Counter Register)
- 현재 실행 중인 JVM 명령어의 주소를 저장합니다.
- 네이티브한 메서드의 경우에는, 해당 명령어의 위치를 알 수 없기 때문에 undefined 값을 기록합니다.
- 네이티브 메서드 스택 (Native Method Stack)
- Java가 아닌 다른 언어로 작성된 코드를 호출할 때 생성되었다가, 호출이 종료되면 사라집니다.
- 바이트 코드(.class)가 아닌, 실행 가능한 기계어로 작성된 프로그램을 실행시시키는 영역으로, JIT 컴파일러에 의해 변환된 Native Code 역시 여기서 실행됩니다.
- 스택 영역 (Stack)
- 메서드 호출 시 지역변수, 매개변수, 리턴 타입/값, 피연산자, 메서드 리턴 주소 등이 저장되는 영역으로, 메서드 호출 시 생성되었다가, 호출이 종료되면 사라집니다.
- 어셈블리어의 경우에는 피연산자를 레지스터에 저장하지만, JVM은 레지스터를 이용하지 않고 스택에 피연산자들을 저장합니다.
- Frame이라는 자료구조로 스택에 데이터가 저장됩니다.
- 메서드 영역 (Method Area)
- 일반적인 메모리 구조에서의 코드 영역과 유사합니다.
- 바이트 코드, 필드, 메서드, 생성자, static 변수, static 메서드, 런타임 상수 풀(클래스 및 인터페이스의 상수 뿐만 아니라 메서드나 필드의 실제 메모리상 주소) 등 클래스 및 인터페이스 정보와 관련된 모든 내용이 저장되는 영역입니다.
- 메서드 영역에서도 GC가 동작하기도 합니다. GC가 Heap영역에서 객체의 메모리를 회수할 때, 해당 클래스의 인스턴스가 Heap 영역에 없으면, 메서드 영역에서도 클래스의 메타데이터가 삭제됩니다.
- 힙 영역 (Heap Area)
- new 키워로 생성된 객체와 배열이 할당되는 영역입니다.
- JVM이 실행 중인 시스템 메모리 중 가장 큰 부분을 차지하며, GC(Garbage Collection) 동작하는 영역입니다.
실행 엔진 (Execution Engine)
- 클래스 로더를 통해 JVM 내부로 넘어와 런타임 메모리 영역(JVM 메모리)에 배치된 바이트코드(.class)들을 읽어 기계어로 번역하여 실행해 줍니다. 이때 실행 엔진은 인터프리터와 JIT 컴파일러 두 가지 방식을 혼합하여 바이트 코드를 실행합니다.
- 실행 엔진 내부적으로는 인터프리터, JIT 컴파일러, GC가 있습니다.
- Interpreter (인터프리터)
- 바이트 코드는 기본적으로 인터프리터 방식으로 동작한다. 실행 엔진은 자바 바이트 코드를 명령어 단위로 읽어서 기계어로 번역하여 실행합니다.
- 한 줄씩 수행하기 때문에 느리다는 단점이 있습니다.
- JIT (Just-In-Time)
- 실행 시점에 인터프리터 방식으로 기계어 코드를 생성하면서 그 코드를 캐싱하여, 같은 함수 가 여러 번 불릴 때 매번 기계어 코드를 생성하는 것을 방지합니다.
- 즉, 전체 컴파일 후 캐싱 → 이후 변경된 부분만 컴파일하고, 나머지는 캐시에서 가져다 바로 실행합니다.
- GC(Garbage Collection)
- 더 이상 사용되지 않는 데이터를 정리하는 역할을 수행합니다. 이렇게 하여 한정된 메모리를 효율적으로 사용할 수 있습니다.
- 주로 힙 영역의 데이터를 정리하는 역할을 하고, 메서드 영역에서도 GC가 동작하기도 합니다.
네이티브 메서드 인터페이스 (JNI; Java Native Interface)
- 자바 애플리케이션이 네이티브(자바가 이외의 언어) 메서드를 호출할 수 있게 해주며, 필요 시 네이티브 라이브러리를 로드하여 사용할 수 있습니다. 이를 통해 자바 코드와 네이티브 코드 간 상호 작용을 할 수 있습니다.
- 일반적으로 자바 코드보다 빠른 네이티브 메서드를 호출하여 최적화함으로써, 성능 향상을 할 수 있다는 장점이 있습니다. 하지만, 복잡성이 증가하고 특정 플랫폼(OS)에 종속될 수 있다는 단점이 존재합니다.
네이티브 메서드 라이브러리 (Native Method Library)
- JNI를 통해 자바 애플리케이션에서 호출될 수 있는 네이티브(자바가 이외의 언어) 메서드의 구현을 포함하는 라이브러리입니다.
- 만일 헤더가 필요하다면, JNI는 이 라이브러리를 로딩하여 실행합니다.
7. JDK와 JRE의 차이
JDK = JRE + @(개발에 필요한 도구)
JDK는 Java 애플리케이션 개발을 위해 제공하는 자바 개발 툴로, 실행을 위해 필요한 JRE와 개발에 도움을 주는 도구들로 이루어져있습니다. JRE는 JVM과 자바 프로그램을 동작시킬 때 필요한 라이브러리들을 포함합니다.
JRE는 읽기 전용, JDK는 읽기/쓰기 전용으로 볼 수 있습니다.
JDK(Java Development Kit)란?
- Java 애플리케이션을 개발하고 실행하기 위한 개발 환경의 세트를 의미합니다.
- JRE와 개발, 모니터, 디버깅, 배포를 위해 필요한 도구(javac.exe, javadoc.exe, jar.exe 등)을 포함합니다.
JRE(Java Runtime Environment)란?
- Java 애플리케이션을 실행하기 위한 환경으로, 개발할 필요는 없는데 실행은 시켜줘야 하는 경우에 꼭 필요합니다.
- JVM과 자바 프로그램을 동작시킬 때 필요한 것들(java.exe, Java Class Libraries 등)을 포함합니다.
참고
https://wonyong-jang.github.io/java/2020/11/08/Java-JVM.html
https://velog.io/@dondonee/Java-JVM-JRE-JDK-%EC%B0%A8%EC%9D%B4
https://sowhat4.tistory.com/61
'Language > Java' 카테고리의 다른 글
문자열 뒤집기(StringBuffer) (0) | 2023.12.21 |
---|