WORA(Write Once Run Anywhere)
고급 언어로 작성된 프로그래밍 언어를 기계어로 번역하는 것에는 대표적으로 2가지 방법이 있다.
컴파일러(Compiler: 번역기)
- 플랫폼 종속적이다.
- 소스코드를 한 번에 연속적으로 번역한다.
- 실행속도가 빠르다.
인터프리터(Interpreter: 해석기)
- 플랫폼 비종속적이다.
- 한 줄씩 기계어 번역을 수행한다.
- 번역속도는 빠르지만 실행속도가 느리다.
여기서 플랫폼은 OS나 코드가 돌아갈 환경을 말한다.
또한, Compiler와 Interpreter를 혼합하여 사용하는 언어를 Hybrid 언어라고 하고, Java는 여기에 속한다.
이 둘을 모두 사용함으로써 Write Once Run Anywhere를 실현할 수 있었다.

자바 컴파일러(javac)는 C의 gcc나 visual c와 같이 excutable 파일을 만드는 것이 목적이 아닌, 해당 소스코드들을 JVM(Java Virtual Machine) 이 이해할 수 있는 자바 바이트코드(.class) 로 변환하는 것이 목적이다.
플랫폼 종속적인 부분을 JVM을 통해 자바 코드를 실행하면 JVM이라는 가상머신 위에서 실행되어 플랫폼의 영향을 받지 않게 한 것이다. 따라서 Java라는 언어는 플랫폼 비종속적이나 JVM에 종속적인 언어이다. (하지만 OS뿐만 아니라 Architecture에 따라 JDK가 다르기 때문에 JVM 자체는 종속적이라 할 수 있다.)
이처럼 자바 소스코드 → JVM (자바 바이트 코드) → Native Code 변환 순서로 Write Once Run Anywhere를 실현했다.
JDK란
JDK는 Java Development Kit의 약어로, Java 프로그램을 개발하기 위해 필요한 도구 모음이다. 크게 JRE와 자바 개발에 필요한 각종 도구들로 나눌 수 있다.

자바의 버전을 표현할 때에는 보통 JDK 또는 Java SE 버전으로 나타낸다. Java SE는 가장 기본이 되는 표준 에디션 자바 플랫폼으로 자바 언어의 핵심 기능을 제공한다. 이 외에도 여러 개의 에디션이 존재한다.
JRE란
JRE는 Java Runtime Environment의 약어로, JVM과 자바 프로그램을 실행시킬 때 필요한 라이브러리 API를 함께 묵어 배포된 패키지이다. 이외에도 자바 런타임 환경에서 사용하는 프로퍼티 세팅이나 리소스 파일(jar)을 가지고 있다.
이처럼 JDK안에 JRE이 있고, JRE안에 JVM이 있는 구성이다.
JVM이란
JVM이란 Java Virtual Machine의 약어로, 자바 애플리케이션을 어느 CPU나 OS에서도 실행할 수 있게 지원하는 역할을 수행한다. 자바 코드를 컴파일하여 바이트 코드로 변환하여 해당 운영체제가 이해할 수 있는 기계어로 실행한다.

JVM은 Class Loader, Execution Engine, Garbage Collector, Runtime Data Area 4가지로 구분할 수 있다.

자바 어플리케이션 실행과정
- 애플리케이션이 실행되면 JVM이 OS로부터 메모리를 할당받는다. (JVM은 할당받은 메모리를 용도에 따라 영역을 구분하여 관리한다.)
- 자바 컴파일러(javac.exe)가 자바 소스코드(. java)를 읽어 바이트 코드(. class)로 변환한다.
- Class Loader를 통해 바이트 코드를 JVM으로 로딩한다.
- 로딩된 바이트 코드는 Execution Engine(Interpreter)을 통해 해석된다.
- 해석된 바이트 코드는 Runtime Data Areas에 배치되어 실행된다. (실행되는 과정에서 GC와 같은 작업이 수행된다.)
Runtime Date Area라는 영역이 실질적인 메모리를 할당받아 관리하게 되는 영역이라고 생각하면 된다.
Class Loader

.java(내가 작성한 파일) 파일은 Java Compiler를 통해 컴파일이 되어 .class로 Byte 코드가 생성된다. 이 Byte 파일들이 Class Loader를 통해 Loading, Linking, Initialization 이 3단계들을 거쳐, Runtime Data Area에 저장되게 된다.
JVM으로 바이트 코드(.class)를 로드하고, 링크를 통해 배치하는 작업을 수행하는 모듈이다. 로드된 바이트 코드들을 엮어서 JVM의 메모리 영역인 Runtime Data Area에 배치한다.
클래스를 메모리에 올리는 로딩 기능은 한 번에 메모리에 올리지 않고, 애플리케이션에서 필요한 경우(호출된 경우) 동적으로 메모리에 적재하게 된다.
클래스 파일의 로딩은 3단계로, Loading -> Linking -> Initialization 순으로 구성된다.
- 로딩(loading) : 클래스를 파일에서 가져와서 JVM의 메모리에 로드한다.
- 링킹(linking) : 레퍼런스를 연결하는 과정이다.
- 초기화(initialization) : static 한 값들을 초기화한다.
더 자세한 Class Loader의 과정은 다음 블로그의 STEP2.4에 잘 나와있으니 참고하면 좋을 것 같다. Class Loader의 과정
Execution Engine

