JVM을 이해하면 자바뿐만 아니라 JVM 위에서 동작하는 언어를 쉽게 이해할 수 있고, 내부에서 어떻게 동작을 해서 코드가 실행되는지 알면 최적화나 리팩토링을 하는데 도움이되기 때문에 JVM에 대해서 정리한다.
JVM은 자바 바이트코드를 실행할 수 있는 주체다.
인터프리터나 JIT 컴파일 방식으로 다른 컴퓨터 위에서 바이트코드를 실행할 수 있도록 구현된다. CPU나 운영 체제의 종류와 무관하게 동일하게 동작할 것을 보장한다.
-위키백과-
JVM은 자바 애플리케이션의 바이트코드를 클래스 로더(Class Loader)를 통해 메모리에 로딩하고, 로딩된 클래스 정보를 런타임 데이터 영역(Runtime Data Area)에 배치한 뒤 실행 엔진(Execution Engine)이 이를 해석하거나 컴파일하여 실행한다.
아래는 자바 소스 코드가 컴파일되어 생성된 바이트코드가 JVM에서 로딩되고 실행되는 과정을 간단히 도식이다.

아래는 JVM과정을 상세화해서 나타낸 도식이다.
이제부터 각 구성요소를 자세히 알아보자
Class Loader는 실행 시점에 .class 파일을 JVM 내로 동적으로 로드하고, 링크를 통해 런타임 데이터 영역 안의 Method Area에 배치하는 작업을 하는 모듈이다.
클래스 로더의 로딩 순서는 다음과 같이 3단계로 구성된다. (Loading -> Linking -> Initialization)

여기서 Linking은 3가지로 더 세분화 되는데 일단 Loading에 대해서 알아보자
Loading은 .class파일을 JVM 메모리로 가져오는 단계다.
이 단계에서 하는 일
클래스 로더가 .class 파일을 찾고 .class 파일의 바이트코드를 읽는다. 그리고 읽은 내용을 JVM의 Method Area에 저장하고 해당 클래스타입의 Class 객체를 생성해서 Heap 영역에 저장한다.
Linking는 클래스 파일을 사용하기 위해 검증하는 과정이고, 검증, 준비, 분석 이렇게 3가지의 세부 단계로 나뉜다.
verify (검증) - 로드된 클래스의 바이트코드가 JVM의 명세에 맞게 올바르게 되어 있는지 검증하는 과정이다.
perpare (준비) - 클래스나 인터페이스에 필요한 메모리를 할당하는 과정이다.
resolve (분석/해결) - 심볼릭 메모리 레퍼런스를 메소드 영역에 있는 실제 레퍼런스로 교체하는 과정이다.
Initialization는 링크 단계에서 확보한 메모리 영역에 클래스의 static 변수를 명시된 값으로 할당한다.

