Repository

6단계: JVM & 성능 최적화 본문

Java

6단계: JVM & 성능 최적화

Mr.Manager 2025. 12. 11. 20:23
반응형

6단계: JVM & 성능 최적화

4년간의 실무에서 마주한 성능 이슈와 JVM 튜닝 경험을 바탕으로, 단순한 이론이 아닌 실제 프로덕션 환경에서 적용 가능한 최적화 기법들을 다룹니다.


6.1 JVM 이해하기

JVM 메모리 구조 완벽 가이드

Heap vs Stack: 메모리 영역별 특성 이해

메모리 영역 다이어그램

┌─────────────────────────────────────────┐
│                JVM Memory                │
├─────────────────────────────────────────┤
│  Method Area (Metaspace in Java 8+)     │
│  - Class metadata                       │
│  - Method bytecode                      │
│  - Constant pool                        │
├─────────────────────────────────────────┤
│  Heap Memory                            │
│  ┌─────────────┬─────────────────────┐  │
│  │ Young Gen   │ Old Gen             │  │
│  │ - Eden      │ - Long-lived objects│  │
│  │ - S0        │                     │  │
│  │ - S1        │                     │  │
│  └─────────────┴─────────────────────┘  │
├─────────────────────────────────────────┤
│  Stack Memory                           │
│  - Method frames                       │
│  - Local variables                     │
│  - Operand stack                       │
├─────────────────────────────────────────┤
│  PC Register                            │
│  - Current instruction pointer         │
├─────────────────────────────────────────┤
│  Native Method Stack                   │
│  - Native method calls                 │
└─────────────────────────────────────────┘

실무에서 마주한 메모리 문제들

public class MemoryIssueExample {
    // ❌ 문제 1: Stack Overflow
    public static void recursiveMethod(int depth) {
        if (depth > 0) {
            recursiveMethod(depth - 1);  // 무한 재귀 시 Stack Overflow
        }
    }

    // ❌ 문제 2: Heap 메모리 누수
    private static final Map<String, Object> CACHE = new HashMap<>();

    public static void addToCache(String key, Object value) {
        CACHE.put(key, value);  // 계속 추가만 하고 제거하지 않음
        // OutOfMemoryError 발생!
    }

    // ❌ 문제 3: String Pool 남용
    public static String processLargeString(String input) {
        // 큰 문자열을 substring으로 처리하면 메모리 누수 가능
        return input.substring(0, 10);  // Java 6 이하에서 문제
    }
}

메모리 영역별 상세 분석

public class MemoryAreaAnalysis {
    // 1. Method Area (Metaspace) - 클래스 정보 저장
    private static final String STATIC_CONSTANT = "Static Value";

    static {
        // static 초기화 블록도 Method Area에 저장
        System.out.println("Class loaded into Method Area");
    }

    // 2. Heap - 모든 객체와 배열
    public void demonstrateHeapUsage() {
        // 객체 생성 (Heap에 저장)
        List<String> list = new ArrayList<>();
        String str = new String("Heap String");

        // 배열 생성 (Heap에 저장)
        int[] numbers = new int[1000];

        // 참조 변수는 Stack에, 실제 객체는 Heap에
        System.out.println("Objects created in Heap");
    }

    // 3. Stack - 메서드 호출과 지역 변수
    public void demonstrateStackUsage() {
        // 지역 변수 (Stack에 저장)
        int localVar = 42;
        String localStr = "Stack String";

        // 메서드 호출 시 새로운 Stack Frame 생성
        anotherMethod(localVar);
    }

    private void anotherMethod(int param) {
        // 새로운 Stack Frame에 param과 지역 변수들 저장
        int localVar2 = 100;
        System.out.println("New stack frame created");
    }
}

Method Area와 Metaspace의 진화

Java 8 이전: PermGen의 문제점

// ❌ Java 7 이하에서 발생하던 문제들
public class PermGenIssues {
    // 1. 클래스 로더 메모리 누수
    public void classLoaderLeak() {
        // 동적 클래스 로딩 시 PermGen에 누적
        for (int i = 0; i < 10000; i++) {
            ClassLoader loader = new CustomClassLoader();
            Class<?> clazz = loader.loadClass("SomeClass");
            // loader를 해제하지 않으면 PermGen 누수
        }
    }

    // 2. String Pool 크기 제한
    public void stringPoolOverflow() {
        // PermGen 크기 제한으로 인한 OutOfMemoryError
        for (int i = 0; i < 1000000; i++) {
            String str = "String" + i;  // String Pool에 누적
        }
    }
}

Java 8+: Metaspace의 개선

// ✅ Java 8+ Metaspace의 장점
public class MetaspaceBenefits {
    // 1. 자동 크기 조정
    // -XX:MetaspaceSize=256m (초기 크기)
    // -XX:MaxMetaspaceSize=512m (최대 크기)

    // 2. 클래스 메타데이터 자동 해제
    public void dynamicClassLoading() {
        ClassLoader loader = new CustomClassLoader();
        Class<?> clazz = loader.loadClass("DynamicClass");

        // loader = null로 설정하면 GC가 메타데이터 해제
        loader = null;
        System.gc();  // Metaspace에서 클래스 정보 제거됨
    }