Runtime Data Areas에 할당된 바이트 코드를 실행 시는 주체 코드를 실행하는 방식은 크게 2가지가 있다.
Interpreter
- 바이트 코드를 해석하여 실행하는 역할을 수행한다.
- 다만 같은 메서드라도 여러 번 호출될 때 매번 새로 수행해야 하는 단점이 있다.
JIT(Just In Time) Compiler
- 같은 메소드라도 여러 번 호출하는 Interpreter의 단점을 해소한다.
- 반복되는 코드를 발견하여 전체 바이트 코드를 컴파일하고 그것을 Native Code로 변경하여 사용한다,
Native 란 자바에서 부모가 되는 C, C++, 어셈블리어를 의미한다.
실행속도가 느리다는 Interpreter의 단점을 보완하기 위해 JIT Compiler를 성능을 높인다.
Garbage Collector
- 더 이상 참조되지 않는 메모리 객체를 모아 제거하는 역할을 수행한다.
- 일반적으로 자동으로 실행되지만, 수동으로 실행하기 위해 System.gc()를 사용할 수도 있다. 하지만 실행이 보장되진 않는다.
우리가 JVM을 공부하는 이유 중 하나가 GC를 알기 위함이다. 이 코드가 어떻게 돌아가야 하며, Java에 특징인 메모리를 자동으로 관리해 준다는 것에서 Performance Tuning을 하게 된다. 이 부분에서 가장 큰 파일을 차지하고 있는 게 Garbage Collection에 대한 설정 값들을 변경해 주는 작업이다.
자세한 건 아래에 정리되어 있다.
Runtime Data Area
애플리케이션이 동작하기 위해 OS에서 할당받은 메모리 공간을 의미한다.
다음과 같이 Method Area, Heap Area, Stack Area, Program counter Register, Native Method Area 5가지로 구성되어 있다.

메모리 영역은 크게 두 부분으로 나눠 볼 수 있다. 모든 스레드가 하나를 공유하는 영역인 Method Area, Heap Area와 스레드 별로 하나씩 생성되는 영역인 Stack Area, PC Register, Native Method Area로 나눠진다.
Method Area
static으로 선언된 변수들을 포함하여 class 레벨의 모든 데이터가 이곳에 저장된다. JVM마다 단 하나의 Method Area가 존재한다.
Method Area에는 Runtime Constant Pool이라는 별도의 영역이 존재하며, 이는 상수 자료형을 저장하여 참조하는 역할을 한다.
저장되는 정보의 종류
- Filed Info : 멤버 변수의 이름, 데이터 타입, 접근 제어자의 정보
- Method Info : 메서드 이름, Return 타입, 매개변수, 접근 제어자의 정보
- Type Info : Class인지 Iterface인지 여부, Type 속성, 이름, Super Class의 이름
Heap과 마찬가지로 GC 관리 대상이다.
Heap Area