클래스 로더의 종류는 부트스트랩, 확장, 시스템(애플리케이션) 클래스 로더 크게 이렇게 3가지로 나눠져있다.
Bootstrap Class Loader는 최상위 클래스 로더다.
JVM이 실행될 때 가장 먼저 실행되고, 다른 클래스 로더와 다르게 자바가 아닌 네이티브 코드(C/C++)로 구현되어 있다.
로드 대상: 자바의 핵심 라이브러리 (java.lang.*, java.util.*, java.io.*등)
다른 모든 클래스 로더는 Bootstrap Class Loader가 로드한 클래스들을 기반으로 동작한다.
Extension Class Loader는 부트스트랩 클래스 로더의 자식이고, 표준 코어 자바 클래스 외의 확장 클래스를 로드한다.
Java 8 때는 URLClassLoader를 상속하고, jre/lib/ext 내 모든 클래스를 로드했다.
하지만 ext 디렉토리는 관리도 안 되고 보안도 취약 -> 모듈 개념과 맞지 않았다.
그래서 Java 9 이후부터는 Platform Loader로 변경되었고, BuiltinClassLoader를 상속한다.
로드 대상: Java SE 표준 모듈 (java.sql, java.xml, java.management 등)
Application Class Loader는 자바 프로그램 실행 시 지정한 Classpath에 있는 클래스 파일 or jar에 속한 클래스들을 로드한다.
로드 대상: 애플리케이션 코드 (내가 작성한 모든 클래스), 외부라이브러리 (Gradle / Maven 의존성)
JVM의 클래스 로더는 새로운 클래스를 로드해야할 때, 다음과 같이 동작한다.
Application Class Loader
↓
Platform Class Loader
↓
Bootstrap Class Loader
Bootstrap Class Loader는 자신의 클래스패스(JDK core lib)에 해당 클래스가 있는지 확인한다.
있으면 직접 로드하고, 없으면 Platform Class Loader에 요청을 넘긴다.
Platform Class Loader가 자신의 영역에서 탐색한다. 있으면 로드하고, 없으면 Application Class Loader에 요청을 넘긴다.
Application Class Loader가 classpath에서 탐색한다. 있으면 로드하고, 없으면 ClassNotFoundException이 발생한다.
클래스 로더의 종류는 위임, 가시성, 유일성 원칙 이렇게 3가지로 나눠져있다.
클래스 로더는 클래스 로딩 요청을 받으면, 자신이 직접 로딩하지 않고 먼저 부모 클래스 로더에게 위임한다.
ex) Application ClassLoader가 요청 받음 → Extension ClassLoader에게 위임
Extension ClassLoader가 로딩 시도 → 성공하면 반환, 실패하면 자식에게 다시 넘김
하위 클래스 로더는 상위 클래스 로더가 로딩한 클래스를 볼 수 있지만, 상위 클래스 로더는 하위 클래스 로더가 로딩한 클래스를 볼 수 없다.
ex)
// java.lang.String (Bootstrap이 로딩)
public class String {
// 이 클래스는 java.util.ArrayList를 사용할 수 있음
// why? - ArrayList도 Bootstrap이 로딩하기 때문에
}
// com.myapp.MyClass (Application이 로딩)
public class MyClass {
// 이 클래스는 String을 사용할 수 있음
// why? - String은 상위 클래스로더가 로딩했기 때문에
private String name;
}
JVM 내에서 하나의 클래스는 특정 클래스 로더에 의해 한 번만 로드될 수 있다.
(유일성 보장)
동적 클래스 로딩이란, JVM이 프로그램 시작 시 모든 클래스를 미리 로드하지 않고, 클래스가 실제로 필요해지는 시점에 로드하는 방식이다.
동적 클래스 로딩은 로드타임 동적 로딩, 런타임 동적 로딩 이렇게 2가지 방식이 있다.
먼저 로드타임 동적 로딩부터 알아보자
public class HelloWorld {
public static void main(String[] args) {
System.out.println("안녕하세요!");
}
}
위 코드의 경우 다음과 같이 동작한다.
JVM이 실행되고, 클래스 로딩이 시작되면 각 클래스가 로드되는 과정에서 필요한 경우 최상위 클래스인 java.lang.Object가 로드된다.
클래스 로더는 명령 행에서 지정한 HelloWorld 클래스를 로드하기 위해
HelloWorld.class 파일을 읽는다.
이후 main 메서드가 실행되는 과정에서 필요한 클래스인 java.lang.String과 java.lang.System이 필요한 시점에 추가로 로드된다.
이처럼 프로그램 실행 중, 코드의 실행 흐름에 따라 필요한 클래스를 그 시점에 로드하는 방식을 로드타임 동적 로딩이라고 한다.
public class RuntimeLoading {
public static void main(String[] args) {
try {
Class cls = Class.forName(args[0]);
Object obj = cls.newInstance();
Runnable r = (Runnable) obj;
r.run();
} catch (Exception e) {
e.printStackTrace();
}
}
}
위 코드에서 Class.forName(className)은 파라미터로 전달된 className에 해당하는 클래스를 런타임 시점에 로드하고 초기화한 뒤, 해당 클래스를 대표하는 Class 객체를 반환한다.
Class.forName() 메서드가 실행되기 전까지는 RuntimeLoading 클래스가 어떤 클래스를 동적으로 로드할지 컴파일 시점에는 알 수 없다.
RuntimeLoading 클래스가 로드될 때는 args[0]에 해당하는 클래스는 아직 로드되지 않으며, main() 메서드가 실행되는 도중 Class.forName(args[0])가 호출되는 순간에 해당 클래스가 로드·링크·초기화된다.
이처럼 컴파일 시점이 아니라 실행 시점에 로드 대상 클래스가 결정되고 로드되는 방식을 런타임 동적 로딩이라고 한다.
장점
단점
실행 엔진은 클래스 로더를 통해 런타임 데이터 영역에 배치된 바이트코드를 해석하거나 컴파일하여 실행하는 모듈이다.
.class는 JVM이 이해할 수 있는 중간 레벨로 컴파일된 코드다. 그래서 실행 엔진은 인터프리터와 JIT컴파일러 두 가지 방식을 혼합해서 바이트코드를 실행한다.
인터프리터는 바이트코드를 한 줄씩 읽어 네이티브 코드로 변환하고 즉시 실행하는 방식이다.
네이티브코드 : CPU와 운영체제가 직접 이해하고 실행할 수 있는 기계어 수준의 코드
JVM안에서 바이트코드는 기본적으로 인터프리터 방식으로 동작한다.
컴파일 과정 없이 바로 실행되서 초기 실행 속도는 빠르지만, 반복 실행 시에는 매번 해석 과정을 반복해야해서 속도가 느리다. 그리고 중복되는 바이트코드를 매번 컴파일 하기 때문에 비효율적이다.
JIT 컴파일러는 위의 Interpreter의 단점을 보완하기 위해 도입된 모듈이다.
JVM은 실행 중 메서드 호출 횟수와 루프 반복 횟수를 기반으로 Hot Spot을 탐지하고, Hot Spot을 네이티브 코드로 컴파일해서 이후 실행에서는 인터프리터를 거치지 않고 직접 실행한다.
c1 컴파일러는 JIT 컴파일러가 시작되는 초반에 성능을 끌어올리기 위해 만들어진 컴파일러다. (클라이언트 컴파일러라고도 불린다.)
C1은 인터프리터와 C2 사이의 빈틈을 메우기 위해 존재한다고 보면 된다.
C1이 없으면 JVM은 초반이 너무 느리거나, 초반에 너무 비싸다.
C2 컴파일러는 JIT컴파일러의 전반적인 성능을 최적화하기 위해 만들어진 컴파일러다. (서버 컴파일러라고도 불린다.)
C2컴파일러는 C1컴파일러가 수집한 프로파일링 데이터(호출 빈도, 분기 패턴 등)를 바탕으로 최적화를 수행한다. 인라이닝, 루프 최적화, 탈출 분석 등 고급 최적화 기법을 적용해서 C1보다 훨씬 더 최적화된 네이티브 코드를 생성한다.
컴파일 시간은 더 오래 걸리지만, 생성된 코드는 장기적으로 높은 성능을 제공한다고 보면 좋을것 같다.