    // 3. 메모리 사용량 모니터링
    public void monitorMetaspace() {
        MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
        MemoryUsage metaspaceUsage = memoryBean.getNonHeapMemoryUsage();

        System.out.println("Metaspace Used: " + metaspaceUsage.getUsed());
        System.out.println("Metaspace Max: " + metaspaceUsage.getMax());
    }
}

OutOfMemoryError 원인과 해결

실무에서 마주한 OOM 케이스들

public class OOMCaseStudies {

    // Case 1: Heap Space 부족
    public void heapSpaceOOM() {
        List<byte[]> memoryConsumer = new ArrayList<>();

        try {
            while (true) {
                // 1MB씩 메모리 할당
                memoryConsumer.add(new byte[1024 * 1024]);
            }
        } catch (OutOfMemoryError e) {
            System.err.println("Heap space exhausted: " + e.getMessage());

            // 해결책 1: 힙 크기 증가
            // -Xms2g -Xmx4g

            // 해결책 2: 메모리 사용량 최적화
            optimizeMemoryUsage();
        }
    }

    // Case 2: Metaspace 부족
    public void metaspaceOOM() {
        try {
            // 동적 클래스 생성으로 Metaspace 포화
            for (int i = 0; i < 100000; i++) {
                generateDynamicClass("Class" + i);
            }
        } catch (OutOfMemoryError e) {
            System.err.println("Metaspace exhausted: " + e.getMessage());

            // 해결책: Metaspace 크기 조정
            // -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=1g
        }
    }

    // Case 3: Stack Overflow
    public void stackOverflow() {
        try {
            recursiveMethod(10000);
        } catch (StackOverflowError e) {
            System.err.println("Stack overflow: " + e.getMessage());

            // 해결책: 스택 크기 증가
            // -Xss2m (기본값: 1m)
        }
    }

    private void optimizeMemoryUsage() {
        // 메모리 효율적인 객체 생성
        List<String> optimizedList = new ArrayList<>(1000);  // 초기 용량 지정

        // 불필요한 객체 생성 피하기
        StringBuilder sb = new StringBuilder(256);  // 적절한 초기 용량
        for (int i = 0; i < 100; i++) {
            sb.append("item").append(i);
        }
    }

    private void generateDynamicClass(String className) {
        // 동적 클래스 생성 로직
    }
}

메모리 모니터링 실전 코드

public class MemoryMonitor {
    private static final Logger logger = LoggerFactory.getLogger(MemoryMonitor.class);

    public static void startMemoryMonitoring() {
        Timer timer = new Timer();
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                printMemoryUsage();
                checkMemoryThresholds();
            }
        }, 0, 30000);  // 30초마다 실행
    }

    private static void printMemoryUsage() {
        Runtime runtime = Runtime.getRuntime();
        MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();

        // Heap 메모리
        MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
        long heapUsed = heapUsage.getUsed();
        long heapMax = heapUsage.getMax();
        double heapUsagePercent = (double) heapUsed / heapMax * 100;

        // Non-Heap 메모리 (Metaspace 포함)
        MemoryUsage nonHeapUsage = memoryBean.getNonHeapMemoryUsage();
        long nonHeapUsed = nonHeapUsage.getUsed();

        logger.info("Memory Usage - Heap: {}/{} ({:.1f}%), Non-Heap: {}",
            formatBytes(heapUsed),
            formatBytes(heapMax),
            heapUsagePercent,
            formatBytes(nonHeapUsed)
        );

        // GC 정보
        List<GarbageCollectorMXBean> gcBeans = ManagementFactory.getGarbageCollectorMXBeans();
        for (GarbageCollectorMXBean gcBean : gcBeans) {
            logger.info("GC: {} - Count: {}, Time: {}ms",
                gcBean.getName(),
                gcBean.getCollectionCount(),
                gcBean.getCollectionTime()
            );
        }
    }

    private static void checkMemoryThresholds() {
        MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
        MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();

        double usagePercent = (double) heapUsage.getUsed() / heapUsage.getMax() * 100;

        if (usagePercent > 90) {
            logger.warn("High memory usage: {:.1f}%", usagePercent);
            // 알림 발송, 로그 정리 등
        } else if (usagePercent > 80) {
            logger.info("Memory usage warning: {:.1f}%", usagePercent);
        }
    }

    private static String formatBytes(long bytes) {
        if (bytes < 1024) return bytes + " B";
        if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0);
        if (bytes < 1024 * 1024 * 1024) return String.format("%.1f MB", bytes / (1024.0 * 1024.0));
        return String.format("%.1f GB", bytes / (1024.0 * 1024.0 * 1024.0));
    }
}

가비지 컬렉션(GC) 이해하기

GC의 동작 원리: Mark and Sweep부터 Generational까지

기본 GC 알고리즘

public class GCBasis {

    // 1. Mark and Sweep 기본 원리
    public void demonstrateMarkAndSweep() {
        // Root 객체들
        List<String> rootList = new ArrayList<>();
        String rootString = "Root";

        // 참조 체인
        rootList.add(rootString);
        String unreachable = "Unreachable";  // Root에서 접근 불가

        // GC 과정:
        // 1. Mark: Root부터 시작해서 도달 가능한 객체들 마킹
        // 2. Sweep: 마킹되지 않은 객체들 해제

        rootList = null;  // 이제 unreachable도 GC 대상
    }