Eden, Survivor0, Survivor1, Old Generation으로 분리되어 있으며, Eden, Survivor1,2는 Young Generation이라고 한다.
Java 7까지는 Permanent라는 영역이 따로 존재했지만, 이후엔 Native Method Stack에 편입되었다.
Heap Area는 객체를 저장하기 위한 메모리 영역이다. new 연산자로 생성된 모든 Object와 Instance 변수와 배열을 저장한다.
Heap 영역은 물리적으로 두 영역을 구분할 수 있다. (Young / Old Generation)
- Young Generation : 생명 주기가 짧은 객체를 GC 대상으로 하는 영역
- Old Generation : 생명 주기가 긴 객체를 GC 대상으로 하는 영역
Garbage Collection 생명주기에 의해 지속적으로 메모리가 정리된다. 이때 Minor GC, Major GC로 구분할 수 있다.
Young Generation
- 우선 메모리에 객체가 생성되면, Eden 영역에 객체가 지정된다.
- Eden 영역에 데이터가 가득 차게 되면, Eden 영역에 있던 객체가 Survivor0, Survivor1로 옮겨진다. 이때 0,1은 우선순위가 따로 있는 것은 아니다.
- Eden 영역에서 Survivor0, Survivor1로 옮겨지는 객체들은 어딘가에서 참조되고 있는 객체들이다. 둘 중 하나의 영역이 가득 차게 되면 공간이 남아있는 Survivor로 이동하게 된다.
이러한 메커니즘 때문에 Survivor1 또는 Survivor2 둘 중 하나는 항상 비워있는 공간이 있는 채로 유지된다.
이 과정에서 1차 GC라고 불리는 Minor GC가 발생하며, Minor GC는 New/Young 영역에서 발생하는 GC로 Eden 영역, Survivor0, Survivor1에서 사용되지 않는 객체들을 삭제한다.
Old Generation
- Survivor0, Survivor1을 왔다 갔다 하는 과정에서 오랜 기간 살아남은 객체들을 Old 영역으로 이동한다. 보통 Old 영역은 Young 영역보다 크게 할당하며, 이러한 이유로 Old 영역의 GC는 Young 영역보다 적게 발생한다.
- 단, Young 영역에서 Old 영역으로 넘어가는 객체 중, Survivor 영역을 거치지 않고 Eden 영역에서 바로 Old 영역으로 넘어가는 객체도 존재하는데, 이는 객체의 크기가 아주 클 경우 발생한다. (예를 들어 Survivor영역의 크기에 비해 객체의 크기가 더 클 경우에 해당한다.)
오래된 기준은 Young Generation 영역에서 Minor GC 가 발생하는 동안 얼마나 오래 살아남았는지로 판단한다.
각 객체엔 Minor GC에서 살아남은 횟수를 기록하는 age bit가 있으며, MaxTenuringThreshold라는 설정 값을 초과하면 Old Generation 영역으로 객체가 이동된다고 한다. 물론, 초과하지 않아도 Survivor 영역의 메모리가 부족할 경우에 미리 Old Generation 영역으로 객체가 이동될 수 있다.
또한, Old 영역에서는 2차 GC인 Major GC(FULL GC)가 일어나게 된다.
번외) Permanent
Java8 이전에 Class Meta Data, Method Meta Data, Static Object 변수, 상수, JVM, JIT 관련 데이터 등을 저장하던 Permanent 영역을 Java8에서부터는 Metaspace영역으로 대체했다. Metaspace영역은 Native 메모리 영역으로 JVM이 아닌 OS에 의해 관리되도록 변경되었다. 즉, static 객체는 heap이 아닌 별도의 Native 메모리에서 관리된다.
Stack Area

각 스레드를 위한 분리된 Runtime Stack 영역이다. 메서드를 호출할 때마다 Stack Frame으로 불리는 Entry가 Stack Area에 생성된다. 스레드의 역할이 종료되면 바로 소멸되는 특성의 데이터를 저장한다. 각 종 형태의 변수나 임시 데이터, 스레드 또는 메서드의 정보를 저장한다.
PC Register

PC는 Program Counter의 약어로, 각 스레드가 시작될 때 생성되며, 현재 실행 중인 상태 정보를 저장하는 영역이다.
스레드가 로직을 처리하면서 지속적으로 갱신되며, 스레드가 생성될 때마다 하나씩 존재한다. 어떤 명령을 실행해야 할지에 대한 기록이 있다. (현재 수행 중인 부분의 주소를 가진다.)
Native Method Area

바이트 코드가 아닌 실제 실행할 수 있는 기계어로 작성된 프로그램을 실행시키는 영역이며, Java가 아닌 다른 언어로 작성된 코드를 위한 영역이다. Java Native Interface를 통해 바이트 코드로 전환하여 저장한다. 이 영역 또한 각 스레드 별로 생성된다.
JNI(Java Native Interface)란?

