새로운 가비지 컬렉터를 HotSpot에 추가한다고 가정해 보자. 공통 인터페이스를 구현하고 등록하면 끝날 것 같지만, 과거의 HotSpot에서는 그렇지 않았다. CollectedHeap 구현 객체를 생성하는 코드가 구체적인 Collector를 직접 선택했고, 모니터링 코드와 컴파일러에도 Collector별 지식이 흩어져 있었다.
흥미로운 점은 당시에도 이미 CollectedHeap이라는 추상 클래스가 있었다는 사실이다. 인터페이스는 있었지만 관심사는 충분히 분리되지 않은 상태였다.
JEP 304는 이 문제를 다룬다. 이 글에서는 새로운 인터페이스의 메서드를 나열하기보다 다음 두 가지에 집중한다.
- 공통 타입이 있는데도 왜 관심사의 분리에 실패했는가
- 구체 구현을 고르는 조건문을 없애지 않고도 어떻게 구조를 개선했는가
배경 개념
OpenJDK와 HotSpot
HotSpot은 Sun에서 개발을 시작해 OpenJDK로 공개됐고, 현재도 OpenJDK에서 개발되는 JVM 구현이다. OpenJDK 소스 트리에서 HotSpot은 JVM 역할을 담당한다.
JVM 명세는 클래스 파일을 어떻게 실행해야 하는지 정의하지만, 어떤 GC 알고리즘을 사용해야 하는지는 정하지 않는다. HotSpot은 JVM 명세를 구현하는 전체 런타임이고, Serial, Parallel, G1, ZGC 같은 Collector는 HotSpot 내부에서 메모리 회수 방식을 담당하는 하위 구현이다.
HotSpot 아래에 다시 구현체가 있는 것은 구현 대상의 층위가 다르기 때문이다. HotSpot은 JVM 전체를 구현하고, 각 Collector는 그 안에서 메모리 할당과 회수 방식을 구현한다. 짧은 일시 정지 시간, 높은 처리량, 작은 메모리 사용량을 한 알고리즘이 모든 환경에서 동시에 최적화하기 어려우므로 HotSpot은 서로 다른 목표의 Collector를 함께 제공한다.
Heap과 Collector
Java Heap은 Java 객체가 주로 저장되는 메모리 영역이다. Collector는 사용하지 않는 객체를 찾아 공간을 회수하고, 필요하면 살아 있는 객체를 이동한다.
HotSpot의 CollectedHeap은 Java Heap을 HotSpot 내부에서 표현하고 관리하는 C++ 추상 클래스다. Java Heap이라는 메모리 영역 자체와 CollectedHeap이라는 C++ 객체는 같은 것이 아니다. G1CollectedHeap, ZCollectedHeap 같은 구현 객체가 Java Heap의 초기화, 메모리 할당, 수집 작업을 Collector의 방식에 맞게 처리한다.
이 글에서 Heap은 메모리 영역을, CollectedHeap은 해당 영역을 관리하는 C++ 클래스 계층을 뜻한다.
JEP 304
JEP는 OpenJDK의 주요 변경 사항을 설명하고 추진하는 문서다. JDK 10에 포함된 JEP 304: Garbage-Collector Interface는 서로 다른 GC 구현의 소스 코드를 더 잘 격리하는 것을 목표로 했다.
목표는 애플리케이션의 GC 성능을 직접 높이는 것이 아니었다. HotSpot 내부의 모듈성을 높이고, 새 Collector를 추가할 때 기존 코드를 덜 건드리고, 특정 Collector를 빌드에서 제외하기 쉽게 만드는 구조 개선이었다.
인터페이스가 있어도 분리되지 않은 구조
JEP 304 이전에도 CollectedHeap은 존재했다. JDK 8의 소스에도 자신을 Java Heap 구현을 위한 추상 클래스라고 설명한다. 그렇다면 왜 별도의 GC 인터페이스 작업이 필요했을까?
CollectedHeap이 공통 Heap 연산을 추상화한 것과 HotSpot 전체에서 구체 Collector를 모르는 것은 별개의 문제이기 때문이다.
실제로 새 Collector를 추가하려면 다음과 같은 공통 영역도 함께 수정해야 했다. Collector별 지식이 GC 구현 디렉터리 밖의 공통 코드에 노출돼 있었던 것이다.
- JVM 시작 과정에서 어떤
CollectedHeap구현 객체를 생성할지 결정하는 코드 - 메모리 풀과 GC 스레드를 노출하는 모니터링 코드
- 객체 참조를 읽고 쓸 때 GC별 처리를 삽입하는 인터프리터와 JIT 컴파일러
- 특정 Collector를 포함할지 판단하는 빌드 코드
즉, 관심사의 분리는 공통 메서드 몇 개를 묶는 것만으로 달성되지 않는다. 구현체의 생성부터 실행, 관측, 컴파일러 연동까지 구체 구현을 알아야 하는 코드의 범위를 함께 통제해야 한다.
CollectedHeap 생성 코드에 노출된 Collector
JDK 8의 Universe::initialize_heap()은 VM 옵션을 확인하고 구체 CollectedHeap 클래스를 직접 생성했다. 변수명과 세부 초기화를 생략하면 다음 구조다.
if (UseParallelGC) {
_collectedHeap = new ParallelScavengeHeap();
} else if (UseG1GC) {
_collectedHeap = new G1CollectedHeap(...);
} else {
if (UseSerialGC) {
gc_policy = new MarkSweepPolicy();
} else if (UseConcMarkSweepGC) {
gc_policy = new ConcurrentMarkSweepPolicy();
}
_collectedHeap = new GenCollectedHeap(gc_policy);
}
Universe는 HotSpot의 전역 런타임 상태를 초기화하는 공통 코드다. 그런데 이 코드가 ParallelScavengeHeap과 G1CollectedHeap이라는 구체 CollectedHeap 클래스를 알고 있다.
이 구조에서 Collector를 하나 추가하면 새 클래스만 작성해서는 끝나지 않는다. Universe의 조건문도 수정해야 한다. Collector 종류가 늘어날수록 공통 초기화 코드가 함께 바뀐다.
CollectedHeap 타입으로 값을 보관한다는 사실은 이 결합을 없애지 못한다. 변수의 타입은 추상적이지만 객체를 고르는 책임은 여전히 공통 코드에 남아 있기 때문이다.
GCArguments와 GCConfig로 이동한 책임
현재 OpenJDK의 같은 코드는 훨씬 짧다.
jint Universe::initialize_heap() {
_collectedHeap = GCConfig::arguments()->create_heap();
log_info(gc)("Using %s", _collectedHeap->name());
return _collectedHeap->initialize();
}
전체 객체 생성 흐름은 팩토리 구조로 볼 수 있다. 다만 한 클래스가 선택과 생성을 모두 담당하지는 않는다. GCConfig는 사용할 GCArguments를 선택하고, 선택된 GCArguments가 CollectedHeap 구현 객체를 생성한다.
GCConfig: 사용할 GCArguments 선택
GCConfig는 HotSpot 빌드에 포함된 Collector와 각 Collector의 GCArguments 구현체를 다음과 같이 연결해 둔다.
UseG1GC → G1Arguments
UseZGC → ZArguments
UseEpsilonGC → EpsilonArguments
실제 구조에서 필요한 부분만 남기면 다음과 같다.
struct IncludedGC {
bool& option;
GCArguments& arguments;
};
static IncludedGC includedGCs[] = {
{ UseG1GC, g1Arguments },
{ UseZGC, zArguments }
};
void GCConfig::initialize() {
selectedArguments = select_gc();
}
GCArguments* GCConfig::select_gc() {
for (IncludedGC& gc : includedGCs) {
if (gc.option) {
return &gc.arguments;
}
}
return nullptr;
}
-XX:+UseG1GC가 지정되면 UseG1GC 값이 true가 되고, select_gc()는 여기에 연결된 G1Arguments를 반환한다. GCConfig가 G1CollectedHeap을 직접 생성하는 것은 아니다. 어떤 객체가 생성 방법을 제공할지 선택하는 등록소이자 선택기다. GCConfig의 실제 코드도 같은 방식으로 IncludedGCs를 순회해 하나의 GCArguments를 선택한다.
GCArguments: CollectedHeap을 생성하는 팩토리
GCArguments는 Collector별 JVM 옵션과 Heap 설정을 처리하면서 create_heap()으로 CollectedHeap 구현 객체를 생성하는 추상 클래스다. G1Arguments, ZArguments, EpsilonArguments가 구체 팩토리 역할을 한다.
class GCArguments {
public:
virtual CollectedHeap* create_heap() = 0;
};
class G1Arguments : public GCArguments {
public:
CollectedHeap* create_heap() override {
return new G1CollectedHeap();
}
};
G1Arguments가 G1CollectedHeap 생성 방법을 알고, ZArguments가 ZCollectedHeap 생성 방법을 아는 구조다. create_heap()이 생성하는 것은 Java 객체가 저장될 메모리 공간 자체가 아니라 이를 관리할 C++ 객체다. GCArguments의 실제 코드와 G1Arguments::create_heap()에서도 이 관계를 확인할 수 있다.
CollectedHeap: 실행 중인 Java Heap 관리
CollectedHeap은 생성된 뒤 HotSpot이 Heap을 공통 방식으로 사용할 수 있게 하는 추상 클래스다. 실제 클래스에는 더 많은 메서드가 있지만, 이 글에 필요한 부분만 남기면 다음과 같다.
class CollectedHeap {
public:
virtual jint initialize() = 0;
virtual void collect(GCCause::Cause cause) = 0;
};
class G1CollectedHeap : public CollectedHeap {
public:
jint initialize() override;
void collect(GCCause::Cause cause) override;
};
G1CollectedHeap, ZCollectedHeap 같은 구현체는 Java Heap에 필요한 메모리와 내부 자료구조를 초기화하고, 메모리 할당과 수집을 각 Collector의 방식으로 처리한다.
Universe: JVM 전역 상태와 Heap 초기화
Universe는 JVM 시작에 필요한 전역 상태를 보관한다. 그중 하나가 현재 JVM이 사용하는 단 하나의 CollectedHeap이다. 필요한 부분만 남기면 다음과 같다.
class Universe {
static CollectedHeap* collectedHeap;
};
jint Universe::initialize_heap() {
collectedHeap = GCConfig::arguments()->create_heap();
return collectedHeap->initialize();
}
JVM은 이후 초기화 과정에서 Java 클래스와 객체를 생성하므로 먼저 객체를 저장할 Heap이 필요하다. 실제 universe_init()도 Heap을 먼저 초기화한 뒤 문자열 테이블과 클래스 관련 구조를 초기화한다. Universe가 선택된 CollectedHeap을 보관하므로 JVM 시작 과정의 Heap 객체 생성과 초기화 호출도 이 클래스에서 시작한다.
Heap 외에도 Universe는 다음과 같은 JVM 전역 정보를 보관한다.
- 기본 타입과 배열의 클래스 정보
- 기본 타입의
Class객체 - 공통으로 재사용하는 빈 배열과
null대용 객체 - 메모리가 부족할 때 사용할 사전 생성된
OutOfMemoryError - 메인 스레드 그룹과 시스템 스레드 그룹
Universe라는 이름을 선택한 정확한 경위는 공개된 설계 기록에서 확인하기 어렵다. 다만 소스 주석은 이 클래스를 JVM이 알고 있는 시스템 클래스와 객체를 보관하는 공간으로 설명하고, 초기 객체를 만드는 메서드도 genesis()라고 부른다. 따라서 JVM 실행에 필요한 초기 전역 상태를 하나의 공간으로 다룬다는 의미의 이름으로 보인다. 이는 소스 구조를 바탕으로 한 해석이다. Universe 소스, universe_init() 소스
여기서 객체 생성과 메모리 초기화가 구분된다.
create_heap():G1CollectedHeap같은 C++ 구현 객체 생성initialize(): 선택된 Collector가 Java Heap에 필요한 메모리와 자료구조 초기화
G1을 선택한 경우의 전체 흐름은 다음과 같다.
-XX:+UseG1GC
↓
GCConfig가 G1Arguments 선택
↓
Universe가 G1Arguments.create_heap() 호출
↓
G1Arguments가 G1CollectedHeap 객체 생성
↓
Universe가 G1CollectedHeap.initialize() 호출
↓
G1 방식으로 Java Heap 초기화
현재 코드는 후속 리팩터링도 포함하지만, Collector 선택과 생성 책임이 이동한 구조는 그대로 확인할 수 있다.
조건문의 제거가 아닌 격리
GCConfig에도 선택 로직은 남아 있다. HotSpot에 포함된 Collector 목록을 순회하면서 UseG1GC 같은 옵션을 확인하고, 활성화된 옵션과 연결된 GCArguments를 선택한다.
Collector를 선택해야 하는 이상 구체 구현을 아는 코드는 반드시 존재한다. 중요한 것은 조건문을 모두 없애는 것이 아니라, 그 지식을 가질 수 있는 장소를 제한하는 것이다.
JDK 8에서도 Universe는 생성한 Heap을 CollectedHeap*로 보관했다. 생성 이후의 공통 코드는 구체 클래스가 무엇인지 몰라도 initialize()와 collect() 같은 메서드를 호출할 수 있었다. 문제는 그 이전 단계였다. Universe가 GC 옵션을 직접 확인하고 new G1CollectedHeap(...)처럼 구체 클래스를 생성했기 때문에 Collector 선택과 생성 책임이 공통 초기화 코드에 남아 있었다.
개선된 구조에서는 각 클래스가 다음 작업만 담당한다.
GCConfig: 옵션을 확인해 사용할GCArguments선택GCArguments::create_heap(): 선택된 Collector의CollectedHeap구현 객체 생성CollectedHeap: 초기화, 메모리 할당, 수집처럼 실행 중 필요한 공통 메서드 제공Universe: 선택된 객체의 생성과 초기화를 호출하고 결과를 JVM 전역 상태로 보관
구체 클래스 이름이 완전히 사라진 것은 아니다. GCConfig와 각 GCArguments 구현체만 구체 Collector를 알고, Universe를 비롯한 공통 실행 코드에서는 해당 지식이 제거됐다.
좋은 관심사의 분리는 구체적인 선택을 사라지게 하지 않는다. 구체 구현을 알아야 하는 지점을 제한한다.
실행 밖으로 이어지는 GC 관심사
CollectedHeap 구현 객체의 생성만 분리했다고 끝나지 않는다. GC는 모니터링, 컴파일러, 빌드 구성과도 연결된다.
모니터링을 위한 Serviceability
Serviceability는 JVM의 모니터링과 진단을 위해 메모리 풀, 메모리 관리자, GC 스레드 같은 내부 상태를 제공하는 기능이다. Collector마다 Heap 구성이 다르므로 공통 MemoryService가 이 구조를 직접 알면 모니터링 코드에 GC별 지식이 퍼진다.
JEP 304와 연결된 JDK-8191564는 GC 관련 코드가 services 디렉터리에 흩어져 있다는 문제를 명시한다. 현재의 MemoryService는 Collector별 분기 대신 CollectedHeap에 필요한 정보를 요청한다.
heap->initialize_serviceability();
heap->memory_pools();
heap->gc_threads_do(...);
heap->memory_managers();
공통 모니터링 코드는 정보를 사용하는 방법을 담당하고, 그 정보를 Collector 구조에 맞게 만드는 책임은 각 Heap 구현이 맡는다. 실행 알고리즘뿐 아니라 관측 방법까지 인터페이스 경계에 포함한 사례다.
객체 참조 접근을 위한 BarrierSet
GC는 수집을 시작한 시점에만 동작하는 것이 아니다. Collector에 따라 애플리케이션 스레드가 객체 참조를 읽거나 쓰는 순간에도 추가 처리가 필요하다. GC에서 말하는 Barrier는 참조를 읽거나 쓰는 코드 앞뒤에 끼워 넣는 짧은 추가 작업이다.
G1에서 오래 살아남은 객체가 새로 생성된 객체를 참조하는 상황을 예로 들 수 있다.
oldObject.child = youngObject;
Young GC는 Young 영역 밖에서 Young 객체를 가리키는 참조도 알아야 해당 객체를 살아 있는 것으로 판단할 수 있다. 이 정보를 찾기 위해 매번 Old 영역 전체를 조사하면 비용이 커진다. G1BarrierSet의 쓰기 배리어는 참조가 변경될 때 관련 카드 영역을 기록하고, Collector는 이 기록을 이용해 조사 범위를 줄인다.
ZGC는 애플리케이션 스레드가 실행되는 동안에도 객체를 이동할 수 있다. 어떤 필드가 주소 A의 객체를 가리키고 있는데 ZGC가 그 객체를 주소 B로 옮겼다고 가정해 보자. Heap의 모든 참조를 한순간에 A에서 B로 바꾸기는 어렵기 때문에 애플리케이션이 아직 A를 가리키는 참조를 읽을 수 있다.
이동 전: field ──→ 주소 A의 객체
이동 후: field ──→ 주소 A의 이전 참조 주소 B의 실제 객체
읽을 때: Load Barrier가 A를 확인 ──→ B로 보정
ZGC의 읽기 배리어인 Load Barrier는 이 상태를 감지해 이동된 주소 B의 객체를 사용하게 한다. 이 처리가 없으면 애플리케이션은 더 이상 객체가 존재하지 않는 이전 위치를 사용할 수 있다. G1은 주로 참조를 쓸 때 수집 범위를 기록하고, ZGC는 참조를 읽을 때도 이동 결과를 확인해야 한다. Collector마다 필요한 Barrier의 시점과 동작이 다른 이유다. ZGC 설계 문서
인터프리터와 JIT 컴파일러는 객체 참조 접근 코드를 만들 때 이러한 배리어도 함께 삽입한다. 공통 경계가 없다면 컴파일러 코드에는 아래의 가상 코드처럼 Collector별 분기가 반복될 수 있다. 이 경우 새 Collector를 추가할 때 공통 컴파일러 코드와 CPU별 코드도 수정해야 한다.
if (UseG1GC) {
emit_g1_write_barrier();
} else if (UseZGC) {
emit_z_load_barrier();
}
BarrierSet 계층은 컴파일러가 배리어를 요청하는 공통 방식을 정의한다. 실제 동작은 G1BarrierSet, ZBarrierSet 같은 Collector별 구현이 제공한다.
인터프리터와 JIT 컴파일러
↓ 공통 배리어 API 호출
BarrierSet
├── G1BarrierSet
└── ZBarrierSet
이 경계가 있으면 컴파일러는 Collector별 배리어의 내부 동작을 알 필요가 없다. 새로운 Collector를 추가할 때 수정 범위를 해당 BarrierSet 구현 쪽으로 제한할 수 있다는 것이 구체적인 효과다.
빌드에서도 구체 Collector를 한곳에서만 알게 하기
HotSpot은 C++로 작성되며 JDK를 빌드할 때 네이티브 코드로 컴파일된다. 각 Collector는 실행 중에 설치하는 플러그인이 아니다. JDK를 빌드할 때 어떤 Collector의 코드를 HotSpot 바이너리에 포함할지 정한다.
문제는 특정 Collector를 제외했는데 공통 코드가 여전히 그 Collector의 클래스를 참조하는 경우다. 예를 들어 G1 코드를 빌드에서 제외해도 Universe가 G1CollectedHeap을 직접 생성한다면 컴파일러는 존재하지 않는 클래스를 만나게 된다. 이를 피하려면 Universe를 비롯해 G1을 참조하는 모든 코드에 별도의 조건을 추가해야 한다.
개선된 구조에서는 Collector별 헤더, GCArguments 구현 객체, 등록 정보를 GCConfig 주변에 모은다. G1을 제외한 빌드에서는 C++ 컴파일 전에 GCConfig의 등록 목록에서 G1Arguments가 제거된다. 반면 Universe는 어떤 Collector가 포함됐는지 알지 않고 항상 GCConfig::arguments()와 GCArguments::create_heap()만 호출한다.
GCConfig에는 Collector별 조건이 남아 있다. 달라진 점은 조건의 위치다. 구체 Collector를 아는 빌드 코드를 제한된 지점에 모았기 때문에 Collector를 추가하거나 제외할 때 Universe 같은 공통 코드를 함께 수정할 필요가 줄었다. JEP 304는 이를 목표로 제시했고, 이후의 조건부 컴파일 작업이 Collector별 등록과 빌드 조건을 정리했다.
새로운 Collector를 추가할 때 생긴 변화
메모리를 회수하지 않는 Epsilon
JEP 318: Epsilon은 메모리를 할당하지만 사용하지 않는 객체를 회수하지 않는 Collector다. 할당 가능한 Heap을 모두 사용하면 회수로 공간을 만들 수 없으므로 이후 객체 할당이 실패한다.
회수 알고리즘이 없는데도 Collector라고 부르는 이유는 HotSpot의 GC 하위 시스템이 메모리 회수만 담당하지 않기 때문이다. Epsilon도 EpsilonArguments와 EpsilonHeap을 구현해 다음 역할을 수행한다.
- JVM 옵션을 통해 Collector로 선택됨
- Java 객체가 사용할 Heap 메모리 초기화
- 객체를 위한 메모리 할당
- 모니터링과 진단 기능에 Heap 정보 제공
Epsilon에서 봐야 할 핵심은 회수 알고리즘의 난도가 아니다. Epsilon도 JVM 옵션으로 선택되고, 전용 Heap 객체를 만들고, 메모리를 할당하며, Heap 정보를 모니터링 기능에 제공해야 한다. JEP 304 이후의 경계를 사용하면 Universe가 EpsilonHeap을 직접 알게 만들지 않고도 이 연결 과정을 처리할 수 있다.
새 Collector를 추가할 때 Universe 같은 관련 없는 공통 코드를 함께 수정하는 범위를 줄였다는 점이 중요하다. Epsilon은 성격이 매우 단순한 Collector도 같은 연결 구조에 들어갈 수 있음을 보여주는 사례다.
새로운 알고리즘과 연결 구조의 분리
JEP 333: ZGC는 JEP 304를 의존 작업으로 명시했다. 그렇다고 JEP 304가 ZGC의 알고리즘을 제공한 것은 아니다. ZGC 개발자는 애플리케이션 스레드와 동시에 수행되는 수집, 객체 이동, 배리어 처리 같은 ZGC 고유 기능을 별도로 구현해야 했다.
JEP 304가 줄인 것은 이 알고리즘을 HotSpot에 연결하기 위해 수정해야 하는 공통 코드의 범위다. 공통 초기화 코드는 ZGC의 내부 알고리즘을 직접 알지 않고도 GCArguments를 통해 ZCollectedHeap 객체를 생성하고, 이후에는 CollectedHeap의 공통 메서드로 이를 사용할 수 있다.
Epsilon과 ZGC의 차이를 함께 보면 역할이 더 분명해진다.
- Epsilon: 메모리를 회수하지 않는 단순한 동작을 기존 HotSpot 구조에 연결
- ZGC: 동시 수집과 객체 이동을 수행하는 새로운 알고리즘을 기존 HotSpot 구조에 연결
두 Collector의 알고리즘은 각각 별도로 개발됐다. JEP 304의 역할은 알고리즘 구현이 아니라, 성격이 다른 Collector를 추가할 때 공통 초기화와 모니터링 같은 기존 코드까지 함께 수정해야 하는 범위를 줄이는 것이었다.
관심사의 분리에서 배울 점
인터페이스의 존재보다 변경 범위
CollectedHeap이 이미 존재했다는 점이 이 사례의 핵심이다. 공통 타입을 만들었다고 관심사의 분리가 끝나지 않는다. 새 구현체를 추가할 때 생성, 설정, 모니터링, 컴파일러 연동에 구체 구현의 이름을 계속 추가해야 한다면 경계가 충분하지 않다.
주요 동작뿐 아니라 생명 주기와 관측 지점
구현체는 실행만 되는 것이 아니라 생성되고, 설정되고, 관측되고, 종료된다. JEP 304에서는 CollectedHeap의 실행 메서드뿐 아니라 구현 객체 생성과 serviceability까지 분리 대상이었다.
애플리케이션 코드에서도 같은 질문을 적용할 수 있다. 예를 들어 결제 처리에서 pay() 호출 방식만 인터페이스로 통일했더라도, 결제 수단별 객체 생성과 설정 검증, 메트릭 수집에 조건문이 반복된다면 구체 구현에 대한 의존성은 여전히 공통 코드에 남아 있다.
구체 구현을 아는 지점의 제한
팩토리나 설정 코드에는 구현체를 고르는 분기가 남는다. 이는 추상화 실패가 아니라 구체 구현을 알아야 하는 책임을 한곳에 모은 결과다. 검토할 것은 조건문의 개수가 아니라 위치다.
마무리
HotSpot 아래에 여러 GC 구현체가 있는 이유는 HotSpot과 Collector가 서로 다른 대상을 구현하기 때문이다. HotSpot은 JVM 전체를 구현하고, Collector는 그 안에서 교체 가능한 메모리 관리 전략을 구현한다.
JEP 304가 보여주는 관심사의 분리는 인터페이스 추출보다 넓다. 이미 있던 CollectedHeap을 출발점으로 삼아 구현체 선택과 생성, 모니터링, 빌드 구성을 Collector 경계 쪽으로 옮겼다. 인접한 BarrierSet 작업은 컴파일러와의 접점도 같은 방식으로 분리했다.
결국 좋은 경계는 클래스 다이어그램보다 변경 시나리오에서 확인된다. 새 구현체를 추가할 때 기존 코드를 얼마나 적게 수정하는가. JEP 304는 그 질문을 HotSpot의 GC 구조에 적용한 사례다.
참고 자료
- JEP 304: Garbage-Collector Interface
- JDK 8 CollectedHeap 소스
- JDK 8 Universe 소스
- 현재 OpenJDK Universe 소스
- 현재 OpenJDK GCArguments 소스
- 현재 OpenJDK GCConfig 소스
- JDK-8191564: Move GC related serviceability code into GC specific directories
- 현재 OpenJDK MemoryService 소스
- 현재 OpenJDK BarrierSet 소스
- JDK-8200729: Conditional compilation of GCs
- JEP 318: Epsilon: A No-Op Garbage Collector
- JEP 333: ZGC: A Scalable Low-Latency Garbage Collector
- OpenJDK hotspot-gc-dev 2018년 5월 기록
'개발 공부 > Java' 카테고리의 다른 글
| 자바 Collector 구조 (feat. groupingBy() 사용법) (0) | 2024.01.14 |
|---|---|
| 형 변환 정리 (0) | 2022.05.17 |
| 내부 클래스에 static이 권장되는 이유 (0) | 2022.04.26 |
| 함수형 인터페이스와 람다식 (0) | 2022.04.26 |
| ISP (인터페이스 분리 원칙) in 스프링 프레임워크 (0) | 2022.04.17 |