CS 정리

STW, Stop the world 제어하기

문쿼리 2025. 4. 13. 01:57

GC에 대해 공부하다보니, 어떻게 좋은 성능으로 최적화 할 수 있을까? 감지 방법과 해결 방법에 대해 알아보자

 

!감지 하기

GC 성능이 나쁜 상황은 다음과 같다.

  • GC 시간이 길다/ Throughput이 낮다
    : GC Pause Time이 100ms 이상 반복적으로 발생, GC Time 비율이 전체 10% 이상
  • Minor GC/ Major GC/ Full GC가 잦다
    : Minor GC가 초에 수 회 반복, Full GC 가 분단위로 발생
  • Promotion Failure 발생
    : Full GC 발생 가능성 상승
  • CPU 사용률이 GC에 쏠림
  • OutOfMemoryError 발생
    = 장애상황

어떻게 알 수 있을까?
로그 분석 또는 모니터링 도구 활용할 것 ( VisualVM, GCViewer, Prometheus + Grafana 등)

// GC 로그 보기

// java 8 이하
-XX:+PrintGCDetails
2025-04-13T10:00:00.123+0900: 0.456: [GC [PSYoungGen: 5120K->1024K(9216K)] 10240K->6144K(19456K), 0.0034567 secs]
// 5초 이상 걸렸다면 튜닝 필요
[Full GC (Ergonomics)  2G->500M(2G), 5.1234567 secs]

// java 9 이상
-Xlog:gc*			
[2025-04-11T14:00:00.123+0900][info][gc] GC(0) Pause Young (Normal) 256M->128M(512M) 3.456ms
[2025-04-11T14:00:01.456+0900][info][gc] GC(1) Concurrent Cycle  -- 20ms

// STW 발생, 1ms 이하인지 확인
[2025-04-11T10:00:00.123+0000][info][gc] GC(0) Pause Mark Start 0.123ms	
// GC Root 참조 추적
[2025-04-11T10:00:00.124+0000][info][gc] GC(0) Concurrent Mark
// STW 발생, 1ms 이하인지 확인, GC가 객체 이동 시작
[2025-04-11T10:00:00.234+0000][info][gc] GC(0) Pause Relocate Start 0.876ms
// 객체를 새 주소로 복사
[2025-04-11T10:00:00.235+0000][info][gc] GC(0) Concurrent Relocate

// ZGC 등 초저지연 GC인데 5초 이상 걸렸다면, 튜닝 필요
[Full GC (Allocation Failure)  4G->1G(4G), 5.543 sec]

 


!실전 :STW가 자주 발생하지 않도록 최적화하기

Stop the world는 java 성능저하의 핵심.

 

1. GC 알고리즘 최적화
저지연 GC활용. 최소 G1이상 가능하면 ZGC/Shenandoah로 전환 할 것.

 

2. JVM 설정 및 GC 설정 최적화

  • Young / Old 비율 조절
    : Young이 너무 작으면 Minor GC가 빈번하고,
    Old가 너무 작으면 승격 실패로 Full GC 발생 가능성이 높아짐
    GC 로그를 통해 튜닝할 것.
  • GC 스레드 수 조절
    : CPU 코어 수에 맞게 적절히 조절.

3. Java 코드 최적화