코드 캐시는 JVM 내부에 존재하는 네이티브 메모리 영역이다.
이 곳에는 JIT컴파일 코드가 저장된다. (JIT 컴파일된 코드, 인터프리터 관련 코드, JVM 자체의 네이티브코드 등)
코드 캐시의 크기와 동작 방식
JVM 시작 시, 코드 캐시는 설정된 값으로 최대 크기가 고정되서 확장할 수 없다. 코드 캐시가 꽉 차면 그 때부터 더 이상 JIT 컴파일이 안 되고, 컴파일되지 않은 코드는 인터프리터로만 실행된다.
이 과정에서 JVM은 Code Cache Sweeper를 통해 사용되지 않는 네이티브 코드를 제거한다.
Code Cache Sweeper
Code Cache Sweeper는 주기적으로 실행되고, 더 이상 사용하지 않는 코드, 역최적화된 코드, 오래된 컴파일 결과를 제거해서 코드 캐시 공간을 확보한다.
네이티브 코드가 코드캐시에서 제거 되는 상황
역최적화될 때, 다른 컴파일 버전으로 교체됐을 때, (단계별 컴파일), 메서드를 지닌 클래스가 언로딩될 때 네이티브 코드가 코드 캐시에서 제거된다.
역최적화란?
JIT 컴파일러가 "이 코드는 이렇게 실행될 것" 이라고 가정하고 최적화한 결과가 실행 중에 깨졌을 때, JVM이 해당 코드를 다시 덜 최적화된 상태로 되돌리는 과정이다.
Non-method: 이 영역에는 JVM 내부 코드, JIT 컴파일러 자체의 코드, 바이트코드 등 메서드가 아닌 코드가 포함되어 있다.
이러한 코드는 JVM이 실행되는 동안 계속 필요하므로 코드 캐시에서 영구적으로 유지된다. (크기는 3MB로 고정되어 있다.)
Profiled: 이 영역에는 C1 컴파일러에 의해 컴파일된 메서드가 저장된다.
이 메서드들은 기본적인 최적화만 적용되었고, 실행 중에 성능 데이터(호출 빈도, 분기 패턴 등)를 수집한다.
수명이 짧은 이유는 자주 사용되지 않으면 캐시에서 제거되고, 핫스팟으로 판단되면 더 높은 수준의 최적화를 거쳐 Non-profiled 영역으로 이동하기 때문이다.
Non-profiled: 이 영역에는 C2 컴파일러에 의해 완전히 최적화된 메서드가 저장된다.
Profiled 영역에서 수집된 성능 데이터를 바탕으로 인라이닝, 루프 최적화, 탈출 분석 등 최적화가 적용된다. 이 메서드들은 더 이상 프로파일링이 없고, 애플리케이션의 핫스팟으로 판단되서 오래 사용될 것으로 예상되서 수명이 길다.
OSR은 실행 중인 메서드를 인터프리터 모드에서 컴파일 모드로 전환하는 기법이다.
JVM의 기본 컴파일 단위는 전체 메서드이기 때문에, 한 메서드의 바이트코드는 한꺼번에 네이티브 코드로 컴파일된다. But 메서드 자체는 자주 호출되지 않지만 내부에 핫 루프가 있는 경우가 존재한다. 이래서 OSR이 필요!
동작 방식은 먼저 인터프리터가 메서드 실행 중 루프를 발견하고, 루프가 핫스팟으로 판단되면 루프를 컴파일한다. 컴파일이 완료되면 실행 중인 스택 프레임을 그대로 유지하면서 컴파일된 코드로 교체하고, 루프는 컴파일 모드로 계속 실행된다.