자바가 다른 언어로 만들어진 애플리케이션과 상호 작용할 수 있는 인터페이스를 제공한다. JVM이 Native Method를 적재하고 수행할 수 있도록 한다. 실질적으로 제대로 동작(대응)하는 언어는 C, C++ 이 있다. (물론 다른 언어도 가능하지만 완전히 호환되는 것은 저 두 가지이다.)
Garbage Collector
앞으로 사용되지 않는 객체의 메모리를 Garbage라고 부른다. 이런 Garbage를 정해진 스케줄에 의해 정리해 주는 것이 GC(Garbage Collection)이다. Heap 영역에서 동적으로 할당했던 메모리 중 필요 없게 된 객체를 모아 주기적으로 제거해 주는 프로세스이다.
다른 언어는 프로그래머가 수동으로 메모리 할당과 해제를 하나하나 해줬어야 했다. 하지만 Java에서는 GC가 메모리 관리를 해주기 때문에 한정된 메모리를 효율적으로 사용할 수 있다. 따라서 메모리 관리, 메모리 누수 문제에 대해 직접 관리하는 부분이 적어진다.
Stop The World
GC를 수행하기 위해 JVM이 멈추는 현상을 의미한다. GC가 작동하는 동안 GC관련 Thread를 제외한 모든 Thread는 멈춘다. 또한, 어떠한 GC알고리즘을 사용하더라도 Stop The World 상태를 피할 수는 없다. 일반적으로 '튜닝'이라는 것은 이 멈추는 시간을 최소화하는 것을 말한다. 따라서 튜닝을 제대로 이해하기 위해서는 GC가 어떤 방식으로 동작하고, 어떠한 경우에 발생하는지 알아서 각 애플리케이션의 특징에 맞는 설정 값을 넣어야 한다.
가비지 컬렉션 작동 방식
위 Heap에서 GC가 어떻게 Reachable과 Unreachable을 판단하는지 말했었다.
Mark And Sweep(Mark-Sweep)이란 다양한 GC에서 사용되는 객체를 솎아내는 내부의 알고리즘이다. GC가 동작하는 기초적인 청소과정이다. 가비지 컬렉션이 될 객체를 Mark(식별)하고 Sweep(제거)하며 객체가 제거되어 파현화된 메모리 영역을 앞에서부터 채워나가는 Compaction을 수행한다.
- Mark : Root Space로부터 그래프 순회를 통해 연결된 객체를 찾아 어떤 객체를 참조하고 있는지 찾아 Mark 한다.
- Sweep : 참조하지 않은(Unreachable) 객체들을 Heap에서 Sweep 한다.
- Compact : Sweep 후 분산된 객체들을 Heap의 시작주소로 모아 메모리가 할당된 부분과 그렇지 않은 부분으로 Compact 한다.
현재 Java17, 21 기준 GC 종류
java -XX:+PrintFlagsFinal -version | grep GC | grep Use
해당 명령어로 전체 Flag에서 GC목록을 확인할 수 있다.
- SerialGC
- ParallelGC
- ShenandoahGC
- G1GC
- ZGC
Java17, 21 기준으로 Defalut GC는 G1 GC이다.
Serial GC
- 서버의 CPU 코어가 1개일 때 사용하기 위해 개발된 가장 단순한 GC이다.
- GC를 처리하는 스레드가 싱글 스레드여서 Stop The World 시간이 길다.
- Minor GC는 Mark-Sweep을 사용하고, Major GC는 Mark-Sweep-Compact를 사용한다.
- 거의 안 쓴다. 디바이스 성능이 좋지 않아 CPU 코어가 1개인 경우에만 사용한다.
Parallel GC
- Java 8의 Default GC이다.
- Serial GC와 기본적 알고리즘은 동일하나, Minor GC를 멀티 스레드로 수행한다.
- Serial GC보다는 Stop The World 시간이 짧다.
CMS GC
- 애플리케이션 스레드와 GC 스레드가 동시에 실행되어 Stop The World 시간을 줄이기 위해 고안됐다.
- 하지만 GC 과정이 복잡하고, GC대비 CPU 사용량이 높다.
- Java 14부터 사용이 중지되었다.
G1 GC
- Java 9+ 의 Default GC이다.
- 기존 GC 알고리즘에선 Heap 영역을 물리적으로 고정된 Young/Old 영역으로 나누어 사용했지만, G1 GC는 이런 개념을 뒤엎는 Region이라는 영역으로 Heap 영역을 분할하여 상황에 따라 Eden, Survivor, Old 등 역할을 동적으로 부여했다.
- 일일이 메모리를 탐색하지 않고, GC(메모리)가 가득 찬 영역(Region)을 빠르게 회수하여 빈 공간을 확보해 GC 빈도가 줄어드는 효과가 있다.
- 기존에 Eden → Survivor0 → Survivor1으로 순차적 이동과 달리 G1 GC에서는 순차적으로 이동하지는 않는다. 대신 G1 GC는 효율적이라고 생각하는 위치로 객체를 Reallocate(재할당) 시킨다. (예를 들어 Survivor1 영역에 있는 객체가 Eden 영역으로 할당하는 것이 더 효율적이라고 판단될 경우 Eden 영역으로 이동시킨다.)
- 4GB 이상의 Heap 메모리, Stop The World가 0.5초 필요한 상황에서 사용한다. (Heap이 작을 경우 사용을 권장하지 않는다.)
ShenandoahGC
- 기존 CMS가 가진 단편화를 가지고 있으며, G1이 가진 pause 이슈를 해결했다.
- 강력한 Concurrency(동시성)와 가벼운 로직으로 Heap 사이즈에 영향을 받지 않는다.
- 일정한 pause 시간이 소요된다.
Z GC
- 대량의 메모리(8MB ~ 16TB)를 Low-latency(Input과 Output사이 과정의 지연을 최소화)로 잘 처리하기 위해 디자인된 GC G1의 Region처럼, ZGC는 ZPage라는 영역을 사용한다.
- G1의 Region은 크기가 고정인데 비해, ZPage는 2mb 배수로 동적으로 운영된다.(큰 객체가 들어오면 2^ 로 영역을 구성해서 처리한다.)
- 최대 장점 중 하나는 Heap 크기가 증가하더도 Stop The World의 시간이 절대 10ms를 넘지 않는다는 것이다.
자세한 동작원리는 다음 블로그를 참고하면 좋을 것 같다.
https://d2.naver.com/helloworld/0128759
참고 문헌
https://1-7171771.tistory.com/140
WORA(Write Once Run Anywhere)
고급 언어로 작성된 프로그래밍 언어를 기계어로 번역하는 것에는 대표적으로 2가지 방법이 있다.
컴파일러(Compiler: 번역기)
- 플랫폼 종속적이다.
- 소스코드를 한 번에 연속적으로 번역한다.
- 실행속도가 빠르다.
인터프리터(Interpreter: 해석기)
- 플랫폼 비종속적이다.
- 한 줄씩 기계어 번역을 수행한다.
- 번역속도는 빠르지만 실행속도가 느리다.
여기서 플랫폼은 OS나 코드가 돌아갈 환경을 말한다.
또한, Compiler와 Interpreter를 혼합하여 사용하는 언어를 Hybrid 언어라고 하고, Java는 여기에 속한다.
이 둘을 모두 사용함으로써 Write Once Run Anywhere를 실현할 수 있었다.

