Java Virtual Machine
자바를 실행하기 위한 가상 기계, 자바와 운영체제 사이에서 중개자 역할을 수행하며, 자바가 운영체제에 구애 받지 않고 프로그램을 실행할 수 있도록 도와줍니다.
*가상기계: 소프트웨어로 구현된 하드웨어를 뜻하는 넓은 의미의 용어
자바는 완전한 기계어가 아니라 바이트코드이기 때문에 이를 해석하고 실행 할 수 있는 가상 운영체제가 필요한데,이것이 JVM입니다.
운영체제별로 따로 실행하고 관리하는 방법이 다르기에 별도로 운영체제와 자바 프로그램을 실행하고 관리하는게 아닌 중계하는 JVM을 두어서 여러 운영체제에서 동일한 실행 결과를 나타내도록 설계한것입니다.
또한 Java 는 OS에 종속적이지 않다는 특징을 가지고 있는데, OS에 종속받지 않고 실행되기 위해선 OS 위에서 Java 를 실행시킬 무언가가 필요하게 되는데, 그 역할을 하는 것이 JVM입니다.
*컴파일 : 프로그램 코드를 기계가 이해할 수 있는 언어로 바꾸는 작업
그렇다면 JVM을 통해 OS가 어떻게 우리가 작성한 Java코드를 읽을 수 있을까?
1)
우리가 작성한 Java 소스코드, 즉 원시코드(*.java
)는 기계가 이해할 수 없기 때문에 기계어로 컴파일을 해줘야한다.
하지만 Java는 이 JVM 이라는 가상머신을 거쳐서 OS에 도달하기 때문에 OS가 인식할 수 있는 기계어로 바로 컴파일 되는게 아니라 먼저, Java compiler를 통해 JVM이 인식할 수 있는 Java bytecode(*.class
)로 변환된다.
→ Java Complier 의javac
라는 명령어를 사용하면.class
파일을 생성할 수 있다.
2)
우리의 기대와는 달리, 변환된 bytecode는 기계어가 아니기 때문에 아직 OS에서 실행되지 않는다.
이 때, JVM이 OS가 bytecode를 이해할 수 있도록 해석해준다.
3)
실제 바이트 코드를 실행하는 시점에서 자바 가상 머신(JVM, 정확히는 JRE)이 바이트 코드를 JIT 컴파일러를 통해 기계어로 변환한다.
요약 ✂️
Java소스코드를 (.java) Java compiler의 javac 명령어를 통해 JVM이 인식할 수 있는 Bytecode(.class)로 변환 ➡️ JVM을 구동시켜주는 java명령어를 통해 .class파일을 실행 ➡️ JVM의 Class Loader를 통하여 바이트코드를 JVM내로 로드하고 Execution Engine에서 두 가지 방식을 활용(interpreter, JIT)하여 기계어로 바꿔준다.
Java에서 기존 클래스 파일인 바이트코드를 실행하는 방법은 interpreter 방식이다. 하지만, interpreter는 명령어를 하나씩 해석하여 처리하기 때문에 명령어를 하나씩 실행하는 속도는 빠를지 모르지만 전체적인 코드를 생각하면 실행 속도가 느린 것을 알 수 있다. 이를 해결하기 위해 JIT 컴파일러가 등장하였다.
😲 JIT 컴파일러란❓
Just-In-Time
프로그램을 실제 실행하는 시점(런타임 시점)에 기계어로 번역하는 컴파일러
JIT 컴파일러는 자주 사용하는 코드를 캐싱하고 이후에 같은 코드가 나오면 캐싱된 것을 가져다 쓴다. 매번 명령어를 하나씩 해석하여 처리하는 interpreter 방식보다 빠르게 기계어로 번역할 수 있다.
하지만 , JIT 컴파일러는 초기 구동할 때 런타임 단계에서 컴파일하는데 시간과 메모리를 소모하기 때문에 JVM은 모든 코드를 JIT 컴파일러 방식으로 실행하지 않고, 인터프리터 방식을 사용하다가 일정한 기준이 넘어가면 JIT 컴파일러 방식으로 실행한다.
😲 JVM은 어떤 구조로 구성되어 있을까❓
✎ Class Loader
.class에서 바이트코드를 읽고 메모리에 저장한다.
단계 설명 로딩 loading class를 읽어오는 과정 링크 linking 레퍼런스를 연결하는 과정 초기화 initialzation static 값 초기화 및 변수에 할당
✎ Runtime Data Area
= Memory Area
JVM이 프로그램을 수행하기위해 운영체제로부터 할당받은 메모리 공간
자바 애플리케이션을 실행할 때 사용되는 데이터들을 적재하는 영역
1) Stack
- 지역변수, 메소드의 매개변수,리턴 값 등과 같이 메서드 안에서 사용되는 값들을 임시로 저장
- stack에서는 heap 영역의 객체를 참조할 수 있으며, 기본타입 변수는 stack에 생성됨
- 메서드 호출 시마다 각각의 스택 프레임(그 메서드만을 위한 공간)이 생성됨, 메서드 수행이 끝나면 프레임별로 삭제함
- JVM은 오직 JVM 스택에 스택 프레임을 추가하고(push) 제거하는(pop) 동작만 수행한다.
- Last In First Out, 나중에 들어온 데이터가 먼저 나간다(선입후출)
2) PC Register
- 스레드가 시작될때 생성되는 공간
- 어떤 부분을 무슨 명령으로 실행해야할 지에 대한 기록을 하는 부분
- JVM이 실행하고 있는 현재 주소를 저장하는 역할
3) Method Area = 클래스 영역
- JVM이 실행되면서 생기는 공간이다.
- Class 정보, 전역변수 정보, Static 변수 정보가 저장되는 공간이다.
- Runtime Constant Pool 에는 말 그대로 '상수' 정보가 저장되는 공간이다.
- 모든 스레드에서 정보가 공유된다.
4) Heap
- new 연산자로 생성된 객체, Array와 같은 동적으로 생성된 데이터가 저장되는 공간
- Garbage Collector가 참조되지 않는 메모리를 확인하고 제거하는 영역
- Reference Type 의 데이터가 저장되는 공간
- 모든 스레드에서 정보가 공유됨
- 힙 영역은 메모리의 낮은 주소에서 높은 주소의 방향으로 할당됨
5) Native Method Stack
자바 외 언어로 작성된 네이티브 코드 및 메소드를 실행을 위한 메모리 영역
Class Loader에 저장된 Byte Code를 명령어 단위로 분류하여 하나씩 실행하게 하는 엔진
엔진 설명 interpreter 바이트코드를 한줄씩 실행한다. JIT 컴파일러 interpreter의 효율을 높이기 위해 반복되는 코드를 캐싱하고, 컴파일된 코드를 바로 사용한다. Garbage Collector 더 이상 참조하지 않는 객체들을 정리한다.
💛 Write Once, Run Anywere(한 번 작성하면 어디서든 실행된다)
JVM을 통해 운영체제에 독립적으로 실행 가능한 환경을 제공해주기 때문에,
JVM을 이용하여 가상 컴퓨터 환경을 구축하고 각 운영체제에 영향을 받지 않고 자바 코드를 실행할 수 있다.
💛 메모리 관리
JVM의 GC(가비지 콜렉션)에 의해 메모리 관리가 자동으로 되어 사용 중이지 않은 메모리를 알아서 찾아서 해제해주기 때문에, 개발자가 따로 메모리 관리에 신경 쓰지 않아도 된다.
한 번의 컴파일링으로 기계어가 만들어지는 C와 C++(일괄 컴파일 방식 언어)에 비해 속도가 느리다.
😲 속도가 느린이유는 무엇일까❓
일반 애플리케이션의 코드는 OS만 거치고 하드웨어로 전달되는데, Java 애플리케이션은 JVM을 한 번 더 거칠뿐 아니라 하드웨어에 맞게 완전히 컴파일 된 상태가 아닌, 실행 시에 해석(interpret)되기 때문에 속도가 느리다는 단점이 있다. JVM이 컴파일하여 생성한 byte code를 사용하긴 하지만, JVM이 실행중에 byte code를 native code로 변환하는 시간을 필요로 하기 때문이다. 그러나 기계어로 빠르게 변환해주는 JVM 내부의 최적화된 JIT 컴파일러를 통해서 속도의 격차는 많이 줄어들고 있다.
😲 왜 Java언어는 native code(=machine code)가 아닌 byte code를 생성하는 것일까❓
자바의 특징인 이식성 때문이다.
직접 native code를 작성하게 되면 , 그 native code는 CPU에 종속적인 특성을 갖게된다. 즉 컴파일 된 CPU에서만 돌아간다는 것이다. 반면에 byte code는 JVM 위에서 돌아가기 때문에, 어떤 플랫폼이건 JVM만 있으면 실행이 가능하게 되는 것이다.
😲 binary code, native code(=machine code), byte code의 차이는❓
바이트코드(Byte Code) : 가상머신(Virtual Machine)이 인식할 수 있는 코드
- 소스코드를 가상머신이 이해할 수 있는 중간코드로 컴파일 한 것.
- 이름이 바이트 코드인 이유는 소스코드가 자바 컴파일러에 의해 변환되는 코드의 명령어 크기가 1Byte이기 때문이다.
- 자바 바이트 코드의 확장자는 '. class'이다.
- 자바 바이트 코드는 JVM만 설치되어 있다면 어떤 OS든 실행될 수 있다.
바이너리 코드(Binary Code) : 컴퓨터가 인식할 수 있는 0과 1로 구성된 이진코드를 의미
- 컴파일러에 의해 생성된 목적 파일이 Binary code라 이해하면 쉽다.
단, 바이너리 코드는 컴퓨터가 이해할 수 있는 언어지만 '링커'에 의해 메모리 주소값을 반영하고 CPU가 직접 해독하고 실행할 수 있도록 수정되어야 기계어가 되는 것이기 때문에 가장 기계어와 유사한 레벨의 코드지만 완전한 기계어는 아니다.
기계어(Machine Language) : 0과 1로 구성된 바이너리 코드
- 기계어가 이진코드로 이루어졌을 뿐. 모든 이진코드가 기계어는 아니다. (바이너리코드 != 기계어)
- 기계어는 특정 언어가 아니다.
- CPU가 직접 해독하고 실행할 수 있는 비트 단위(0과 1)로 쓰인 컴퓨터 언어로 가장 Low Level Language다. CPU의 종류에 따라 서로 다른 코드를 갖게 된다.
😲 왜 소스 코드를 직접 실행하지 않고 byte code로 변환할까❓
byte code는 특정 하드웨어가 아닌 가상 컴퓨터에서 돌아가는 실행 프로그램을 위한 이진표현법이다. 하드웨어가 아닌 소프트웨어에 의해 처리되기 때문에, 보통 기계어보다 더 추상적이다.
바이트코드는 특정 하드웨어에 대한 의존성을 줄이고, 인터프리팅도 쉬운 결과물을 생성하고자 하는 프로그래밍언어에 의해, 출력 코드의 한 형태로 사용된다. 컴파일되어 만들어진 바이트코드는 특정 하드웨어의 기계 코드를 만드는 컴파일러의 입력으로 사용되거나, 가상 컴퓨터에서 바로 실행된다.
사람이 읽기 쉽도록 쓰인 소스코드와 비교하면, 바이트 코드는 덜 추상적이며, 더 간결하고, 더 컴퓨터 중심적이다. 예를 들어 바이트코드는 변수의 접근 범위(지역변수 또는 전역변수 인지 여부) 등과 같은 의미 분석 단계의 결과를 부호화한다. 그래서 일반적으로 소스 코드를 직접 분석/실행하는 것보다 더 좋은 성능을 보여 준다.
😲 JVM은 JAVA언어에서만 사용할까❓
JVM은 자바언어 에서만 사용하지 않는다. 대표적으로 스칼라, JRUBY, 코틀린도 JVM 이라는 가상 환경을 사용한다.
[1주차] JVM은 무엇이며 자바 코드는 어떻게 실행하는 것인가.
자바를 공부하기 전에 알아두면 좋을 것들!! #1 (JDK, JRE, JVM 알아보기)
JVM 메모리 구조란? (JAVA)