    // 2. Generational Hypothesis 실증
    public void generationalHypothesis() {
        // 대부분의 객체는 짧은 생명주기를 가짐
        for (int i = 0; i < 1000000; i++) {
            String temp = "Temp" + i;  // 대부분 즉시 GC 대상
        }

        // 일부 객체만 오래 살아남음
        List<String> longLived = new ArrayList<>();
        longLived.add("Long lived object");
    }
}

Young Generation과 Old Generation

public class GenerationalGC {

    public void demonstrateGenerationalGC() {
        // 1. Eden에 객체 생성
        List<String> youngObjects = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            youngObjects.add("Young " + i);
        }

        // 2. Minor GC 발생 시뮬레이션
        // - Eden이 가득 찰 때 발생
        // - 살아있는 객체는 S0/S1으로 이동
        // - 여러 번 살아남은 객체는 Old Generation으로 이동

        // 3. Old Generation으로의 승격
        List<String> oldObjects = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            oldObjects.add("Old " + i);
        }

        // 4. Major GC 발생
        // - Old Generation이 가득 찰 때 발생
        // - Stop-the-World 시간이 길어짐
    }

    // GC 친화적인 객체 생성 패턴
    public void gcFriendlyPatterns() {
        // ✅ 객체 풀링
        ObjectPool<StringBuilder> pool = new ObjectPool<>(StringBuilder::new, 10);
        StringBuilder sb = pool.borrow();
        try {
            sb.append("Hello");
        } finally {
            pool.returnObject(sb);
        }

        // ✅ 불변 객체 활용
        String immutable = "Immutable String";

        // ✅ WeakReference 사용
        WeakReference<String> weakRef = new WeakReference<>("Weak String");
    }
}

다양한 GC 알고리즘 비교

Serial GC vs Parallel GC vs G1GC

public class GCAlgorithmComparison {

    // Serial GC: 단일 스레드, 작은 힙에 적합
    // JVM 옵션: -XX:+UseSerialGC
    public void serialGCCharacteristics() {
        // 특징:
        // - 단일 스레드로 GC 수행
        // - Stop-the-World 시간이 길지만 오버헤드 적음
        // - 작은 애플리케이션에 적합
    }

    // Parallel GC: 멀티 스레드, 중간 크기 힙에 적합
    // JVM 옵션: -XX:+UseParallelGC
    public void parallelGCCharacteristics() {
        // 특징:
        // - 멀티 스레드로 GC 수행
        // - 처리량(Throughput) 최적화
        // - CPU 코어가 많을 때 효과적
    }

    // G1GC: 대용량 힙, 낮은 지연시간 목표
    // JVM 옵션: -XX:+UseG1GC
    public void g1GCCharacteristics() {
        // 특징:
        // - 힙을 여러 Region으로 분할
        // - 예측 가능한 지연시간
        // - 8GB 이상 힙에 권장

        // G1GC 튜닝 옵션들
        // -XX:MaxGCPauseMillis=200  // 목표 지연시간
        // -XX:G1HeapRegionSize=16m  // Region 크기
        // -XX:G1NewSizePercent=20    // Young Gen 비율
    }

    // ZGC: 초저지연 GC (Java 15+)
    // JVM 옵션: -XX:+UseZGC
    public void zgcCharacteristics() {
        // 특징:
        // - 10ms 이하의 지연시간
        // - 대용량 힙 지원 (8TB까지)
        // - 실시간 애플리케이션에 적합
    }
}

실무 GC 튜닝 예제

public class GCTuningExample {

    // 프로덕션 환경 GC 설정 예제
    public static void productionGCConfig() {
        // G1GC 설정 (권장)
        String[] g1gcOptions = {
            "-XX:+UseG1GC",
            "-XX:MaxGCPauseMillis=200",
            "-XX:G1HeapRegionSize=16m",
            "-XX:G1NewSizePercent=20",
            "-XX:G1MaxNewSizePercent=40",
            "-XX:G1MixedGCCountTarget=8",
            "-XX:G1MixedGCLiveThresholdPercent=85",
            "-Xms2g",
            "-Xmx4g"
        };

        // GC 로그 설정
        String[] gcLogOptions = {
            "-Xlog:gc*:gc.log:time",
            "-XX:+PrintGCDetails",
            "-XX:+PrintGCTimeStamps",
            "-XX:+PrintGCApplicationStoppedTime"
        };
    }

    // GC 모니터링 코드
    public static void monitorGC() {
        List<GarbageCollectorMXBean> gcBeans = ManagementFactory.getGarbageCollectorMXBeans();

        for (GarbageCollectorMXBean gcBean : gcBeans) {
            System.out.println("GC Name: " + gcBean.getName());
            System.out.println("Collection Count: " + gcBean.getCollectionCount());
            System.out.println("Collection Time: " + gcBean.getCollectionTime() + "ms");

            // GC 통계 계산
            if (gcBean.getCollectionCount() > 0) {
                double avgTime = (double) gcBean.getCollectionTime() / gcBean.getCollectionCount();
                System.out.println("Average GC Time: " + avgTime + "ms");
            }
        }
    }

