JVM(Java Vitual Machine)이란
자바 프래그램의 실행 환경을 만들어주는 가상 머신
자바의 플랫폼 독립성
자바 컴파일러는 자바파일(*.java)을 자바 바이트코드(*.class)로 컴파일한다.
JVM은 플랫폼(OS) 별로 존재하며 자바 바이트코드를 바이너리 코드(기계어)로 변환하여 컴퓨터(CPU)가 처리할 수 있도록 한다.
따라서 자바는 바이트 코드라는 플랫폼에 종속되지 않고 JVM 처리할 수 있는 중간 언어와 플랫폼마다 존재하는 자바 가상 머신을 통하여
운영체제에 상관 없이 실행할 수 있는 환경을 제공한다.
* 바이트 코드 : 특정 하드웨어가 아닌 가상 머신에서 사용되는 언어
* 바이너리 코드 : 컴퓨터가 이해할 수 있는 언어로 OS마다 다름
자바 가상 머신의 동작 방식
- main 메서드를 포함한 클래스를 실행시킴
- 컴파일러가 자바 소스를 바이트코드로 컴파일
- 컴파일된 코드가 JVM의 클래스 로더로 전달
- main 메서드가 포함된 클래스가 클래스 로더에 전달됨
- 클래스 로더에 main 메서드를 포함한 클래스와 java.lang, java.util과 같은 ㅈ바바 기본 api 클래스가 전달됨 - 클래스 로더는 JVM메모리 영역에 클래스 파일을 할당
[클래스 로더의 실행 순서]
1) loading : 클래스 파일을 JVM 메모리에 적재
2) verifying : 자바 언어 명세에 맞는 클래스파일인지 검증
3) preparing : 클래스, 필드, 메서드 등 필요한 메모리 할당
4) resolving : 클래스 상수 풀 내 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경
5) initializing : static 필드 값 초기화 - 실행 엔진(Execution Engine)에 의해 바이트 코드를 바이너리 코드로 해석하여 실행함
1) 인터프리터 : 바이트 코드를 한줄씩 해석하여 실행, 똑같은 것 읽어도 매번 해석 -> 반복 작업 많을시 실행 속도 느림
2) JIT 컴파일러 : 바이트 코드를 미리 바이너리 코드로 변환하여 반복적으로 인터프리팅 하지 않고 바이너리 코드 실행
-> 반복 작업 많을시 인터프리터 보다 빠름 (한줄 자체 해석은 인터프리터보다 느림)
[참고] 용어 정리
JVM 내에서 이뤄지는 작업의 시점을 런타임이라고 하며, JVM 영역 밖 컴파일러에 의해 작업이 이뤄지는 시점을 컴파일 타임이라고 함
JVM의 구성요소
Class Loader
Class Loader는 클래스 파일을 JVM내의 메모리 영역(Runtime Data Area)에 적재하는 역할을 한다.
동적 로딩
클래스 로더는 런타임 시점에 클래스를 JVM 메모리에 로드하는 동적 로딩 특징을 가지고 있으며 아래와 같은 동작 방식을 갖는다.
1. 구동 시점 동적 로딩
public class Main { public static void main(String[] args) { System.out.println("hello"); } } |
위 프로그램은 java Main 이라는 명령어를 통해 실행하게 된다.
자바 프로그램을 실행할 때 전달하는 main 메서드를 포함한 최상위 클래스와 java.lang, java.util 등의 자바 기본 API 클래스들이
클래스 로더에 전달되어 실행된다.
2. 지연로딩(Lazy Loading)
public class Main { public static void main(String[] args) { System.out.println("Start main"); Sub sub = new Sub(); sub.doWork(); } } class Sub { public void doWork() { System.out.println("work"); } } |
위에서 Main은 구동시점에 로드되고 Sub는 main메서드에 의해 호출되는 시점에 로드된다.
3. 명시적 동적 로딩
Class.forName("com.example.MyClass") |
명시적 동적 로딩은 코드를 통해 직접 클래스를 로드하도록 명시할 수 있는 방식이다.
위와 같이 리플렉션을 통해 필요로하는 클래스를 로드하게 할 수 있다.
개발자가 직접 로딩 시점을 설정할 수 있다.
클래스 로더의 로드 과정
로딩 (Loading) : 클래스 파일을 가져와서 JVM 메모리영역(Run Time Data Area)에 적재
링크 (Linking)
- 검증 (verifying) : JVM 명세 명시된대로 구성되어 있는지 검사 -> 검증 실패시 VerifyError 발생
- ex1. 유효한 바이트 코드 여부 검사
- ex2. 상속관계에 있는 클래스 파일 존재 여부 파악
(컴파일러가 하는 문법적 검사 아닌 컴파일된 클래스 파일 존재 여부 파악)
(상속관계에 존재하는 클래스 파일은 함께 가져옴, 지연로딩 X) - 준비 (preparing) : 메모리 할당
- 해석 (resolving) : 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경한다.
(상수값을 참조하고 있는 주소에서 실제 상수값 주소로 변경)
초기화 (Initializing) : 클래스 파일의 코드를 읽으며 정적변수에 값 할당, 계층 구조시 부모에서 자식까지 static 블록 초기화 실행
* 심볼릭 레퍼런스 : 클래스파일 경로
* 다이렉트 레퍼런스 : 실제 메모리에 할당된 주소
Execution Engine
클래스 로더에 의해 JVM 메모리에 할당된 바이트 코드를 실행시키는 엔진
코드를 실행하는 방식은 크게 2가지로 Interpreter와 JIT Compiler가 존재한다.
Interpreter
- 바이트코드를 한줄씩 해석하여 실행함
- 바이트코드 한줄의 해석은 빠르지만 실행시 마다 매번 해석하여 반복 작업이 많은 경우 속도 저하
(같은 메서드라도 여러번 호출될 때 매번 새로 수행해야 함)
* 자바는 기본적으로 인터프리터 방식으로 동작함
JIT (Just In Time) Compiler
- Interpreter의 단점을 보완하기 위해 도입했음
- 실행 시점에 바이트 코드 전체를 기계어로 변환하여 실행하는 방식
한번 변환한 기계어는 지속적으로 재사용하므로 반복 수행 명령어가 많은 경우 인터프리터 보다 빠르다.
허나, 명령어 한줄의 해석 시간은 인터프리터 보다 느려 반복 수행 명령어가 많은 경우에 사용이 유리하다.
JIT 활성화 조건
자바는 기본적으로 Interpreter 방식으로 바이트 코드를 실행시킨다. 허나, 임계치 도달하면 JIT 방식으로 코드를 실행을 변경시킨다.
* (임계치) = (메서드가 호출된 횟수) + (메서드 루프문 반복 횟수)
Runtime Data Area
런타임 데이터 영역은 JVM 메모리 영역이라고 불리우며 자바 애플리케이션을 실행하며 사용하는 데이터들을 적재하는 영역이다.
Method Area | - 클래스 구조(메서드, 생성자, 필드, static 포함)와 상수들이 저장됩니다. - 정적 영역이라고 불리움 |
Heap Area | - 런타임시 동적으로 할당하여 사용하는 메모리 영역 - new 연산자로 생성된 인스턴스(참조 타입)와 배열 저장 - 참조 타입 변수의 메모리 주소가 Heap 영역을 가리킴 - 가비지 컬렉터의 관리 대상 |
Stack Area | - 메서드를 실행하며 생기는 임시 정보 저장 - 메서드 매개변수, 지역변수, 리턴값, 연산중 발생하는 데이터 저장 - method가 호출될 때마다 스택 프레임이 생성 - 스택 프레임은 메서드의 생명주기와 같음 - 후입선출(LIFO, Last In Firstt Out) 구조로 push와 pop 기능 사용 - 원시타입 변수는 스택 영역에 직접 값을 갖음 |
PC Register (Program Counter) |
- 현재 수행 중인 JVM 명령 주소를 갖음 - 해당 영역은 스레드가 시작될 때 초기화 되며 스레드가 종료되면 메모리에서 해제됩니다. - 스레드 전환시 해당 스레드의 PC 레지스터 값이 로드되어 실행된다. |
Native Method Stack Area | - 자바 외 언어로 작성된 네이티브 코드를 위한 Stack - 네이티브 메소드의 매개변수, 지역변수 등을 바이트 코드로 저장 - JIT에 의해 실행되는 명령어가 이 영역에서 실행된다. |
* 스택 프레임 : 현재 실행중인 메서드의 상태 정보 저장
* stack 영역에서 new 연산자를 통해 생성한 객체의 참조 주소만 갖고 있고 스택에서 객체의 참조 주소를 통해 객체를 조작함
스택에 의해 참조 되지 않는다면 가비지 컬렉터에 의해 제거 대상이 됨
스레드 공유 여부
스택 영역과 PC 레지스터는 스레드마다 생성되는 영역으로 스레드 종료시 사라지는 영역이며,
힙 영역과 메서드 영역은 모든 스레드가 공유하는 영역으로 프로그램 종료시 제거되는 영역이다.
Heap Area
Heap 영역은 Young Generation과 Old Generation으로 나뉜다.
Young Generation
- Eden : new 연산자를 통해서 생성된 객체가 위치
- Survivor 0 / Survivor 1 : Eden 영역에서 GC에 의해 제거되지 않고 살아남은 객체가 S0, S1으로 차례대로 이동됨
(S0에서 GC 처리 후 살아남은 객체 S1으로 이동 - 마이너 가비지 컬렉션이 사용됨
Old Generation
- Young Generation에서 살아남은 객체가 존재
- Old 영역은 Young 영역보다 크게 할당되어 있으며 올드영역에서는 가비지컬렉터가 young 영역보다 적게 발생
- 메이저 가비지 컬렉션 사용됨
Stack
메서드를 실행하며 발생하는 변수들의 정보가 저장되는 영역으로 메서드 호출시마다 메서드마다 스택 프레임이 생성되고
매개변수, 지연변수, 리턴 값 및 연산 결과들이 저장되고 메서드 종료시 스택 프레임은 삭제된다.
스택과 힙의 차이
1. 저장되는 데이터
스택의 경우 기본 타입의 값들만 저장되며 힙은 참조타입 객체를 저장하게 된다.모든 연산은 스택에서 이뤄지는데 스택에서는 참조타입 객체의 주소값을 가지고 있어힙 영역에 접근하여 참조타입 객체를 조작한다.
2. 사이즈 결정 시점 및 변경 가능 여부
스택의 사이즈는 컴파일 타임에 크기가 결정, 힙 사이즈는 런타임에 변경 가능하다.
3. 변수 할당 해제(메모리 관리)
스택은 메서드 종료시 스택 프레임이 제거되므로 할당된 변수가 빠르게 제거된다.
허나, 힙은 가비지 컬렉터에 의해 시간의 흐름에 따라 참조되는지 여부를 따져 제거하는 방식을 따른다.
4. 지역성 및 연속성
스택에서 변수의 메모리를 할당하는 스택 프레임은 메서드 스코프 내에 있는 변수들만 다룬다.
이는 메서드 실행시 사용하는 변수들이 스택프레임에 집중되어있고 실행 순서별로 스택에 쌓이므로 연속적인 특징을 지닌다.반면, 힙의 경우 지역성과 연속성을 보장하지 않고 임의의 위치에 할당된다.
PC 레지스터
CPU의 PC 레지스터
- PC(프로그램 카운터)는 CPU에 의해 다음에 실행될 명령어의 주소를 저장한다.
- CPU가 명령어를 실행할 때마다, PC는 자동으로 증가하여 다음 명령어의 위치를 가리킨다.
- CPU의 레지스터는 CPU내에 존재하는 기억장치이다.
JVM의 PC 레지스터
CPU의 PC 레지스터라는 공간이 있음에도 불구하고 JVM이 별도의 PC 레지스터라는 공간을 사용하는 이유에 대해 알아보자.
CPU는 기계어를 수행할 수 있다. 허나 자바 컴파일 소스는 기계어가 아닌 바이트 코드 형태로 존재하고
인터프리터에 의해 실행 시마다 해당 라인을 기계어로 변환하여 CPU에 의해 수행된다.
즉, 실행 시점에 기계어를 만드므로 다음에 수행할 기계어 주소가 존재하지 않는다.
따라서 자바는 JVM의 PC 레지스터 공간을 별도로 만들어 다음에 수행할 바이트 코드 주소를 저장시키고
CPU에 의해 실행될 때 해당 바이트 코드가 참조되게 하고 인터프리터에 의해 기계어로 변환되어 수행될 수 있도록 한다.
허나, 인터프리터가 아닌 JIT를 통해 명령어가 수행되는 경우, 바이트 코드가 아닌 기계어로 존재하기에
CPU의 PC 레지스터 공간을 사용하고 JVM의 PC 레지스터 공간에는 주소값이 저장되지 않는다.
네이티브 메서드 스택
네이티브 메서드 스택 영역은 메서드 스택 영역과 역할을 같지만 네이티브 코드를 위한 스택이다.
인터프리터 실행환경인 메서드 스택영역과 JNI(Java Native Interface)라는 인터페이스와 이를 구현한 Native Method Library 실행환경을 명확히 분리하기 위하여 별도로 제공되는 스택이다.
JVM 을 설명하며 가비지 컬렉터에 대한 설명은 하지 않았는데 가비지 컬렉터에 대한 설명은 아래 포스팅에서 진행하겠다.
https://developer111.tistory.com/entry/%EA%B0%80%EB%B9%84%EC%A7%80-%EC%BB%AC%EB%A0%89%EC%85%98
'Language > 자바&코틀린' 카테고리의 다른 글
[진행중][자바, 코틀린] 제너릭이란? 그리고 공변성과 원시(primitive) 타입 비허용 (0) | 2023.12.20 |
---|---|
String, StringBuilder, StringBuffer의 차이 (0) | 2023.11.15 |
불변(Immutable)객체와 가변(mutable) 객체 (0) | 2023.11.15 |
인터페이스와 추상클래스의 차이 (0) | 2023.11.15 |
java.lang.OutOfMemoryError: Java heap space 오류 해결 (0) | 2021.01.14 |