JVM은 자바 클래스를 내부적으로 C++의 klass 구조체로 표현한다.
klass 안에는 해당 클래스의 메서드 정보를 담은 vtable(virtual method table) 이 있다.
vtable은 메서드 → 실제 실행 코드 주소(네이티브 코드 메모리주고 or 인터프리터 코드)를 가리키는 테이블이다.
즉, JVM은 메서드를 직접 실행하지 않고 vtable이 가리키는 주소를 따라가서 코드를 실행한다.
처음에는 이 주소가 인터프리터 코드를 가리킨다.
메서드가 자주 실행되어 핫 메서드로 판단되면, JIT 컴파일러가 해당 메서드를 네이티브 코드로 컴파일하고 이를 Code Cache에 저장한다.
컴파일이 끝나면 JVM은 klass 안의 vtable에 들어 있는 메서드 실행 주소를 인터프리터 코드 → 네이티브 코드로 교체한다.
이 주소를 바꾸는 동작을 포인터 스위즐링(pointer swizzling) 이라고 한다.
인라인은 메서드 호출을 코드로 직접 삽입해서 실행 속도를 높이는 최적화 기법이다.
메서드 호출 시 스택에 정보를 저장하고 파라미터를 전달하는 등의 과정에서 오버헤드가 발생한다. 객체 지향 설계를 따르면 작은 메서드가 많아져 호출 횟수가 증가하는데, JVM은 이를 자동으로 인라인 처리하여 최적화한다.
ex)
addNumbers(1,2) 메서드 호출이 있고 해당 메서드가 a + b를 반환한다면, 컴파일 후에는 1 + 2로 직접 계산되어 메서드 호출 자체가 사라진다.
인라인 적용 기준은 메서드의 호출 빈도와 크기다. 자주 호출되는 핫스팟 메서드고, 크기가 작을수록 인라인하기 적합하다. 인라인은 디폴트로 활성화되어 있고, -XX:MaxFreqInlineSize=<n>, -XX:MaxInlineSize=<n> 옵션으로 조정할 수 있다.
탈출 분석은 C2 컴파일러가 객체의 스코프를 분석해서, 객체가 메서드나 스레드를 벗어나지 않으면 스택에 할당하거나 동기화를 제거하는 최적화 기법이다.
GlobalEscape: 객체가 메서드 외부에서도 참조되는 경우 (ex: 필드에 저장), 힙에 할당되어 GC 대상이 된다.
ArgEscape: 객체가 메서드 인자로 전달되지만 호출 종료 후 참조가 유지되지 않는 경우, 스택에 할당된다.
NoEscape: 객체가 메서드 내에서만 사용되는 경우, 스택에 할당된다.
예를 들어 루프 내에서 Factorial 객체를 생성하고 사용하는 경우, 해당 객체는 루프 내에서만 존재하므로 NoEscape 상태다. JVM은 이 경우 두 가지 최적화를 수행한다.
synchronized 메서드라도 다른 스레드가 접근할 수 없으므로 동기화 락 제거JVM은 C1과 C2 컴파일러의 장점을 모두 활용하기 위해 계층적 컴파일이라는 전략을 사용한다.
계층적 컴파일은 메서드가 얼마나 자주 호출되는지에 따라 컴파일 수준을 점진적으로 높여가는 방식이다.
Level0: 인터프리터 모드로 실행 (프로파일링 정보 수집)
Level1: C1 컴파일러로 간단하게 컴파일 (최적화 없음)
Level2: C1 컴파일러로 제한적인 최적화와 함께 컴파일
Level3: C1 컴파일러로 모든 최적화를 적용해서 컴파일
Level4: C2 컴파일러로 최대 수준의 최적화를 적용해서 컴파일
가비지 컬렉터는 더 이상 사용되지 않는 객체를 탐지하고 가비지 컬렉션 과정을 수행하는 JVM 내부의 실행 주체다.
어떤 객체가 계속 사용 중인지 추적한다. (객체가 더 이상 참조되지 않으면 사용되지 않는 것으로 간주한다.)
더 이상 사용되지 않는 객체의 메모리를 해제하여 다른 객체가 사용할 수 있도록 한다.
Mark and Sweep은 GC Root로부터 도달 가능한 객체를 먼저 표시하고, 표시되지 않은 객체를 제거하는 가비지 컬렉션 알고리즘이다.
가비지 컬렉션의 대상은 다음과 같은 기준으로 판별된다.
Reachable (도달 가능): 객체가 하나 이상의 변수에 의해 참조되고 있는 상태다.
-> 가비지 컬렉션 대상 X
Unreachable (도달 불가능): 객체가 더 이상 어떤 변수에도 참조되지 않은 상태다.
-> 가비지 컬렉션 대상 O