    // GC 성능 측정
    public static void measureGCPerformance() {
        long startTime = System.currentTimeMillis();

        // 대량 객체 생성으로 GC 유발
        List<Object> objects = new ArrayList<>();
        for (int i = 0; i < 1000000; i++) {
            objects.add(new Object());

            if (i % 100000 == 0) {
                long currentTime = System.currentTimeMillis();
                System.out.println("Created " + i + " objects in " + (currentTime - startTime) + "ms");
            }
        }

        // 강제 GC (권장하지 않음, 테스트용)
        System.gc();

        long endTime = System.currentTimeMillis();
        System.out.println("Total time: " + (endTime - startTime) + "ms");
    }
}

GC 최적화 실전 기법

메모리 효율적인 코딩 패턴

public class MemoryEfficientPatterns {

    // 1. 객체 재사용
    public void objectReuse() {
        // ❌ 매번 새 객체 생성
        for (int i = 0; i < 1000; i++) {
            StringBuilder sb = new StringBuilder();  // GC 압박
            sb.append("Item ").append(i);
        }

        // ✅ 객체 재사용
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 1000; i++) {
            sb.setLength(0);  // 초기화
            sb.append("Item ").append(i);
        }
    }

    // 2. 적절한 컬렉션 선택
    public void collectionSelection() {
        // ❌ 불필요한 LinkedList 사용
        List<String> list = new LinkedList<>();
        for (int i = 0; i < 100000; i++) {
            list.add("Item " + i);  // 메모리 오버헤드
        }

        // ✅ ArrayList 사용 (초기 용량 지정)
        List<String> optimizedList = new ArrayList<>(100000);
        for (int i = 0; i < 100000; i++) {
            optimizedList.add("Item " + i);
        }
    }

    // 3. WeakReference 활용
    public void weakReferenceUsage() {
        // 캐시에서 메모리 누수 방지
        Map<String, WeakReference<Object>> cache = new HashMap<>();

        cache.put("key1", new WeakReference<>(new Object()));
        cache.put("key2", new WeakReference<>(new Object()));

        // GC 발생 시 약한 참조는 자동 해제됨
        System.gc();

        // 캐시 정리
        cache.entrySet().removeIf(entry -> entry.getValue().get() == null);
    }

    // 4. 스트림 방식 처리
    public void streamProcessing() {
        // 대용량 파일 처리 시 메모리 효율적
        try (Stream<String> lines = Files.lines(Paths.get("large-file.txt"))) {
            long count = lines
                .filter(line -> line.contains("keyword"))
                .count();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

6.2 성능 최적화

Java 성능 측정과 프로파일링

JMH 벤치마킹: 정확한 성능 측정

JMH 설정과 기본 사용법

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(1)
public class StringConcatenationBenchmark {

    private String str1 = "Hello";
    private String str2 = "World";

    @Benchmark
    public String stringConcatenation() {
        return str1 + str2;  // String 연결
    }

    @Benchmark
    public String stringBuilder() {
        return new StringBuilder().append(str1).append(str2).toString();
    }

    @Benchmark
    public String stringBuilderPreSized() {
        return new StringBuilder(10).append(str1).append(str2).toString();
    }

    @Benchmark
    public String stringFormat() {
        return String.format("%s%s", str1, str2);
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
            .include(StringConcatenationBenchmark.class.getSimpleName())
            .build();

        new Runner(opt).run();
    }
}

실무 벤치마킹 예제들

@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@State(Scope.Benchmark)
public class CollectionPerformanceBenchmark {

    private List<Integer> arrayList;
    private List<Integer> linkedList;
    private Set<Integer> hashSet;
    private Set<Integer> treeSet;

    @Setup
    public void setup() {
        arrayList = new ArrayList<>();
        linkedList = new LinkedList<>();
        hashSet = new HashSet<>();
        treeSet = new TreeSet<>();

        // 테스트 데이터 준비
        for (int i = 0; i < 10000; i++) {
            arrayList.add(i);
            linkedList.add(i);
            hashSet.add(i);
            treeSet.add(i);
        }
    }

    @Benchmark
    public boolean arrayListContains() {
        return arrayList.contains(5000);
    }

    @Benchmark
    public boolean linkedListContains() {
        return linkedList.contains(5000);
    }

    @Benchmark
    public boolean hashSetContains() {
        return hashSet.contains(5000);
    }

    @Benchmark
    public boolean treeSetContains() {
        return treeSet.contains(5000);
    }

    @Benchmark
    public void arrayListAdd() {
        arrayList.add(10000);
    }

    @Benchmark
    public void linkedListAdd() {
        linkedList.add(10000);
    }
}

고급 JMH 기법

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
public class AdvancedBenchmarking {

    // 파라미터화된 벤치마크
    @Param({"100", "1000", "10000"})
    public int size;

    private List<String> testData;

    @Setup
    public void setup() {
        testData = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            testData.add("Item" + i);
        }
    }

    @Benchmark
    public List<String> streamFilter() {
        return testData.stream()
            .filter(s -> s.contains("5"))
            .collect(Collectors.toList());
    }

    @Benchmark
    public List<String> traditionalFilter() {
        List<String> result = new ArrayList<>();
        for (String item : testData) {
            if (item.contains("5")) {
                result.add(item);
            }
        }
        return result;
    }

    // 컴파일러 최적화 방지
    @Benchmark
    public int blackholeBenchmark(Blackhole bh) {
        int result = expensiveComputation();
        bh.consume(result);  // 결과를 소비하여 최적화 방지
        return result;
    }

    private int expensiveComputation() {
        int sum = 0;
        for (int i = 0; i < 1000; i++) {
            sum += i * i;
        }
        return sum;
    }

    // 벤치마크 그룹
    @Benchmark
    @Group("group1")
    public void writer() {
        // 쓰기 작업
    }

    @Benchmark
    @Group("group1")
    public void reader() {
        // 읽기 작업
    }
}

VisualVM과 JProfiler 활용

VisualVM 실전 사용법

public class VisualVMExample {

    // VisualVM에서 모니터링할 수 있는 코드
    public static void main(String[] args) throws InterruptedException {
        // 1. 메모리 사용량 증가
        List<Object> memoryConsumer = new ArrayList<>();

        // 2. CPU 사용량 증가
        for (int i = 0; i < 1000000; i++) {
            if (i % 100000 == 0) {
                System.out.println("Processing: " + i);
                Thread.sleep(100);  // VisualVM 연결 시간 확보
            }

            // 메모리 할당
            memoryConsumer.add(new Object());

            // CPU 집약적 작업
            calculatePrimes(1000);
        }

        // 3. 스레드 상태 확인
        createMultipleThreads();

        // 4. GC 모니터링
        System.gc();

        Thread.sleep(10000);  // VisualVM 분석 시간
    }

    private static void calculatePrimes(int limit) {
        for (int i = 2; i < limit; i++) {
            boolean isPrime = true;
            for (int j = 2; j * j <= i; j++) {
                if (i % j == 0) {
                    isPrime = false;
                    break;
                }
            }
        }
    }

    private static void createMultipleThreads() {
        for (int i = 0; i < 10; i++) {
            final int threadId = i;
            new Thread(() -> {
                try {
                    Thread.sleep(5000);
                    System.out.println("Thread " + threadId + " completed");
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }).start();
        }
    }
}

JProfiler 통합 예제

public class JProfilerIntegration {

    // JProfiler API 사용 (라이센스 필요)
    public void jprofilerIntegration() {
        // CPU 프로파일링
        // com.jprofiler.api.agent.Profiler.startCPURecording(true);

        // 메모리 프로파일링
        // com.jprofiler.api.agent.Profiler.startMemoryRecording(true);

        // 성능 집약적 작업
        performIntensiveOperation();

        // 프로파일링 중지
        // com.jprofiler.api.agent.Profiler.stopCPURecording();
        // com.jprofiler.api.agent.Profiler.stopMemoryRecording();
    }

    private void performIntensiveOperation() {
        // 복잡한 알고리즘 실행
        for (int i = 0; i < 1000000; i++) {
            // 메모리 할당
            String data = "Data" + i;

            // CPU 집약적 계산
            double result = Math.sqrt(i) * Math.sin(i);
        }
    }

    // 수동 프로파일링 포인트
    public void manualProfilingPoints() {
        long startTime = System.nanoTime();

        // 작업 수행
        doWork();

        long endTime = System.nanoTime();
        long duration = endTime - startTime;

        System.out.println("Operation took: " + duration + " nanoseconds");
    }

    private void doWork() {
        // 실제 작업
    }
}

병목 지점 찾기와 해결

성능 병목 진단 도구

public class BottleneckDetection {

    // 1. 메모리 병목 진단
    public void detectMemoryBottleneck() {
        MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();

        // 힙 메모리 사용량 모니터링
        MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
        long usedMemory = heapUsage.getUsed();
        long maxMemory = heapUsage.getMax();

        double usagePercent = (double) usedMemory / maxMemory * 100;

        if (usagePercent > 80) {
            System.out.println("Memory bottleneck detected: " + usagePercent + "%");

            // 해결책 제시
            suggestMemoryOptimizations();
        }
    }

    // 2. CPU 병목 진단
    public void detectCPUBottleneck() {
        OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();

        if (osBean instanceof com.sun.management.OperatingSystemMXBean) {
            com.sun.management.OperatingSystemMXBean sunOsBean = 
                (com.sun.management.OperatingSystemMXBean) osBean;

            double cpuUsage = sunOsBean.getProcessCpuLoad();

            if (cpuUsage > 0.8) {  // 80% 이상
                System.out.println("CPU bottleneck detected: " + (cpuUsage * 100) + "%");

                // 해결책 제시
                suggestCPUOptimizations();
            }
        }
    }

    // 3. GC 병목 진단
    public void detectGCBottleneck() {
        List<GarbageCollectorMXBean> gcBeans = ManagementFactory.getGarbageCollectorMXBeans();

        for (GarbageCollectorMXBean gcBean : gcBeans) {
            long collectionTime = gcBean.getCollectionTime();
            long collectionCount = gcBean.getCollectionCount();

            if (collectionCount > 0) {
                double avgCollectionTime = (double) collectionTime / collectionCount;

                if (avgCollectionTime > 100) {  // 100ms 이상
                    System.out.println("GC bottleneck detected in " + gcBean.getName() + 
                        ": " + avgCollectionTime + "ms average");

                    // 해결책 제시
                    suggestGCOptimizations(gcBean.getName());
                }
            }
        }
    }

    private void suggestMemoryOptimizations() {
        System.out.println("Memory optimization suggestions:");
        System.out.println("1. Increase heap size: -Xmx4g");
        System.out.println("2. Use G1GC: -XX:+UseG1GC");
        System.out.println("3. Optimize object creation patterns");
        System.out.println("4. Use object pooling for frequently created objects");
    }

    private void suggestCPUOptimizations() {
        System.out.println("CPU optimization suggestions:");
        System.out.println("1. Profile CPU-intensive methods");
        System.out.println("2. Optimize algorithms");
        System.out.println("3. Use parallel processing");
        System.out.println("4. Reduce unnecessary computations");
    }

    private void suggestGCOptimizations(String gcName) {
        System.out.println("GC optimization suggestions for " + gcName + ":");
        System.out.println("1. Tune GC parameters");
        System.out.println("2. Reduce object allocation rate");
        System.out.println("3. Use appropriate GC algorithm");
        System.out.println("4. Monitor GC logs");
    }
}

실전 성능 최적화 예제

public class PerformanceOptimizationExamples {

    // 1. 문자열 처리 최적화
    public String optimizedStringProcessing(List<String> items) {
        // ❌ 비효율적
        // String result = "";
        // for (String item : items) {
        //     result += item + ",";  // 매번 새 String 객체 생성
        // }

        // ✅ 효율적
        StringBuilder sb = new StringBuilder(items.size() * 10);  // 예상 크기로 초기화
        for (String item : items) {
            sb.append(item).append(",");
        }
        return sb.toString();
    }

    // 2. 컬렉션 최적화
    public List<String> optimizedCollectionProcessing(List<String> input) {
        // ❌ 비효율적
        // List<String> result = new ArrayList<>();
        // for (String item : input) {
        //     if (item.length() > 5) {
        //         result.add(item.toUpperCase());
        //     }
        // }

        // ✅ 효율적
        return input.stream()
            .filter(item -> item.length() > 5)
            .map(String::toUpperCase)
            .collect(Collectors.toList());
    }

    // 3. 캐싱 최적화
    private final Map<String, String> cache = new ConcurrentHashMap<>();

    public String optimizedCaching(String key) {
        // ❌ 비효율적
        // if (cache.containsKey(key)) {
        //     return cache.get(key);
        // }
        // String value = expensiveOperation(key);
        // cache.put(key, value);
        // return value;

        // ✅ 효율적
        return cache.computeIfAbsent(key, this::expensiveOperation);
    }

    private String expensiveOperation(String key) {
        // 비용이 큰 연산 시뮬레이션
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return "Processed: " + key;
    }

    // 4. 메모리 효율적인 파일 처리
    public void optimizedFileProcessing(String filePath) throws IOException {
        // ❌ 비효율적 (전체 파일을 메모리에 로드)
        // List<String> lines = Files.readAllLines(Paths.get(filePath));
        // for (String line : lines) {
        //     processLine(line);
        // }

        // ✅ 효율적 (스트림 방식)
        try (Stream<String> lines = Files.lines(Paths.get(filePath))) {
            lines.forEach(this::processLine);
        }
    }

    private void processLine(String line) {
        // 라인 처리 로직
        System.out.println("Processing: " + line);
    }
}

메모리 누수 탐지와 해결

메모리 누수의 주요 원인들

실무에서 마주한 메모리 누수 케이스들

public class MemoryLeakExamples {

    // Case 1: 정적 컬렉션에 객체 누적
    private static final List<Object> STATIC_CACHE = new ArrayList<>();

    public void staticCollectionLeak() {
        // ❌ 문제: 정적 컬렉션에 계속 추가만 함
        for (int i = 0; i < 1000000; i++) {
            STATIC_CACHE.add(new Object());
        }
        // 해결책: TTL 구현 또는 WeakReference 사용
    }

    // Case 2: 리스너 등록 후 해제하지 않음
    private final List<EventListener> listeners = new ArrayList<>();

    public void listenerLeak() {
        // ❌ 문제: 리스너 등록 후 해제하지 않음
        EventListener listener = new EventListener() {
            @Override
            public void onEvent(String event) {
                System.out.println("Event: " + event);
            }
        };

        listeners.add(listener);
        // 해결책: removeListener() 메서드 제공
    }

    // Case 3: 내부 클래스의 외부 참조
    public void innerClassLeak() {
        final String largeString = "Very large string...";  // 외부 변수

        // ❌ 문제: 내부 클래스가 외부 변수를 참조
        Runnable task = new Runnable() {
            @Override
            public void run() {
                System.out.println(largeString);  // 외부 참조로 인한 누수
            }
        };

        // 해결책: 필요한 데이터만 복사
    }

    // Case 4: ThreadLocal 사용 후 정리하지 않음
    private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public void threadLocalLeak() {
        // ❌ 문제: ThreadLocal 사용 후 정리하지 않음
        threadLocal.set("Thread specific data");

        // 해결책: finally 블록에서 정리
        try {
            // 작업 수행
        } finally {
            threadLocal.remove();  // 중요!
        }
    }
}

메모리 누수 방지 패턴들

public class MemoryLeakPrevention {

    // 1. WeakReference를 활용한 캐싱
    public static class WeakCache<K, V> {
        private final Map<K, WeakReference<V>> cache = new ConcurrentHashMap<>();
        private final ReferenceQueue<V> queue = new ReferenceQueue<>();

        public void put(K key, V value) {
            // 약한 참조로 저장
            cache.put(key, new WeakReference<>(value, queue));

            // 정리된 참조들 제거
            cleanUp();
        }

        public V get(K key) {
            WeakReference<V> ref = cache.get(key);
            if (ref != null) {
                V value = ref.get();
                if (value == null) {
                    cache.remove(key);  // GC된 객체 제거
                }
                return value;
            }
            return null;
        }

        private void cleanUp() {
            Reference<? extends V> ref;
            while ((ref = queue.poll()) != null) {
                cache.entrySet().removeIf(entry -> entry.getValue() == ref);
            }
        }
    }

    // 2. TTL(Time To Live) 기반 캐시
    public static class TTLCache<K, V> {
        private final Map<K, CacheEntry<V>> cache = new ConcurrentHashMap<>();
        private final long ttlMillis;

        public TTLCache(long ttlMillis) {
            this.ttlMillis = ttlMillis;
        }

        public void put(K key, V value) {
            cache.put(key, new CacheEntry<>(value, System.currentTimeMillis()));
        }

        public V get(K key) {
            CacheEntry<V> entry = cache.get(key);
            if (entry != null) {
                if (System.currentTimeMillis() - entry.timestamp < ttlMillis) {
                    return entry.value;
                } else {
                    cache.remove(key);  // 만료된 항목 제거
                }
            }
            return null;
        }

        private static class CacheEntry<V> {
            final V value;
            final long timestamp;

            CacheEntry(V value, long timestamp) {
                this.value = value;
                this.timestamp = timestamp;
            }
        }
    }

    // 3. 자동 리소스 관리
    public static class AutoCloseableResource implements AutoCloseable {
        private boolean closed = false;

        public void doWork() {
            if (closed) {
                throw new IllegalStateException("Resource is closed");
            }
            // 작업 수행
        }

        @Override
        public void close() {
            if (!closed) {
                // 리소스 정리
                closed = true;
                System.out.println("Resource closed");
            }
        }
    }

    // 사용 예제
    public void useAutoCloseableResource() {
        try (AutoCloseableResource resource = new AutoCloseableResource()) {
            resource.doWork();
        } // 자동으로 close() 호출
    }
}

Heap Dump 분석과 해결

Heap Dump 생성과 분석

public class HeapDumpAnalysis {

    // Heap Dump 생성
    public void generateHeapDump() {
        try {
            // 1. 프로그래밍 방식으로 Heap Dump 생성
            HotSpotDiagnosticMXBean diagnosticBean = 
                ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);

            String fileName = "heapdump_" + System.currentTimeMillis() + ".hprof";
            diagnosticBean.dumpHeap(fileName, true);

            System.out.println("Heap dump created: " + fileName);

        } catch (IOException e) {
            System.err.println("Failed to create heap dump: " + e.getMessage());
        }
    }

    // 메모리 사용량 분석
    public void analyzeMemoryUsage() {
        MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();

        // Heap 메모리 분석
        MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
        System.out.println("Heap Memory Analysis:");
        System.out.println("  Used: " + formatBytes(heapUsage.getUsed()));
        System.out.println("  Committed: " + formatBytes(heapUsage.getCommitted()));
        System.out.println("  Max: " + formatBytes(heapUsage.getMax()));

        // Non-Heap 메모리 분석
        MemoryUsage nonHeapUsage = memoryBean.getNonHeapMemoryUsage();
        System.out.println("Non-Heap Memory Analysis:");
        System.out.println("  Used: " + formatBytes(nonHeapUsage.getUsed()));
        System.out.println("  Committed: " + formatBytes(nonHeapUsage.getCommitted()));
        System.out.println("  Max: " + formatBytes(nonHeapUsage.getMax()));
    }

    // 클래스별 메모리 사용량 분석
    public void analyzeClassMemoryUsage() {
        List<MemoryPoolMXBean> memoryPools = ManagementFactory.getMemoryPoolMXBeans();

        for (MemoryPoolMXBean pool : memoryPools) {
            MemoryUsage usage = pool.getUsage();
            if (usage != null) {
                System.out.println("Memory Pool: " + pool.getName());
                System.out.println("  Used: " + formatBytes(usage.getUsed()));
                System.out.println("  Max: " + formatBytes(usage.getMax()));
            }
        }
    }

    private String formatBytes(long bytes) {
        if (bytes < 1024) return bytes + " B";
        if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0);
        if (bytes < 1024 * 1024 * 1024) return String.format("%.1f MB", bytes / (1024.0 * 1024.0));
        return String.format("%.1f GB", bytes / (1024.0 * 1024.0 * 1024.0));
    }
}

