과제를 받고 왜 JVM에 대해서 알아야 하는지 궁금해졌다. "메모리를 효율적으로 쓰려면 JVM의 구조를 알아야 한다." 는 하지만 솔직히 와닿지는 않는다. 그래서 일단 JVM에 대해서 알아보기로 했다.
자바를 개발하기 위해 사용되는 SDK(소프트웨어 개발 키드)다. 라이브러리, JRE, 컴파일러, 디버거 등 개발도구를 포함하고 있다.
자바를 실행하기 위한 최소 환경이다. 클래스 라이브러리, JVM 및 배포 도구가 포함되어 있다. JVM을 이용하여 Byte code를 실행시킨다.

Java Virtual Machine의 줄임말로 직역하면 자바 가상 기계라는 뜻이며 간단하게 말하면 '자바를 실행시키기 위한 가상 컴퓨터'라고 생각하면 된다.
실행 시에 하드웨어에 맞게 해석되기 때문에 속도가 느리다는 단점이 있다. 또한 자바가 OS에 독립적인 대신 JVM은 OS에 종속적이라 다른 Window에서 작성한 자바 애플리케이션을 Linux에서 실행시키려면 Linux용 JVM이 필요하다.
자바로 작성된 모든 애플리케이션의 소스코드는 우선적으로 Java 컴파일러를 통해 JVM이 이해할 수 있는 Java Byte code로 번역된다.
Java Byte code
자바를 컴파일 하면 JDK나 JRE에 포함되어 있는 javac.exe 실행파일이 실행되는데 이때 자바의 소스코드가 JVM이 이해할 수 있는 java Byte code 로 번역이 되는 것이다. 그리고 클래스 파일을 생성하게 된다. 클래스 파일 안에 존재하는 데이터가 바로 Byte code다.
그리고 JVM은 Java Byte code를 OS가 이해할 수 있는 기계어로 번역해준다.
- 기계어란 0과 1로 이루어진 바이너리 코드다.
- 기계어는 특정 언어가 아니라 CPU가 이해하는 명령어 집합이다. 바이너리 코드는 이진 코드라고도 부르는데 기계어 = 이진 코드는 아니다.
- 기계어는 캐시에 보관되기 때문에 한 번 컴파일 되고 다시 실행할 경우 빠르게 실행된다.

런타임, 즉 Byte code가 실행될 때 동적로딩 방식으로 클래스를 로드하고 Runtime Data Area에 클래스를 적재하는 역할을 맡고 있다. 클래스 로더는 Loading, Linking, Initialization 세가지 과정을 거친다.
한 번 로드된 클래스는 메소드 영역에 저장되기 때문에 매번 다시 로드할 필요가 없다.
동적 로딩
- 프로세스가 시작될 때 프로세스의 주소 공간 전체를 한 번에 메모리에 올리는 것이 아니라 필요한 코드와 데이터만 메모리에 적재시키는 방식을 말한다. 이런 방식을 사용하면 메모리를 좀 더 효율적으로 사용할 수 있다.
컴파일된 클래스를 메모리에 로드하는 역할을 한다. 이때 클래스 로더가 지켜야할 4가지의 원칙이 존재한다.
위임 계층 원칙

가시성 원칙
유일성 원칙
FQCN (Fully Qualified Class Name)
= 패키지명 + 클래스명
언로드 금지 원칙
로드된 클래스 파일들을 검증하고 사용할 수 있게 준비하는 과정이다.
검증 (Verification)
준비 (Preparation)
분석 (Resolution)
심볼릭 참조 (symbolic reference)
- 사용자가 작성한 class, field, method의 이름을 지칭한다.
- 심볼릭 참조를 직접 참조로 대체한다는 것은 사용자가 정의한 class, field, method의 이름을 실제 메모리 주소로 대체한다는 것을 의미한다.
- 소스코드가 Byte code로 변경될 때 이미 로드된 자바의 기본 클래스 파일의 이름이 그대로 올라가기 때문에 심볼릭 참조를 이용해서 클래스 이름의 실제 메모리 값을 찾아주는 것이다.
클래스 파일의 코드를 읽고 코드에 명시된 원래 값을 정적 변수에 할당되며 정적 초기화 블럭이 실행된다.

JVM의 메모리 영역으로 프로그램 실행에 사용되는 메모리를 관리하는 영역이다. Runtime Data Area 내부에서도 몇가지 영역으로 나뉘어있다.
JVM이 시작할 때 생성되고 종료할 때 소멸된다. JVM의 모든 스레드가 힙과 메소드 영역을 공유한다.
1. Method Area (= Class Area = Static area)
클래스, 인터페이스, 메소드, 변수, 정적 변수의 Byte code를 저장한다. 클래스에 대한 정보가 이곳에 저장되기 때문에 인스턴스를 생성하려면 Method Area를 참조해야 한다.
해당 영역에 저장되는 데이터의 종류는 아래와 같다.
Type Information : 클래스와 인터페이스 정보
Runtime Constant Pool
Field Information : 클래스 변수의 정보를 저장한다.
Method Infeomation : 메소드의 정보를 저장한다.
Class Variable : 정적 변수의 정보를 저장한다.
2. Heap
new 키워드를 통해 생성되는 객체와 배열이 저장된다. 메소드 영역에 로드된 클래스만 객체로 생성이 가능하다. 또한 힙의 데이터는 스택에 참조된다. 만약 힙 영역이 가득 차게 되면 OutOfMemoryError가 발생한다.
Garbage Collection의 대상이 되는 영역이며 참조되지 않은 메모리는 GC에 의해 제거된다.
힙 또한 여러개의 영역으로 구분되어 있다.