Mark 과정:
Sweep 과정:
Compact 과정:
Root Space: method area, static 변수, stack, native method stack

JVM의 힙 영역은 처음 설계될 때 다음 2가지를 전제로 설계되었다.
-> 객체는 대부분 일회성이고 메모리에 오랫동안 남아있는 경우는 드물다
이러한 특성을 이용해 JVM 개발자들은 효율적인 메모리 관리를 위해, 객체의 생존 기간에 따라 물리적인 힙 영역을 나누게 되었고 Young과 Old 이렇게 2가지 영역으로 설계했다.

Young 영역은 새로 생성된 객체가 할당되는 영역이다.
대부분의 객체가 금방 Unreachable상태가 되기 때문에 많은 객체가 Young 영역에 생성됐다가 사라진다.
Young 영역에 대한 가비지 컬렉션을 Minor GC라고 부른다.
Old 영역은 Young 영역에서 Reachable상태를 유지해서 살아남은 객체가 승격되어 저장되는 영역이다.
Young 영역보다 크게 할당되고, 가비지는 적게 발생한다.
Old 영역에 대한 가비지 컬렉션을 Major GC라 하고, 힙 전체를 대상으로 수행되는 GC를 Full GC라고 한다.
Old 영역이 Young 영역보다 크게 할당되는 이유는 Young 영역의 수명이 짧은 객체들은 큰 공간을 필요로 하지 않는다. 그리고 장시간 살아남는 객체가 누적되서 더 큰 공간이 필요하고, 큰 객체들은 Young 영역이 아니라 Old영역에 할당되기 때문이다.
힙 영역은 효율적인 GC를 위해 Young 영역을 3가지 영역(Eden, survivor 0, survivor 1)으로 나눈다.