객체 생명주기 짧게하기, 큰 객체 피하기, 메모리 풀 활용하기, System.gc() 억제하기

  • . 객체 생명 주기를 짧게하기
    : Young에서 minor GC를 통하면 Eden에서 제거됨으로 승격이 줄어듦
    : 약한 참조 객체 활용 (SoftReference, WeakReference, PhantomReference 등)
    public class BadExample {
        private static List<User> cachedUsers = new ArrayList<>();
    
        public static void loadUser() {
            User user = new User("Alien");
            cachedUsers.add(user); // GC 대상 안 됨 → 오래 살아남음 → Old Gen으로
        }
    }
    
    public class BadExampleSolution {
    	// 메모리 부족하면 지워도 되는 객체
    	private static List<SoftReference<User>> softCachedUsers = new ArrayList<>();
    	// 참조가 사라지면 지워지는 객체
    	private static List<WeakReference<User>> weakCachedUsers = new ArrayList<>();
        
        public static void main(String[] args) {
            WeakReference<User> weakRef;
    
            {
                // user는 이 블록 안에서만 살아있음
                User user = new User("Alien");
                weakRef = new WeakReference<>(user);
    
                // strong reference 유지 중 → 아직 GC 안됨
                System.out.println("Before null: " + weakRef.get());
            }
    
            // user 변수는 이제 범위를 벗어남 → GC 입장에서는 WeakReference만 남음
            System.gc();
        }
    
    }
    
    public void GoodExample() {
        // 이 객체는 지역 변수로만 사용됨 → 메서드 종료 시 사라짐 → Young Gen에서 GC 처리
        User user = new User("Alien");
        user.doSomething();
    }


  • 큰 객체 피하기
    : 큰 객체는 바로 Old로 갈 수 있기 때문에 STW의 원인이 됨
    public class BadExample {
    	public void process(byte[] source) {
    		// 매번 새로 생성됨으로 큰 객체에 대한 부담 증가
    		byte[] localBuffer = new byte[10 * 1024 * 1024];
    		System.arraycopy(source, 0, localBuffer, 0, source.length);
    	}
    }
    
    public class GoodExample {
    	// 전역 변수로 설정한 버퍼를 통해 재사용
    	private static final byte[] reusableBuffer = new byte[10 * 1024 * 1024];
    	public void process(byte[] source) {
    		System.arraycopy(source, 0, reusableBuffer, 0, source.length);
    	}
    }
    
    public class GoodExampleMultipleThread {
    	// ThreadLocal 전역 변수로 설정한 버퍼를 통해 재사용
    	private static final ThreadLocal<byte[]> localBuffer = ThreadLocal.withInitial(() -> new byte[10 * 1024 * 1024]);
        
    	public void multipleThreadProcess(byte[] source) {
    		byte[] threadBuffer = localBuffer.get(); // 각 스레드마다 독립적인 버퍼
    		System.arraycopy(source, 0, threadBuffer, 0, source.length);
    	}
    }
  • 메모리 풀 사용하기
    : 반복되는 객체 할당/해제를 Pool로 관리하여 GC 대상을 줄인다
    public class BadExample {
        public void process() {
            // 매번 새로 버퍼를 할당함 → GC 부담 증가
            ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
            
            // 작업 처리
            buffer.put("data".getBytes());
    
            // 버퍼는 GC 대상 → 메모리 낭비 발생
        }
    }
    
    public class GoodExample {
        // 전역 공유 버퍼 풀
        private static final Queue<ByteBuffer> pool = new ArrayDeque<>();
    
        public void process() {
            ByteBuffer buffer = pool.poll();
            if (buffer == null) {
                buffer = ByteBuffer.allocateDirect(1024);
            }
    
            // 작업 처리
            buffer.put("data".getBytes());
    
            // 사용 후 재사용을 위해 반환
            buffer.clear();
            pool.offer(buffer);
        }
    }
    
    public class GoodExampleMultipleThread {
        // 스레드마다 독립된 버퍼 보유
        private static final ThreadLocal<ByteBuffer> threadLocalBuffer =
            ThreadLocal.withInitial(() -> ByteBuffer.allocateDirect(1024));
    
        public void process() {
            ByteBuffer buffer = threadLocalBuffer.get();
    
            // 작업 처리
            buffer.clear();
            buffer.put("data".getBytes());
    
            // 반환 불필요 - ThreadLocal 버퍼는 스레드가 계속 유지
        }
    }


  • 메모리 풀 클래스 구현하기
    // 메모리 풀 방식을 이용한 BufferPool class 구현하기
    public class ByteBufferPool {
        private final Queue<ByteBuffer> pool = new ConcurrentLinkedQueue<>();
        private final int bufferSize;
    
        public ByteBufferPool(int bufferSize) {
            this.bufferSize = bufferSize;
        }
    
        public ByteBuffer acquire() {
            ByteBuffer buffer = pool.poll();
            return (buffer != null) ? buffer : ByteBuffer.allocateDirect(bufferSize);
        }
    
        public void release(ByteBuffer buffer) {
            buffer.clear(); // 상태 초기화
            pool.offer(buffer);
        }
    }
    
    public class GoodExample {
        private static final ByteBufferPool pool = new ByteBufferPool(1024);
    
        public void process(byte[] source) {
            ByteBuffer buffer = pool.acquire();
            buffer.put(source);
            pool.release(buffer);
        }
    }

  • System.gc() 억제하기
    외부 라이브러리들에 System.gc()를 호출하는 경우가 더러 있음 (SLF4J, Netty 등),
    운영 환경에서 아래 JVM 옵션을 설정해줄 것
    -XX:+DisableExplicitGC
    java -XX:+PrintGC -XX:+DisableExplicitGC GcTest	// GC 테스트 시 로그 보기

'CS 정리' 카테고리의 다른 글

Java 대용량 데이터 처리(1) - Cursor 방식  (0) 2025.04.20
Java 대용량 데이터 처리(0)  (0) 2025.04.19
메세지 큐, Message Queue  (0) 2025.04.13
메모리 단편화, Fragmentation  (0) 2025.04.12
GC, Garbage Collection  (0) 2025.04.12