자바 컴파일러(javac)는 C의 gcc나 visual c와 같이 excutable 파일을 만드는 것이 목적이 아닌, 해당 소스코드들을 JVM(Java Virtual Machine) 이 이해할 수 있는 자바 바이트코드(.class) 로 변환하는 것이 목적이다.
플랫폼 종속적인 부분을 JVM을 통해 자바 코드를 실행하면 JVM이라는 가상머신 위에서 실행되어 플랫폼의 영향을 받지 않게 한 것이다. 따라서 Java라는 언어는 플랫폼 비종속적이나 JVM에 종속적인 언어이다. (하지만 OS뿐만 아니라 Architecture에 따라 JDK가 다르기 때문에 JVM 자체는 종속적이라 할 수 있다.)
이처럼 자바 소스코드 → JVM (자바 바이트 코드) → Native Code 변환 순서로 Write Once Run Anywhere를 실현했다.
JDK란
JDK는 Java Development Kit의 약어로, Java 프로그램을 개발하기 위해 필요한 도구 모음이다. 크게 JRE와 자바 개발에 필요한 각종 도구들로 나눌 수 있다.

자바의 버전을 표현할 때에는 보통 JDK 또는 Java SE 버전으로 나타낸다. Java SE는 가장 기본이 되는 표준 에디션 자바 플랫폼으로 자바 언어의 핵심 기능을 제공한다. 이 외에도 여러 개의 에디션이 존재한다.
JRE란
JRE는 Java Runtime Environment의 약어로, JVM과 자바 프로그램을 실행시킬 때 필요한 라이브러리 API를 함께 묵어 배포된 패키지이다. 이외에도 자바 런타임 환경에서 사용하는 프로퍼티 세팅이나 리소스 파일(jar)을 가지고 있다.
이처럼 JDK안에 JRE이 있고, JRE안에 JVM이 있는 구성이다.
JVM이란
JVM이란 Java Virtual Machine의 약어로, 자바 애플리케이션을 어느 CPU나 OS에서도 실행할 수 있게 지원하는 역할을 수행한다. 자바 코드를 컴파일하여 바이트 코드로 변환하여 해당 운영체제가 이해할 수 있는 기계어로 실행한다.

JVM은 Class Loader, Execution Engine, Garbage Collector, Runtime Data Area 4가지로 구분할 수 있다.

자바 어플리케이션 실행과정
- 애플리케이션이 실행되면 JVM이 OS로부터 메모리를 할당받는다. (JVM은 할당받은 메모리를 용도에 따라 영역을 구분하여 관리한다.)
- 자바 컴파일러(javac.exe)가 자바 소스코드(. java)를 읽어 바이트 코드(. class)로 변환한다.
- Class Loader를 통해 바이트 코드를 JVM으로 로딩한다.
- 로딩된 바이트 코드는 Execution Engine(Interpreter)을 통해 해석된다.
- 해석된 바이트 코드는 Runtime Data Areas에 배치되어 실행된다. (실행되는 과정에서 GC와 같은 작업이 수행된다.)
Runtime Date Area라는 영역이 실질적인 메모리를 할당받아 관리하게 되는 영역이라고 생각하면 된다.
Class Loader

.java(내가 작성한 파일) 파일은 Java Compiler를 통해 컴파일이 되어 .class로 Byte 코드가 생성된다. 이 Byte 파일들이 Class Loader를 통해 Loading, Linking, Initialization 이 3단계들을 거쳐, Runtime Data Area에 저장되게 된다.
JVM으로 바이트 코드(.class)를 로드하고, 링크를 통해 배치하는 작업을 수행하는 모듈이다. 로드된 바이트 코드들을 엮어서 JVM의 메모리 영역인 Runtime Data Area에 배치한다.
클래스를 메모리에 올리는 로딩 기능은 한 번에 메모리에 올리지 않고, 애플리케이션에서 필요한 경우(호출된 경우) 동적으로 메모리에 적재하게 된다.
클래스 파일의 로딩은 3단계로, Loading -> Linking -> Initialization 순으로 구성된다.
- 로딩(loading) : 클래스를 파일에서 가져와서 JVM의 메모리에 로드한다.
- 링킹(linking) : 레퍼런스를 연결하는 과정이다.
- 초기화(initialization) : static 한 값들을 초기화한다.
더 자세한 Class Loader의 과정은 다음 블로그의 STEP2.4에 잘 나와있으니 참고하면 좋을 것 같다. Class Loader의 과정
Execution Engine