Eden에는 새로 생성된 객체가 위치한다.
정기적인 쓰레기 수집 후 살아남은 객체들은 Survivor 영역으로 보낸다.
Survivor 0 / Survivor 1은 최소 1번의 GC 이상 살아남은 객체가 존재하는 영역이다.
Survivor 0 또는 Survivor 1 둘 중 하나는 꼭 비어 있어야 하는 규칙이 있다.

처음 생성된 객체는 Eden영역에 위치한다.
그러다 객체가 계속 생성되어 Eden 영역이 꽉차면 Minor GC가 실행된다.
Minor GC가 실행되면 Mark 동작을 통해 Reachable 객체를 탐색한다.
Eden 영역에서 살아남은 객체는 1개의 Survivor 영역으로 이동한다.
그리고 Eden 영역에서 사용되지 않는 객체의 메모리를 해제한다.
살아남은 모든 객체들은 age값이 1씩 증가한다. (age값이 임계값에 다다르면 Promotion(Old 영역으로 이동) 여부를 결정한다.)
다시 Eden 영역에 객체들이 꽉 차면 Minor GC가 발생한다. (반복)
객체의 age가 임계값(여기선 8로 설정)에 도달하게 되면
이 객체들은 Old 영역으로 이동된다. 이를 promotion이라 부른다.
위의 과정이 반복되어 Old 영역의 공간이 부족하게 되면 Major GC가 발생된다.
Old 영역은 Young 영역에 비해 상대적으로 큰 공간을 가지고 있어서 이 공간에서 메모리 상의 객체 제거에 많은 시간이 걸리게 된다. (Young 영역의 10배 이상의 시간 사용)
-> Stop-The-World 문제 발생 (GC가 동작하는 동안 다른 동작을 멈춰서 오버헤드가 발생하는것)
그래서 자바 개발자들이 계속해서 가비지 컬렉션 알고리즘을 발전시켜왔다.
가비지 컬렉션 알고리즘 종류를 알아보자
자바가 발전됨에 따라 힙의 사이즈가 커지면서 애플리케이션 지연 현상이 두드러지게 되었고, 이를 최적화하기 위해 다양한 가비지 컬렉션 알고리즘이 개발되었다.

Serial GC는 서버의 CPU 코어가 1개일 때 사용하기 위해 개발된 가장 단순한 GC다.
GC를 처리하는 쓰레드가 1개여서 가장 Stop-The-World 시간이 길다. (보통 실무에서 안 씀)
Parallel GC는 Java 8의 디폴트 GC다.
Serial GC와 기본적인 알고리즘은 같지만, Young 영역의 Minor GC를 멀티 쓰레드로 수행한다. (Old 영역은 싱글 쓰레드)
Parallel Old GC는 Parallel GC를 개선한 버전이다.
Young 영역 뿐만 아니라, Old 영역에서도 멀티 쓰레드로 GC를 수행한다.
Mark-Summary-Compact 방식 이용
CMS GC는 어플리케이션의 쓰레드와 GC 쓰레드가 동시에 실행되어 Stop-The-World 시간을 최대한 줄이기 위해 고안된 GC다.
GC 과정이 매우 복잡해졌고, 메모리 파편화 문제같은것 때문에 Java 9 버전부터 deprecated 되었고 Java 14에서는 사용이 중지됐다.
G1 GC는 CMS GC를 대체하기 위해 jdk 7버전에서 최초로 release된 GC다.
Java 9+버전의 디폴트 GC로 지정되었다.
기존 GC 알고리즘에서는 힙 영역을 물리적으로 고정된 Young / Old 영역으로 나누어 사용했지만, G1 GC는 이런 개념과 아예 다른 Region이라는 개념을 새로 도입하여 사용한다.
전체 힙 영역을 Region이라는 영역으로 체스같이 분할하여 상황에 따라 Eden, Survivor, Old 등 역할을 고정이 아닌 동적으로 부여한다.
-> 가비지로 가득찬 영역을 빠르게 회수하여 빈 공간을 확보하므로, 결국 GC 빈도가 줄어드는 효과를 얻게 되는 원리.