메모리 누수 탐지 도구

public class MemoryLeakDetector {

    private static final Logger logger = LoggerFactory.getLogger(MemoryLeakDetector.class);

    // 메모리 사용량 모니터링
    public void startMemoryMonitoring() {
        Timer timer = new Timer();
        timer.scheduleAtFixedRate(new TimerTask() {
            private long previousUsedMemory = 0;

            @Override
            public void run() {
                MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
                MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
                long currentUsedMemory = heapUsage.getUsed();

                // 메모리 사용량이 계속 증가하는지 확인
                if (previousUsedMemory > 0) {
                    long memoryIncrease = currentUsedMemory - previousUsedMemory;

                    if (memoryIncrease > 10 * 1024 * 1024) {  // 10MB 이상 증가
                        logger.warn("Significant memory increase detected: {} MB", 
                            memoryIncrease / (1024 * 1024));

                        // 메모리 누수 가능성 경고
                        checkForMemoryLeaks();
                    }
                }

                previousUsedMemory = currentUsedMemory;
            }
        }, 0, 30000);  // 30초마다 체크
    }

    private void checkForMemoryLeaks() {
        // 1. GC 실행
        System.gc();

        // 2. 메모리 사용량 재확인
        MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
        MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();

        double usagePercent = (double) heapUsage.getUsed() / heapUsage.getMax() * 100;

        if (usagePercent > 90) {
            logger.error("High memory usage detected: {:.1f}%", usagePercent);

            // 3. Heap Dump 생성 제안
            logger.info("Consider generating heap dump for analysis");

            // 4. 메모리 누수 가능성 알림
            sendMemoryLeakAlert(usagePercent);
        }
    }