Young(New) Generation
Miner GC
- Young 영역에서 일어나는 GC를 호칭하는 단어다.
- Survivor 공간을 검사하는 역할도 맡고 있다.
Tenured(Old) Generation
Major GC
- Old 영역에서 일어나는 GC를 호칭하는 단어다.
- Major GC가 실행되면 GC를 제외한 모든 스레드가 정지하는데 이 이벤트를 "Stop the world"라고 부른다. 때문에 Miner GC보다 속도가 느리며 Major GC가 실행되는 횟수를 최소화해야 한다.
스레드의 생성/소멸과 함께 하며 스레드마다 하나씩 주어진다.
1. JVM Stack
지역 변수, 메서드의 매개변수, 반환값 등 임시적으로 사용되는 값을 저장하는 영역이다.
프로그램이 실행 후 메서드가 실행되면 JVM은 Stack에 해당 메소드의 프레임을 추가(push)하고 임시적으로 데이터를 저장한다. 메소드가 종료되면 해당 메소드의 프레임을 삭제(pop)한다.

Local Variables Array : 메소드 내의 지역변수를 담고 있는 배열
Operand Stack
Constant Pool Reference : 메소드 영역의 Runtime Constant Pool에 대한 참조
2. Native Method Stack
프로그램을 실행하면서 순수하게 자바로 구성된 코드만을 사용할 수 없는 시스템의 자원이나 API가 존재한다. 자바가 아닌 다른 언어로 작성된 메소드를 Native Method라고 하며 그런 메소드를 관리하는 영역이 Native Stack이다. C Stack이라고도 불린다.
3. PC Register
멀티스레드 프로그래밍 환경에서 한 스레드가 작업을 하다가 다른 스레드에게 CPU 자원을 넘겨주고 다시 받았을 때, 이어서 작업하기 위해서는 실행하던 명령어의 주소값을 저장할 공간이 필요하다. 그 공간이 바로 PC Resister다.
실행했던 메소드가 Native Method라면 undefined가 기록되고 아니라면 사용된 명령어의 주소값이 저장된다.

실행 엔진(Execution Engine)은 Runtime Data Area에 할당된 Byte code를 실행한다. 하지만 Byte code는 기계 바로 수행할 수 없는 형태이기 때문에 상황에 따라 두 가지 방법을 사용하면서 Byte code를 기계에 익숙한 형태(= 네이티브 코드)로 변경해야 한다.
Byte code 명령어를 하나씩 읽어서 해석하고 실행한다. 하나하나의 해석은 빠르나 전체 실행은 느리다. 기본적으로 인터프리터 방식으로 Byte code가 실행된다.
인터프리터의 단점을 보안하기 위해 도입되었다. 인터프리터 방식으로 실행하다가 해당 코드가 자주 실행되면 Byte code를 전체를 컴파일 해서 네이티브 코드로 변경하고 이후에는 해당 메소드를 네이티브 코드로 실행하는 방식이다. 네이티브 코드는 캐시에 저장되기 때문에 한 번 컴파일된 코드는 계속 더 빨리 실행할 수 있다.
그러나 JIT 컴파일러가 컴파일 하는 과정은 인터프리터가 하나씩 해석하는 것보다 훨씬 오래 걸리기 때문에 한 번만 실행되는 코드라면 인터프리터를 사용하는 게 유리하다.
JIT의 동작 과정
1. 우선 Byte code를 중간 단계의 표현인 IR(Intermediate Representation)로 변환한다.
2. 중간 코드를 최적화 한다.
3. 네이티브 코드를 생성한다.
더 이상 참조되지 않는 객체를 찾아서 제거한다.
Class A {
// static 변수, 메소드
// member 변수, 메소드
public static void main(...) {
A a = new A();
}
}
Java 컴파일러에 의해 소스코드가 Byte code로 변환된다.
변환된 Byte code는 JVM의 클래스 로더에 의해 로드된다.
Class A의 클래스 파일을 읽고 원래 코드에 명시된 실제 값을 static 변수에 할당하고 main 메소드를 포함한 static 메소드의 초기화 블럭을 실행한다.
실행엔진이 main 메소드를 시작한다. 처음 실행하는 거라면 인터프리터 방식으로 Byte code를 해석해서 기계어로 변경할 것이다.
main 메소드가 JVM Stack에 추가되고 new 키워드로 인해 A 객체가 생성되어 Heap 영역에 저장된다.
main 메소드가 끝나면 JVM Stack에서 제거된다.
class A {
// static 변수들, 메소드들...
private static final int i = 1; // 원시(primitive) 타입
// 변수명 i와 실제 값 1은 메서드 영역에 저장이 된다.
private static final Integer ii = Integer.parseInt(2); // 참조(Reference) 타입
// 변수명 ii는 메소드 영역에 저장되고 실제 값은 Runtime COnstant Pool에 저장된다.
private static A a = new A();
// a는 메소드 영역에 저장되고 Class A의 객체는 Heap 영역에 저장이 된다.
// 그리고 a에는 객체 A가 저장된 메모리의 주소값이 저장된다.
// Runtime Constant Pool
// -> 상수의 값
// https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/lang/Integer.java
// https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/lang/Long.java
// member 변수들, 메소드들...
private final A aa = new A();
private final String str = "STRING";
private volatile B b;
public static void main(...) {
// 아래의 코드를 실행했을 때,
// JVM의 메모리에는 어떤 변화가 생기나요?
A a = new A();
// Heap에 A객체가 저장됨
// aa, str, b
// Class A의 생성자가 실행이 됨
a.print(...);
}
public void print(...) {
System.out.println(i + ", " + ii + ", " + str);
}
}