Runtime Data Areas에 할당된 바이트 코드를 실행 시는 주체 코드를 실행하는 방식은 크게 2가지가 있다.
Interpreter
- 바이트 코드를 해석하여 실행하는 역할을 수행한다.
- 다만 같은 메서드라도 여러 번 호출될 때 매번 새로 수행해야 하는 단점이 있다.
JIT(Just In Time) Compiler
- 같은 메소드라도 여러 번 호출하는 Interpreter의 단점을 해소한다.
- 반복되는 코드를 발견하여 전체 바이트 코드를 컴파일하고 그것을 Native Code로 변경하여 사용한다,
Native 란 자바에서 부모가 되는 C, C++, 어셈블리어를 의미한다.
실행속도가 느리다는 Interpreter의 단점을 보완하기 위해 JIT Compiler를 성능을 높인다.
Garbage Collector
- 더 이상 참조되지 않는 메모리 객체를 모아 제거하는 역할을 수행한다.
- 일반적으로 자동으로 실행되지만, 수동으로 실행하기 위해 System.gc()를 사용할 수도 있다. 하지만 실행이 보장되진 않는다.
우리가 JVM을 공부하는 이유 중 하나가 GC를 알기 위함이다. 이 코드가 어떻게 돌아가야 하며, Java에 특징인 메모리를 자동으로 관리해 준다는 것에서 Performance Tuning을 하게 된다. 이 부분에서 가장 큰 파일을 차지하고 있는 게 Garbage Collection에 대한 설정 값들을 변경해 주는 작업이다.
자세한 건 아래에 정리되어 있다.
Runtime Data Area
애플리케이션이 동작하기 위해 OS에서 할당받은 메모리 공간을 의미한다.
다음과 같이 Method Area, Heap Area, Stack Area, Program counter Register, Native Method Area 5가지로 구성되어 있다.

메모리 영역은 크게 두 부분으로 나눠 볼 수 있다. 모든 스레드가 하나를 공유하는 영역인 Method Area, Heap Area와 스레드 별로 하나씩 생성되는 영역인 Stack Area, PC Register, Native Method Area로 나눠진다.
Method Area
static으로 선언된 변수들을 포함하여 class 레벨의 모든 데이터가 이곳에 저장된다. JVM마다 단 하나의 Method Area가 존재한다.
Method Area에는 Runtime Constant Pool이라는 별도의 영역이 존재하며, 이는 상수 자료형을 저장하여 참조하는 역할을 한다.
저장되는 정보의 종류
- Filed Info : 멤버 변수의 이름, 데이터 타입, 접근 제어자의 정보
- Method Info : 메서드 이름, Return 타입, 매개변수, 접근 제어자의 정보
- Type Info : Class인지 Iterface인지 여부, Type 속성, 이름, Super Class의 이름
Heap과 마찬가지로 GC 관리 대상이다.
Heap Area