    private void sendMemoryLeakAlert(double usagePercent) {
        // 실제 환경에서는 알림 시스템으로 전송
        logger.error("MEMORY LEAK ALERT: Memory usage at {:.1f}%", usagePercent);

        // 알림 발송 로직
        // - 이메일 알림
        // - 슬랙 알림
        // - 모니터링 시스템 알림
    }

    // 메모리 누수 패턴 감지
    public void detectMemoryLeakPatterns() {
        // 1. 정적 컬렉션 크기 확인
        checkStaticCollections();

        // 2. ThreadLocal 사용량 확인
        checkThreadLocalUsage();

        // 3. 캐시 크기 확인
        checkCacheSizes();
    }

    private void checkStaticCollections() {
        // 리플렉션을 사용하여 정적 컬렉션 크기 확인
        // (실제 구현에서는 애플리케이션별로 커스터마이징 필요)
    }

    private void checkThreadLocalUsage() {
        // ThreadLocal 사용량 모니터링
        // (실제 구현에서는 애플리케이션별로 커스터마이징 필요)
    }

    private void checkCacheSizes() {
        // 캐시 크기 모니터링
        // (실제 구현에서는 애플리케이션별로 커스터마이징 필요)
    }
}

약한 참조(WeakReference) 활용

public class WeakReferenceExamples {

