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 |