Eden, Survivor0, Survivor1, Old Generation으로 분리되어 있으며, Eden, Survivor1,2는 Young Generation이라고 한다.
Java 7까지는 Permanent라는 영역이 따로 존재했지만, 이후엔 Native Method Stack에 편입되었다.
Heap Area는 객체를 저장하기 위한 메모리 영역이다. new 연산자로 생성된 모든 Object와 Instance 변수와 배열을 저장한다.
Heap 영역은 물리적으로 두 영역을 구분할 수 있다. (Young / Old Generation)
- Young Generation : 생명 주기가 짧은 객체를 GC 대상으로 하는 영역
- Old Generation : 생명 주기가 긴 객체를 GC 대상으로 하는 영역
Garbage Collection 생명주기에 의해 지속적으로 메모리가 정리된다. 이때 Minor GC, Major GC로 구분할 수 있다.
Young Generation
- 우선 메모리에 객체가 생성되면, Eden 영역에 객체가 지정된다.
- Eden 영역에 데이터가 가득 차게 되면, Eden 영역에 있던 객체가 Survivor0, Survivor1로 옮겨진다. 이때 0,1은 우선순위가 따로 있는 것은 아니다.
- Eden 영역에서 Survivor0, Survivor1로 옮겨지는 객체들은 어딘가에서 참조되고 있는 객체들이다. 둘 중 하나의 영역이 가득 차게 되면 공간이 남아있는 Survivor로 이동하게 된다.
이러한 메커니즘 때문에 Survivor1 또는 Survivor2 둘 중 하나는 항상 비워있는 공간이 있는 채로 유지된다.
이 과정에서 1차 GC라고 불리는 Minor GC가 발생하며, Minor GC는 New/Young 영역에서 발생하는 GC로 Eden 영역, Survivor0, Survivor1에서 사용되지 않는 객체들을 삭제한다.
Old Generation
- Survivor0, Survivor1을 왔다 갔다 하는 과정에서 오랜 기간 살아남은 객체들을 Old 영역으로 이동한다. 보통 Old 영역은 Young 영역보다 크게 할당하며, 이러한 이유로 Old 영역의 GC는 Young 영역보다 적게 발생한다.
- 단, Young 영역에서 Old 영역으로 넘어가는 객체 중, Survivor 영역을 거치지 않고 Eden 영역에서 바로 Old 영역으로 넘어가는 객체도 존재하는데, 이는 객체의 크기가 아주 클 경우 발생한다. (예를 들어 Survivor영역의 크기에 비해 객체의 크기가 더 클 경우에 해당한다.)
오래된 기준은 Young Generation 영역에서 Minor GC 가 발생하는 동안 얼마나 오래 살아남았는지로 판단한다.
각 객체엔 Minor GC에서 살아남은 횟수를 기록하는 age bit가 있으며, MaxTenuringThreshold라는 설정 값을 초과하면 Old Generation 영역으로 객체가 이동된다고 한다. 물론, 초과하지 않아도 Survivor 영역의 메모리가 부족할 경우에 미리 Old Generation 영역으로 객체가 이동될 수 있다.
또한, Old 영역에서는 2차 GC인 Major GC(FULL GC)가 일어나게 된다.
번외) Permanent
Java8 이전에 Class Meta Data, Method Meta Data, Static Object 변수, 상수, JVM, JIT 관련 데이터 등을 저장하던 Permanent 영역을 Java8에서부터는 Metaspace영역으로 대체했다. Metaspace영역은 Native 메모리 영역으로 JVM이 아닌 OS에 의해 관리되도록 변경되었다. 즉, static 객체는 heap이 아닌 별도의 Native 메모리에서 관리된다.
Stack Area

각 스레드를 위한 분리된 Runtime Stack 영역이다. 메서드를 호출할 때마다 Stack Frame으로 불리는 Entry가 Stack Area에 생성된다. 스레드의 역할이 종료되면 바로 소멸되는 특성의 데이터를 저장한다. 각 종 형태의 변수나 임시 데이터, 스레드 또는 메서드의 정보를 저장한다.
PC Register

PC는 Program Counter의 약어로, 각 스레드가 시작될 때 생성되며, 현재 실행 중인 상태 정보를 저장하는 영역이다.
스레드가 로직을 처리하면서 지속적으로 갱신되며, 스레드가 생성될 때마다 하나씩 존재한다. 어떤 명령을 실행해야 할지에 대한 기록이 있다. (현재 수행 중인 부분의 주소를 가진다.)
Native Method Area

바이트 코드가 아닌 실제 실행할 수 있는 기계어로 작성된 프로그램을 실행시키는 영역이며, Java가 아닌 다른 언어로 작성된 코드를 위한 영역이다. Java Native Interface를 통해 바이트 코드로 전환하여 저장한다. 이 영역 또한 각 스레드 별로 생성된다.
JNI(Java Native Interface)란?