    // 1. WeakHashMap 활용
    public void weakHashMapExample() {
        // WeakHashMap: 키가 약한 참조로 저장됨
        Map<String, String> weakMap = new WeakHashMap<>();

        String key = new String("key");
        weakMap.put(key, "value");

        System.out.println("Before GC: " + weakMap.size());  // 1

        key = null;  // 강한 참조 제거
        System.gc();

        System.out.println("After GC: " + weakMap.size());  // 0 (키가 GC됨)
    }

    // 2. WeakReference 직접 사용
    public void weakReferenceExample() {
        String strongRef = new String("Strong Reference");
        WeakReference<String> weakRef = new WeakReference<>(strongRef);

        System.out.println("Strong ref: " + strongRef);
        System.out.println("Weak ref: " + weakRef.get());

        strongRef = null;  // 강한 참조 제거
        System.gc();

        System.out.println("After GC - Weak ref: " + weakRef.get());  // null
    }

    // 3. ReferenceQueue 활용
    public void referenceQueueExample() {
        ReferenceQueue<String> queue = new ReferenceQueue<>();
        WeakReference<String> weakRef = new WeakReference<>(
            new String("Referenced Object"), queue);

        String strongRef = weakRef.get();
        System.out.println("Before GC: " + strongRef);

        strongRef = null;
        System.gc();

        // ReferenceQueue에서 정리된 참조 확인
        try {
            Reference<? extends String> clearedRef = queue.remove(1000);
            if (clearedRef != null) {
                System.out.println("Cleared reference: " + clearedRef);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    // 4. 실전 활용: 이미지 캐시
    public static class ImageCache {
        private final Map<String, WeakReference<BufferedImage>> cache = new HashMap<>();

        public BufferedImage getImage(String path) {
            WeakReference<BufferedImage> ref = cache.get(path);
            if (ref != null) {
                BufferedImage image = ref.get();
                if (image != null) {
                    return image;  // 캐시 히트
                } else {
                    cache.remove(path);  // GC된 이미지 제거
                }
            }

            // 이미지 로드
            BufferedImage image = loadImage(path);
            cache.put(path, new WeakReference<>(image));
            return image;
        }

        private BufferedImage loadImage(String path) {
            // 이미지 로드 로직
            return new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
        }

        public void clearCache() {
            cache.clear();
        }
    }
}

마치며

이번 글에서는 JVM의 내부 동작 원리와 Java 애플리케이션 성능 최적화에 대해 실무 관점에서 다뤄보았습니다.

핵심 요약:

  1. JVM 메모리 구조: Heap, Stack, Metaspace의 역할과 특성 이해
  2. 가비지 컬렉션: 다양한 GC 알고리즘과 튜닝 방법
  3. 성능 측정: JMH, VisualVM, JProfiler를 활용한 정확한 측정
  4. 메모리 누수: 원인 분석과 방지 패턴
  5. 실전 최적화: 프로덕션 환경에서 적용 가능한 기법들

실무에서는 단순히 이론을 아는 것을 넘어, 실제 문제를 진단하고 해결하는 능력이 중요합니다.

다음 단계에서는:

  • 디자인 패턴과 아키텍처 설계
  • 최신 Java 기능 (Java 17+)
  • 실전 프로젝트와 베스트 프랙티스

를 다룰 예정입니다.

반응형