개요
우선 JVM 자체가 OS 위에서 자바 프로그램을 실행시켜 주는 역할을 수행해
운영체제에 독립적으로 실행할 수 있게 된다는 것은 당연히 알지만
JVM의 구성, 동작원리, 장단점 등에 대해 깊게 정리해보고자 한다.
컴파일 타입 환경에서의 자바 컴파일러
우리가 작성한 원시코드로 이루어진 java 파일은 JVM이 인식할 수 없기 때문에
자바 컴파일러가 java 파일을 자바 바이트코드로 이루어진 class 파일로 변환한다.
(java파일 > 어휘 분석 > 구문 분석 > 의미 분석 > 중간 코드 생성 후 최적화 = class 파일)
실제로 프로그램을 실행한 후에 java 파일들과 이름은 같지만 확장자가 다른
class 파일이 생성된 것을 확인할 수 있다.
cmd 창에서 직접 컴파일을 해봤으면 알 수도 있는데 그때 사용하는 javac 명령어는
JDK에 포함된 자바 컴파일러인 javac.exe를 의미한다.
이후 컴파일된 자바 바이트 코드를 JVM의 클래스 로더에 전달하게 된다.
런타임 환경에서의 JVM
클래스 로더는 동적로딩으로 필요한 클래스들을 로딩하고 링크하여
JVM의 메모리인 런타임 데이터 영역에 올리게 된다.
그 후 실행엔진이 메모리에 올라온 바이트 코드들을 명령어 단위로 가져와 실행하는데
이때, 인터프리터 방식과 JIT 컴파일러 방식을 사용한다.
클래스 로더의 세부 동작
클래스 로더는 로드 - 링크(검증 - 준비 - 분석) - 초기화 단계로 진행된다.
- 클래스 파일을 가져와 메모리에 로드
- 자바 언어 명세와 JVM 명세대로 구성되었는지 검증
- 필드, 메서드, 인터페이스 같이 클래스가 필요로 하는 메모리를 준비(할당)
- 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경(분석)
- 클래스 변수들을 적절한 값으로 초기화
자바 인터프리터
컴파일러는 클래스 파일 전체를 컴퓨터가 인식할 수 있는 이진 코드로 변환한다면
인터프리터는 런타임 시에 한 줄씩 읽으면서 이진 코드로 번역해서 실행한다.
추가적인 메모리를 사용하지 않고, 시스템의 이식성이 뛰어나며
코드가 수정되도 전체 코드를 다시 컴파일할 필요가 없다.
하지만 매번 인터프리팅을 거쳐야 하기 때문에 전체 실행 속도가 컴파일러에 비해 느리고
중간 코드로 변환되어 프로그램의 코드가 유출될 수 있다.
해당 방식을 기본으로 실행하며, 일정 기준이 넘어가면 JIT 컴파일 방식을 사용한다.
JIT 컴파일러
위의 인터프리터의 단점을 보완하기 위해 도입된 방식으로
바이트 코드 전체를 컴파일해 바이너리 코드로 변환해
매번 인터프리팅 하는 것이 아닌 바이너리 코드를 직접 실행한다.
한 번 컴파일만 완료되면 컴퓨터에서 빠르게 실행이 가능하고
기계어로 번역되어 코드 유출 위험이 적다.
하지만 코드가 수정된다면 컴파일을 다시 해야하고, 소스 파일 전체를 컴파일 해
용량이 크고, 모든 소스 파일을 번역하여 컴파일 시간이 비교적 느리며
목적 파일 생성을 위해 추가적인 메모리를 사용한다.
JVM의 구성
클래스 로더
메모리 영역 = [메서드 영역, 힙 영역, JVM 스택 영역, PC 레지스터, 네이티브 메서드 스택 영역]
실행 엔진 = [인터프리터, JIT 컴파일러, GC]
JNI
네이티브 메서드 라이브러리
JVM은 크게 클래스 로더와 메모리, 실행 엔진으로 구성되어 있으며
클래스 로더와 인터프리터, JIT 컴파일러는 위에서 살펴봤으니 나머지만 알아보겠다.
GC(Garbage Collection)
Heap 메모리 영역에서 더이상 사용하지 않는 메모리를 자동으로 회수해
C언어처럼 개발자가 메모리를 직접 관리할 필요가 없다.
일반적으로는 자동으로 실행되지만 실행되는 시간이 따로 정해져 있지는 않고
System.gc() 메서드를 사용해 수동으로도 가능하지만 실행이 보장되지도 않는다.
GC는 해당 게시글에 정리하기에는 내용이 적지 않은 편이라
추후에 따로 정리하겠다.
메모리 영역(런타임 데이터 영역)
자바 애플리케이션을 실행할 때 사용되는 데이터들이 적재되는 영역이다.
- 메서드 영역
- 클래스와 인터페이스의 런타임 상수 풀, 메서드와 필드, 클래스 변수, 메서드 바이트 코드 등이 적재
- JVM 시작시 생성되어 프로그램 종료 시까지 존재
- 런타임 상수 풀은 클래스와 인터페이스의 상수, 메서드와 필드에 대한 모든 레퍼런스를 저장하는 곳
- JVM은 런타임 상수 풀에서 주소를 찾아 참조
- GC 대상이 아니지만 명시적으로 null 선언시 GC 대상이 될 수 있음
- 모든 쓰레드가 공유
- 힙 영역
- 프로그램 상에서 런타임 시 동적으로 할당해 데이터를 저장하는 영역
- New 연산자로 생성된 인스턴스, 배열 같은 참조형 타입 저장
- JVM이 직접 관리
- 객체가 더 이상 사용되지 않거나, 명시적으로 null 선언시 GC 대상
- 객체와 배열이 저장된 곳일뿐 참조 주소는 스택 영역에 있다.
- 모든 쓰레드가 공유
- 스택 영역
- 선입후출 구조
- 메서드 호출 시 생성되는 스레드 수행정보를 기록하는 Frame 저장
- 메서드 정보, 지역변수, 매개변수, 연산 중 발생하는 임시 데이터 저장
- 스택 프레임(중괄호 블록)이나 메서드가 끝날 때 저장된 데이터들이 사라진다.
- 각 쓰레드 마다 생성되는 개별 영역
- PC 레지스터
- 현재 실행 중인 JVM 명령어 주소를 가지고 있다.
- CPU 명령어(instruction)를 수행한다
- JVM의 리소스를 이용해 연산을 수행해야 하기 때문에
- CPU가 직접 연산을 수행하는 것이 아닌 현재 작업 내용을 CPU에게 연산으로 제공
- 이를 위한 버퍼 공간이라고 볼 수 있다.
- 각 쓰레드 마다 생성되는 개별 영역
- 네이티브 메서드 스택 영역
- 자바 외 언어로 작성된 네이티브 코드를 위한 메모리 (C/C++ 등)
- 네이티브 메서드의 매개변수, 지역변수 등을 바이트 코드로 저장
- 네이티브 인터페이스 호출 및 종료 시 생성
- 각 쓰레드 마다 생성되는 개별 영역
JNI (Java Native Interface)
다른 언어로 만들어진 애플리케이션과 상호 작용할 수 있는 인터페이스를 제공한다
네이티브 메서드 라이브러리
다른 언어로 작성된 라이브러리를 칭하며 필요한 경우 JNI가 해당 라이브러리를 로딩한다.
'Java > Notion' 카테고리의 다른 글
JVM Warm Up (0) | 2024.03.04 |
---|---|
Garbage Collection 파헤치기 (0) | 2024.03.03 |
자바의 컴파일 과정 (0) | 2023.10.25 |
BigInteger (0) | 2023.05.08 |
Optional<T> (0) | 2023.05.03 |