Shenandoah GC는 Java 12에 release된 GC다.
레드 햇에서 개발했고, 기존 CMS가 가진 단편화, G1이 가진 pause의 이슈를 해결했다.
강력한 Concurrency와 가벼운 GC 로직으로 힙 사이즈에 영향을 받지 않고 일정한 pause 시간이 소요된다.
Z GC는 Java 15에 release된 GC다.
대량의 메모리를 low-latncy로 잘 처리하기 위해 디자인 된 GC다.
G1의 Region처럼, Z GC는 ZPage라는 영역을 사용하고, G1의 Region은 크기가 고정인데 비해 ZPage는 2mb 배수로 동적으로 운영된다.
힙 크기가 증가해도 Stop-The-World 시간이 절대 10ms를 넘지 않는다.


런타임 데이터 영역은 자바 애플리케이션을 실행할 때 사용되는 데이터들이 저장되는 메모리 공간이다.
런타임 데이터 영역은 위에서 본것처럼 크게 다섯 가지 영역으로 나눠진다.
Thread가 공유하는 영역
Thread가 공유하지 않는 영역
각 영역들에 대해서 자세히 알아보자
메서드 영역은 JVM이 클래스를 로드할 때 메모리 공간을 할당받고, 클래스 메타데이터가 저장되는 영역이다.
메서드 영역은 모든 스레드가 공유하는 영역으로, JVM 시작 시 생성되어 JVM 종료 시 소멸된다. 클래스 로더가 로드한 클래스 정보를 저장하며, 공간이 부족하면 OutOfMemoryError가 발생한다.
힙 영역은 동적으로 할당된 객체와 배열을 저장하는 메모리 영역이다.
new로 생성된 객체와 배열이 저장되며, 크기가 동적으로 변하고 모든 스레드가 공유한다. 그리고 가비지 컬렉션(GC)이 동작한다.
힙 영역에는 객체의 실제 인스턴스 데이터가 저장되고, 객체를 가리키는 참조 변수는 스택에 저장된다.
스택 영역은 메서드 호출마다 생성되는 스택 프레임을 저장하는 영역이다.
각 스택 프레임에는 다음 정보가 포함된다.
스택 영역은 LIFO(Last In, First Out) 구조로 동작하기 때문에 메서드 호출과 종료 시 메모리 할당과 해제가 매우 빠르다.
크기가 제한되어 있어서 재귀 호출이 과도할 경우에는 StackOverflowError가 발생할 수 있다.
PC 레지스터는 각 스레드가 실행할 바이트코드 명령의 주소를 저장하는 영역이다.
멀티 스레드 환경에서 스레드 전환이 발생하더라도, 각 스레드는 자신만의 PC 레지스터를 통해 중단되었던 실행 지점부터 다시 실행할 수 있다.
JVM 실행 흐름을 제어하기 위한 기준점 역할을 한다.
네이티브 메서드를 실행 중인 경우에는 JVM 바이트코드를 실행하지 않으므로 PC 레지스터의 값은 정의되지 않는다.
네이티브 메서드 스택은 자바가 아닌 네이티브 언어(C, C++ 등)로 작성된 메서드가 실행될 때 사용하는 메모리 공간이다.
일반적인 자바 메서드는 자바 스택에서 실행되지만, native 메서드가 호출되면 JVM은 실행 제어를 네이티브 코드로 넘기고 이때 네이티브 메서드 스택이 사용된다.
자바 코드와 네이티브 코드의 연결은 주로 JNI(자바 코드가 네이티브 라이브러리와 상호작용할 수 있도록 해주는 인터페이스)를 통해 이루어지고, 네이티브 메서드 실행 중에는 해당 스택에서 지역 변수와 실행 상태가 관리된다.
네이티브 메서드 스택은 JVM 구현과 운영체제에 따라 동작 방식이 달라질 수 있는 플랫폼 종속적인 영역이라는 점이 특징이다.
https://velog.io/@ddangle/Java-%ED%81%B4%EB%9E%98%EC%8A%A4-%EB%A1%9C%EB%8D%94%EB%9E%80
https://velog.io/@mirrorkyh/GC-%EC%A2%85%EB%A5%98%EC%99%80-%ED%8A%B9%EC%A7%95