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%

주의사항

  1. 메모리 추가: 이중 Card Table로 인해 약간의 추가 메모리 필요
  2. 자동 최적화: 대부분의 사용자는 별도 설정 불필요
  3. 호환성: 기존 GC 옵션과 완전 호환

언제 효과적인가?

  • ✓ 객체 참조를 자주 수정하는 애플리케이션
  • ✓ 높은 처리량이 중요한 시스템
  • ✓ 대규모 힙(멀티 기가바이트) 환경
  • ✗ 메모리가 매우 제한적인 환경

Reference

댓글남기기