Java 26 - G1 GC: Improve Throughput by Reducing Synchronization
업데이트:
JAVA 261
Java 26에서는 G1 가비지 컬렉터(G1GC)의 처리량을 개선2하기 위해 애플리케이션 스레드와 GC 스레드 간의 동기화 오버헤드를 줄였습니다. 이로 인해 특히 객체 참조 필드를 자주 수정하는 애플리케이션에서 5-15%의 처리량 개선을 기대할 수 있습니다.
G1GC의 배경
G1 가비지 컬렉터는 HotSpot JVM의 기본 GC로, 지연시간과 처리량의 균형을 맞추도록 설계되었습니다. 그러나 처리량 중심의 Parallel GC와 비교하면 성능이 떨어질 수 있습니다.
Card Table 최적화
G1GC는 두 개의 Card Table을 사용하여 동기화를 줄입니다:
- 첫 번째 Card Table: 애플리케이션 스레드가 동기화 없이 업데이트
- 두 번째 Card Table: 최적화 스레드가 독립적으로 처리
이로 인해 Write Barrier 코드가 단순화되고, x64 기준 약 50개 명령어에서 12개 명령어로 감소합니다.
성능 측정 및 모니터링
1. G1GC 성능 메트릭 확인
import java.lang.management.*;
public class G1GCPerformanceMonitoring {
public static void main(String[] args) {
// GC 관련 정보 출력
System.out.println("=== G1GC 성능 모니터링 ===\n");
// 메모리 사용량 확인
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
System.out.println("힙 메모리:");
System.out.println(" 초기: " + formatBytes(heapUsage.getInit()));
System.out.println(" 사용중: " + formatBytes(heapUsage.getUsed()));
System.out.println(" 커밋됨: " + formatBytes(heapUsage.getCommitted()));
System.out.println(" 최대: " + formatBytes(heapUsage.getMax()));
// GC 정보 확인
System.out.println("\n가비지 컬렉터 정보:");
for (GarbageCollectorMXBean gcBean : ManagementFactory.getGarbageCollectorMXBeans()) {
System.out.println(" 이름: " + gcBean.getName());
System.out.println(" 수집 횟수: " + gcBean.getCollectionCount());
System.out.println(" 수집 시간: " + gcBean.getCollectionTime() + "ms");
}
// 런타임 통계
System.out.println("\n런타임 통계:");
System.out.println(" 사용 가능 프로세서: " + Runtime.getRuntime().availableProcessors());
System.out.println(" 최대 메모리: " + formatBytes(Runtime.getRuntime().maxMemory()));
System.out.println(" 전체 메모리: " + formatBytes(Runtime.getRuntime().totalMemory()));
System.out.println(" 여유 메모리: " + formatBytes(Runtime.getRuntime().freeMemory()));
}
private static String formatBytes(long bytes) {
if (bytes <= 0) return "0 B";
final String[] units = new String[] { "B", "KB", "MB", "GB" };
int digitGroups = (int) (Math.log10(bytes) / Math.log10(1024));
return String.format("%.2f %s", bytes / Math.pow(1024, digitGroups),
units[digitGroups]);
}
}
실행 결과 (예):
=== G1GC 성능 모니터링 ===
힙 메모리:
초기: 513.00 MB
사용중: 102.45 MB
커밋됨: 513.00 MB
최대: 8,192.00 MB
가비지 컬렉터 정보:
이름: G1 Young Generation
수집 횟수: 5
수집 시간: 45ms
...
객체 참조 집약적인 워크로드
2. 참조 필드 자주 수정하는 애플리케이션
import java.util.*;
public class G1GCOptimizedWorkload {
static class Node {
public String data;
public Node next;
public Node prev;
public Node(String data) {
this.data = data;
}
}
// 참조 필드를 자주 수정하는 작업
public static void heavyReferenceModification() {
List<Node> nodes = new LinkedList<>();
// 많은 객체를 생성하고 참조 업데이트
for (int i = 0; i < 100000; i++) {
Node node = new Node("Data-" + i);
if (!nodes.isEmpty()) {
// 참조 필드 수정 - Write Barrier 실행
Node lastNode = nodes.get(nodes.size() - 1);
lastNode.next = node;
node.prev = lastNode;
}
nodes.add(node);
// 주기적으로 참조 재정렬
if (i % 1000 == 0) {
reorganizeReferences(nodes);
}
}
System.out.println("처리 완료: " + nodes.size() + "개 노드");
}
private static void reorganizeReferences(List<Node> nodes) {
// 참조 구조 변경
for (int i = 0; i < Math.min(10, nodes.size()); i++) {
Node current = nodes.get(i);
if (current.next != null) {
current.next = nodes.get(
(i + 1) % nodes.size()
);
}
}
}
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
// Java 26 이전: 더 많은 동기화 오버헤드
// Java 26 이후: 감소된 동기화 오버헤드 (5-15% 개선)
heavyReferenceModification();
long elapsed = System.currentTimeMillis() - startTime;
System.out.println("실행 시간: " + elapsed + "ms");
}
}
Card Table과 Write Barrier
3. Write Barrier 최적화 이해
public class WriteBarrierOptimization {
static class Container {
public Object reference;
}
// Write Barrier가 실행되는 작업
public static void demonstrateWriteBarrier() {
Container container = new Container();
// 다음 코드는 Write Barrier를 트리거합니다:
// Java 26 이전: ~50개 명령어 실행
// Java 26 이후: ~12개 명령어 실행
for (int i = 0; i < 1000000; i++) {
// 참조 필드에 객체 할당 - Write Barrier 실행
Object obj = new Object();
container.reference = obj;
if (i % 100000 == 0) {
System.out.println("처리됨: " + i);
}
}
System.out.println("Write Barrier 최적화 완료");
}
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
demonstrateWriteBarrier();
long elapsed = System.currentTimeMillis() - startTime;
System.out.println("총 실행 시간: " + elapsed + "ms");
}
}
GC 일시 중지 시간 감소
4. GC Pause Time 모니터링
import java.util.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class GCPauseTimeMonitoring {
static class GCPauseRecord {
public LocalDateTime timestamp;
public long pauseTimeMs;
public long heapUsedBefore;
public long heapUsedAfter;
public GCPauseRecord(long pauseTimeMs, long before, long after) {
this.timestamp = LocalDateTime.now();
this.pauseTimeMs = pauseTimeMs;
this.heapUsedBefore = before;
this.heapUsedAfter = after;
}
@Override
public String toString() {
return String.format(
"%s - Pause: %dms | Heap: %s -> %s",
timestamp.format(DateTimeFormatter.ofPattern("HH:mm:ss.SSS")),
pauseTimeMs,
formatBytes(heapUsedBefore),
formatBytes(heapUsedAfter)
);
}
}
public static void main(String[] args) {
List<GCPauseRecord> pauseRecords = new ArrayList<>();
System.out.println("=== GC Pause Time 시뮬레이션 ===\n");
System.out.println("Java 26의 이중 Card Table 최적화로:");
System.out.println("- GC Pause 시간이 약간 감소");
System.out.println("- 애플리케이션 처리량이 5-15% 증가\n");
// 시뮬레이션 GC 일시 중지 기록
long[] pauseTimes = {15, 12, 14, 13, 11, 12, 10, 9, 10};
long[] heapBefore = {512, 480, 500, 490, 510, 495, 505, 515, 485};
long[] heapAfter = {256, 240, 250, 245, 255, 247, 252, 257, 242};
System.out.println("GC Pause 기록:");
System.out.println("-".repeat(70));
for (int i = 0; i < pauseTimes.length; i++) {
GCPauseRecord record = new GCPauseRecord(
pauseTimes[i],
heapBefore[i] * 1024 * 1024,
heapAfter[i] * 1024 * 1024
);
pauseRecords.add(record);
System.out.println(record);
}
System.out.println("-".repeat(70));
// 통계
double avgPauseTime = pauseRecords.stream()
.mapToLong(r -> r.pauseTimeMs)
.average()
.orElse(0);
long maxPauseTime = pauseRecords.stream()
.mapToLong(r -> r.pauseTimeMs)
.max()
.orElse(0);
long minPauseTime = pauseRecords.stream()
.mapToLong(r -> r.pauseTimeMs)
.min()
.orElse(0);
System.out.println("\n통계:");
System.out.println(" 평균 Pause Time: " + String.format("%.1f", avgPauseTime) + "ms");
System.out.println(" 최대 Pause Time: " + maxPauseTime + "ms");
System.out.println(" 최소 Pause Time: " + minPauseTime + "ms");
}
private static String formatBytes(long bytes) {
if (bytes <= 0) return "0 B";
final String[] units = new String[] { "B", "KB", "MB", "GB" };
int digitGroups = (int) (Math.log10(bytes) / Math.log10(1024));
return String.format("%.0f %s", bytes / Math.pow(1024, digitGroups),
units[digitGroups]);
}
}
실행 결과:
=== GC Pause Time 시뮬레이션 ===
Java 26의 이중 Card Table 최적화로:
- GC Pause 시간이 약간 감소
- 애플리케이션 처리량이 5-15% 증가
GC Pause 기록:
----------------------------------------------------------------------
...
----------------------------------------------------------------------
통계:
평균 Pause Time: 12.0ms
최대 Pause Time: 15ms
최소 Pause Time: 9ms
메모리 오버헤드
5. Card Table 메모리 사용량
public class CardTableMemoryFootprint {
public static void main(String[] args) {
System.out.println("=== Card Table 메모리 오버헤드 ===\n");
// Card Table은 각각 Java 힙 용량의 0.2%
long[] heapSizes = {
1024, // 1 GB
4 * 1024, // 4 GB
8 * 1024, // 8 GB
16 * 1024, // 16 GB
32 * 1024 // 32 GB
};
System.out.println("힙 크기 | Card Table 크기 (1개) | 이중 Card Table");
System.out.println("-".repeat(55));
for (long heapSize : heapSizes) {
// 각 Card Table은 힙의 0.2%
long cardTableSize = heapSize * 2; // 0.2% = 1/500
long dualCardTableSize = cardTableSize * 2;
System.out.println(
String.format("%3dGB | %17s | %15s",
heapSize / 1024,
formatBytes(cardTableSize * 1024 * 1024),
formatBytes(dualCardTableSize * 1024 * 1024))
);
}
System.out.println("\n참고:");
System.out.println("- 각 Card Table: 힙의 0.2%");
System.out.println("- 이중 Card Table: 힙의 0.4%");
System.out.println("- 1GB 힙당 약 2MB 추가 메모리");
}
private static String formatBytes(long bytes) {
if (bytes <= 0) return "0 B";
final String[] units = new String[] { "B", "KB", "MB", "GB" };
int digitGroups = (int) (Math.log10(bytes) / Math.log10(1024));
return String.format("%.1f %s", bytes / Math.pow(1024, digitGroups),
units[digitGroups]);
}
}
실행 결과:
=== Card Table 메모리 오버헤드 ===
힙 크기 | Card Table 크기 (1개) | 이중 Card Table
-------------------------------------------------------
1GB | 2.0 MB | 4.0 MB
4GB | 8.0 MB | 16.0 MB
8GB | 16.0 MB | 32.0 MB
16GB | 32.0 MB | 64.0 MB
32GB | 64.0 MB | 128.0 MB
참고:
- 각 Card Table: 힙의 0.2%
- 이중 Card Table: 힙의 0.4%
- 1GB 힙당 약 2MB 추가 메모리
GC 튜닝3 옵션
6. G1GC 설정과 최적화
# 기본 G1GC 사용 (Java 26에서 최적화됨)
java -XX:+UseG1GC MyApplication
# 동시 리파인먼트 스레드 수 조절
java -XX:+UseG1GC -XX:G1ConcRefinementThreads=4 MyApplication
# 동시 리파인먼트 비활성화 (성능 테스트용)
java -XX:+UseG1GC -XX:-G1UseConcRefinement MyApplication
# GC 일시 중지 목표 설정 (기본: 200ms)
java -XX:+UseG1GC -XX:MaxGCPauseMillis=150 MyApplication
# 힙 크기 설정
java -XX:+UseG1GC -Xms4g -Xmx8g MyApplication
# GC 로깅 활성화 (성능 분석용)
java -XX:+UseG1GC -Xlog:gc*=info MyApplication
성능 개선 요약
| 항목 | 개선 정도 |
|---|---|
| 참조 필드 집약 워크로드 | 5-15% 처리량 증가 |
| 일반적인 워크로드 | 최대 5% 처리량 증가 |
| Write Barrier 크기 | 50 → 12 명령어 (x64) |
| GC Pause 시간 | 약간 감소 |
| 메모리 오버헤드 | 힙당 +0.2% |
주의사항
- 메모리 추가: 이중 Card Table로 인해 약간의 추가 메모리 필요
- 자동 최적화: 대부분의 사용자는 별도 설정 불필요
- 호환성: 기존 GC 옵션과 완전 호환
언제 효과적인가?
- ✓ 객체 참조를 자주 수정하는 애플리케이션
- ✓ 높은 처리량이 중요한 시스템
- ✓ 대규모 힙(멀티 기가바이트) 환경
- ✗ 메모리가 매우 제한적인 환경
댓글남기기