운영체제에 독립적
JVM(Java Virtual Machine)
- Java로 작성된 프로그램이 돌아가도록 만들어주는 것
- 자바 소스코드로 만들어지는 .class 파일을 실행할 수 있음
- 자바 바이트 코드를 실행하는 가상 환경
- 운영체제가 달라져도 자바 코드가 동일하게 작동할 수 있도록 환경을 제공함
- WORA(Write one Run Anywhere)를 만족시킴
- 한 번 작성한 프로그램은 어떤 운영체제 혹은 플랫폼에서도 동일하게 실행되므로 이식성이 좋음
JRE(Java Runtime Environment)
- JVM이 자바 프로그램을 동작시킬 때 필요한 라이브러리 파일 등을 가지고 있음
- JRE는 JVM, Java Class Librarie, Class Loader 요소로 구성됨
- Java Class Librarie: java.io, java.util, java.thread 등 작동에 필수적인 요소를 가지고 있음
- Class Loader: 필요한 클래스들을 JVM 위로 올려주는 역할을 함
JDK(Java Development Kit)
- 자바를 활용하여 프로그램을 개발할 때 필요한 도구 모음
- 자바 프로그램을 실행해야 하므로 JRE를 포함하고 있으며, 컴파일러(javac)와 디버거(Jdb) 등이 포함되어 있음
- 개발과 실행에 필요한 환경 및 기능 제공의 폭에 따라 표준형인 SE(Standard Edition)과 여러 기능이 추가된 EE(Enterprise Edition)으로 나뉨
- JDK 8버전과 11버전이 가장 널리 쓰임
자바 컴파일 과정
- .java 코드 작성 후 run 버튼 눌러서 실행
- 자바 컴파일러가 소스코드를 읽어서 바이트 코드로(.class) 컴파일, 여기서 바이트 코드는 컴퓨터는 읽을 수 없지만 JVM은 읽을 수 있음
- 컴파일된 바이트 코드를 JVM의 클래스 로더에 전달
- 클래스 로더는 동적 로딩을 통해서 필요한 클래스들을 로딩 및 링크하여 런타임 데이터 영역(Runtime Data Area), 즉 JVM의 메모리에 올림
- 실행엔진(Execution Engine)은 JVM 메모리에 올라온 바이트 코드들을 명령어 단위로 하나씩 가져와서 실행
- 인터프리터 방식: 바이트 코드 명령어를 하나씩 읽어서 해석하고 실행, 전체적인 실행속도가 느림
- JIT 컴파일러 방식: 바이트 코드 전체를 컴파일하여 바이너리 코드로 변경하고 직접 실행하는 방식, 전체적인 속도가 인터프리터 방식보다 빠름
자바 컴파일 과정에서 JVM의 역할
- 바이트 코드로 변환된 코드를 클래스 로더가 JVM의 메모리에 올리면 JVM의 실행엔진인 인터프리터나 JIT 컴파일러가 해석하고 실행하는 역할을 함
- JVM은 다른 프로그램을 실행시키는 것이 목적이므로, 자바 프로그램이 어느 플랫폼이나 운영체제 상에서도 실행될 수 있도록 하고, 프로그램 메모리를 관리하여 최적화 함
Class Loader
자바 컴파일러를 통해서 .class 확장자를 가진 파일들은 한 번에 로드되지 않고, 애플리케이션에서 필요할 때 로드된다. 클래스 파일들은 각 디렉터리에 흩어져있으며, 기본적인 라이브러리 클래스 파일들은 환경변수 설정 시 지정했던 $JAVA_HOME 내부 경고에 존재한다. 따라서 각각의 클래스 파일들을 찾아서 JVM 메모리에 탑재해주는 역할을 하는 것이 바로 ClassLoader의 역할이다.
ClassLoader는 크게 Loading, Linking, Initialization 3가지 역할을 한다.
Loading
각 디렉터리에 흩어진 클래스 파일을 JVM 메모리에 탑재한다. 각 클래스 파일이 개발자가 작성한 것인지, 기본 제공 클래스 파일인지와 같은 기준에 의해서 세 가지로 나뉜다.
Bootstrap ClassLoader
- JVM 실행 시 가장 최초로 실행되는 클래스 로더
- 다른 모든 클래스 로더의 부모가 되는 클래스 로더
- JVM을 구동시키기 위한 가장 필수적인 라이브러리의 클래스들을 JVM에 로드함
- 가장 상위 클래스 로더이므로 탑재되는 운영체제에 맞게 네이티브 코드로 작성됨
Extensions ClassLoader
- Bootstrap ClassLoader 다음 우선순위를 가지는 클래스 로더
- localedata, zipfs 등 확장 자바 클래스 라이브러리를 JVM에 로드함
- ${JAVA_HOME}/jre/lib/ext에 있는 클래스 파일들을 로드함
Application ClassLoader(System ClassLoader)
- 개발자가 자바 코드로 작성한 클래스 파일들, Classpath에 있는 클래스들을 로드함
- 개발자가 클래스 로더를 직접 구현할 경우, Application ClassLoader의 자식 형태인 ClassLoader를 구성하게 됨
위의 ClassLoader를 모두 거쳐도 클래스 파일을 찾지 못하면, ClassNotFoundException 예외가 발생한다.
Linking
로드된 클래스 파일들을 검증하고, 사용할 수 있게 준비하는 과정이다. 크게 Verification, Preparation, Resolution의 세 단계로 나뉜다.
Verification
- 클래스 파일이 유효한지 확인하는 과정
- 클래스 파일이 JVM의 구동 조건대로 구현되지 않았을 경우 VerifyError 발생
Preparation
- 클래스 및 인터페이스에 필요한 static field 메모리를 할당하고 이를 기본값으로 초기화
- 초기화된 값들은 뒤의 Initialization 과정에서 코드에 작성된 초기값으로 변경됨
Resolution
- Symbolic Reference 값을 JVM의 메모리 구성 요소인 Method Area의 런타임 환경 풀을 통해 Direct Reference라는 메모리 주소 값을 바꿔줌
- JVM Instruction의 new와 instanceof가 영향을 받음
Initialization
클래스 파일의 코드를 읽는 단계이다. class와 interface의 값들을 지정한 값들로 초기화하고, 초기화 메소드를 실행시킨다.
이때 JVM은 멀티스레딩으로 작동하며, 동시에 초기화를 하는 경우가 있기 때문에 초기화 단계에서도 동시성을 고려해줘야 한다.
클래스 로더 과정이 끝나면 본격적으로 JVM에서 클래스 파일을 구동시킬 준비가 끝난다.
Runtime Data Area(JVM Memory)
JVM의 Runtime Data Area에는 크게 Method Area, Heap, Java Stacks, Native Method Stacks, PC registers 가 존재한다.
Method Area
- 인스턴스 생성을 위한 객체 구조, 생성자, 필드 등이 저장됨
- Rumtime Constant Pool과 static 변수, 메소드 데이터, Class 데이터들도 이 곳에서 관리됨
- 이 영역은 JVM 당 하나만 생성되므로, 모든 Thread들이 Method Area를 공유함
- JVM의 다른 메모리 영역에서 정보 요청이 오면, 실제 물리 메모리 주소로 변환해서 전달함
- JVM 구동 시 생성되고, 종료시까지 유지됨
Heap
- 코드 실행을 위한 Java로 구성된 객체 및 JRE 클래스들이 탑재됨
- 문자열 정보를 가진 String Pool, 실제 데이터를 가진 인스턴스, 배열 등이 저장됨
- JVM 당 하나만 생성되고, 해당 영역의 데이터는 모든 Java Stack 영역에서 참조되어, Thread들이 공유함
- Heap 영역이 가득 차면 OutOfMemoryError가 발생함
- Heap에서는 참조되지 않는 인스턴스와 배열에 대한 정보를 얻을 수 있기 때문에 GC의 주 대상이 됨
- Thread 별로 메모리를 할당받는 Stack 영역에 비해 속도가 느림
- 모든 Thread가 공유하므로 동시성 문제가 발생하며, synchronized 블록을 사용하는 등의 방법을 통해 동시성을 지켜줘야 함
Java Stacks
- 각 Thread 별로 할당되므로 Heap 메모리 영역보다 빠름
- 각 Thread 별로 메모리를 따로 할당하므로 동시성 문제에서 자유로움
- 각 Thread는 메소드를 호출할 때마다 Frame이라는 단위를 추가(push)하며, 메소드가 마무리되어 결과를 반환한 Frame은 해당 Stack에서 제거(pop)됨
- Frame은 메소드 안의 지역변수를 가지고 있는 Local Variable, 메소드 내 연산을 위한 Operand Stack, Constant Pool 참조를 위한 Constant Pool Reference로 구성됨
- 메소드가 호출될 때마다 Stack에 Frame이 쌓임
- Frame의 연산이 끝나면, 호출한 상위 Frame에 결과값을 반환함
- Java Stack 영역이 가득 차면 StackOverflowError가 발생함
Native Method Stacks(C Stacks)
- 다른 프로그래밍 언어로 작성된 메소드들을 Native Method라고 함
- Native Method Stacks는 다른 프로그래밍 언어로 작성된 메소드들을 다루는 영역
- Java Stacks 영역과 유사하게 Native Method가 실행될 경우 Native Method Stack에 해당 메서드가 쌓임
- Thread 별로 Native Method Stack을 가지고 있음
PC registers
- Thread가 각자의 메소드를 실행할 때, Thread 별로 동시에 실행하는 환경이 보장되어야 하므로 명령어 주소값을 저장할 공간이 필요함
- Thread들은 각각 PC Registers를 가지고 있으며, PC Registers는 JVM에서 사용된 명령의 주소값을 저장함
- 실행한 메소드가 Native 하다면 undefined가 기록됨