자바가 다른 언어로 만들어진 애플리케이션과 상호 작용할 수 있는 인터페이스를 제공한다. JVM이 Native Method를 적재하고 수행할 수 있도록 한다. 실질적으로 제대로 동작(대응)하는 언어는 C, C++ 이 있다. (물론 다른 언어도 가능하지만 완전히 호환되는 것은 저 두 가지이다.)
Garbage Collector
앞으로 사용되지 않는 객체의 메모리를 Garbage라고 부른다. 이런 Garbage를 정해진 스케줄에 의해 정리해 주는 것이 GC(Garbage Collection)이다. Heap 영역에서 동적으로 할당했던 메모리 중 필요 없게 된 객체를 모아 주기적으로 제거해 주는 프로세스이다.
다른 언어는 프로그래머가 수동으로 메모리 할당과 해제를 하나하나 해줬어야 했다. 하지만 Java에서는 GC가 메모리 관리를 해주기 때문에 한정된 메모리를 효율적으로 사용할 수 있다. 따라서 메모리 관리, 메모리 누수 문제에 대해 직접 관리하는 부분이 적어진다.
Stop The World
GC를 수행하기 위해 JVM이 멈추는 현상을 의미한다. GC가 작동하는 동안 GC관련 Thread를 제외한 모든 Thread는 멈춘다. 또한, 어떠한 GC알고리즘을 사용하더라도 Stop The World 상태를 피할 수는 없다. 일반적으로 '튜닝'이라는 것은 이 멈추는 시간을 최소화하는 것을 말한다. 따라서 튜닝을 제대로 이해하기 위해서는 GC가 어떤 방식으로 동작하고, 어떠한 경우에 발생하는지 알아서 각 애플리케이션의 특징에 맞는 설정 값을 넣어야 한다.
가비지 컬렉션 작동 방식
위 Heap에서 GC가 어떻게 Reachable과 Unreachable을 판단하는지 말했었다.
Mark And Sweep(Mark-Sweep)이란 다양한 GC에서 사용되는 객체를 솎아내는 내부의 알고리즘이다. GC가 동작하는 기초적인 청소과정이다. 가비지 컬렉션이 될 객체를 Mark(식별)하고 Sweep(제거)하며 객체가 제거되어 파현화된 메모리 영역을 앞에서부터 채워나가는 Compaction을 수행한다.
- Mark : Root Space로부터 그래프 순회를 통해 연결된 객체를 찾아 어떤 객체를 참조하고 있는지 찾아 Mark 한다.
- Sweep : 참조하지 않은(Unreachable) 객체들을 Heap에서 Sweep 한다.
- Compact : Sweep 후 분산된 객체들을 Heap의 시작주소로 모아 메모리가 할당된 부분과 그렇지 않은 부분으로 Compact 한다.
현재 Java17, 21 기준 GC 종류
java -XX:+PrintFlagsFinal -version | grep GC | grep Use
해당 명령어로 전체 Flag에서 GC목록을 확인할 수 있다.
- SerialGC
- ParallelGC
- ShenandoahGC
- G1GC
- ZGC
Java17, 21 기준으로 Defalut GC는 G1 GC이다.
Serial GC
- 서버의 CPU 코어가 1개일 때 사용하기 위해 개발된 가장 단순한 GC이다.
- GC를 처리하는 스레드가 싱글 스레드여서 Stop The World 시간이 길다.
- Minor GC는 Mark-Sweep을 사용하고, Major GC는 Mark-Sweep-Compact를 사용한다.
- 거의 안 쓴다. 디바이스 성능이 좋지 않아 CPU 코어가 1개인 경우에만 사용한다.
Parallel GC
- Java 8의 Default GC이다.
- Serial GC와 기본적 알고리즘은 동일하나, Minor GC를 멀티 스레드로 수행한다.
- Serial GC보다는 Stop The World 시간이 짧다.
CMS GC
- 애플리케이션 스레드와 GC 스레드가 동시에 실행되어 Stop The World 시간을 줄이기 위해 고안됐다.
- 하지만 GC 과정이 복잡하고, GC대비 CPU 사용량이 높다.
- Java 14부터 사용이 중지되었다.
G1 GC
- Java 9+ 의 Default GC이다.
- 기존 GC 알고리즘에선 Heap 영역을 물리적으로 고정된 Young/Old 영역으로 나누어 사용했지만, G1 GC는 이런 개념을 뒤엎는 Region이라는 영역으로 Heap 영역을 분할하여 상황에 따라 Eden, Survivor, Old 등 역할을 동적으로 부여했다.
- 일일이 메모리를 탐색하지 않고, GC(메모리)가 가득 찬 영역(Region)을 빠르게 회수하여 빈 공간을 확보해 GC 빈도가 줄어드는 효과가 있다.
- 기존에 Eden → Survivor0 → Survivor1으로 순차적 이동과 달리 G1 GC에서는 순차적으로 이동하지는 않는다. 대신 G1 GC는 효율적이라고 생각하는 위치로 객체를 Reallocate(재할당) 시킨다. (예를 들어 Survivor1 영역에 있는 객체가 Eden 영역으로 할당하는 것이 더 효율적이라고 판단될 경우 Eden 영역으로 이동시킨다.)
- 4GB 이상의 Heap 메모리, Stop The World가 0.5초 필요한 상황에서 사용한다. (Heap이 작을 경우 사용을 권장하지 않는다.)
ShenandoahGC
- 기존 CMS가 가진 단편화를 가지고 있으며, G1이 가진 pause 이슈를 해결했다.
- 강력한 Concurrency(동시성)와 가벼운 로직으로 Heap 사이즈에 영향을 받지 않는다.
- 일정한 pause 시간이 소요된다.
Z GC
- 대량의 메모리(8MB ~ 16TB)를 Low-latency(Input과 Output사이 과정의 지연을 최소화)로 잘 처리하기 위해 디자인된 GC G1의 Region처럼, ZGC는 ZPage라는 영역을 사용한다.
- G1의 Region은 크기가 고정인데 비해, ZPage는 2mb 배수로 동적으로 운영된다.(큰 객체가 들어오면 2^ 로 영역을 구성해서 처리한다.)
- 최대 장점 중 하나는 Heap 크기가 증가하더도 Stop The World의 시간이 절대 10ms를 넘지 않는다는 것이다.
자세한 동작원리는 다음 블로그를 참고하면 좋을 것 같다.
https://d2.naver.com/helloworld/0128759
참고 문헌
https://1-7171771.tistory.com/140