728x90

배경

실행중이던 서버가 갑자기 다운이 되었다. 로그를 확인해보니 java.lang.OutOfMemoryError: GC overhead limit exceeded 에러가 발생했다. 서버를 재기동해서 그냥 넘어가기는 했지만 다시 발생할 수 있는 문제이기 때문에 원인과 해결 방법을 정리해본다.

 

개발 환경

  • Java 1.8
  • Spring 5.3
  • Oracle 11g
  • MacOS

 

원인

GC는 더 이상 참조되지 않는 객체를 제거해주는 작업을 해주기 때문에 메모리 사용을 최적화하는데 도움을 준다. 하지만 GC가 너무 오랫동안 수행되면 실제 작업보다 많은 CPU를 사용하게 되어서 프로그램 실행이 느려지거나 중단될 수 있다.

OOM은 JVM이 GC를 수행하는데 너무 많은 시간을 소비해서 애플리케이션을 실행할 수 없게 되는 상황에 발생하는 에러이다. Java 문서에는 아래와 같이 나와있다.

CPU의 98% 이상이 GC에 소비되었지만 Heap 메모리의 2% 미만만 복구되는 경우 발생한다.

 

해결 방법 

Heap Size 늘리기

JVM 옵션을 넣어 Heap Size를 늘릴 수 있다. 하지만 애플리케이션 코드상에 메모리 누수가 있는 경우 이 방법이 해결책이 되지는 않는다.

오류가 발생하는 시점을 잠깐 연기해줄 뿐이다. 따라서 애플리케이션의 메모리 사용량을 분석해 메모리 누수의 원인을 찾아내는 것이 중요하다. 

java -Xmx2048m com.xyz.TheClassName

 

Heap Dump 분석

이 문제를 해결하기 위해서는 메모리 누수가 있는 코드를 찾아내야 한다. 

힙 덤프(Heap Dump)란 힙 영역에서 활성화 되어 있는 스냅샷이 저장된 파일이다. 주소, 타입, 클래스 이름, 크기 등 인스턴스에 대한 자세한 정보와 인스턴스에 다른 객체에 대한 참조 여부를 알 수 있다.

이제 Heap Dump 파일을 분석해 메모리 누수 원인을 찾아 해결해보자

 

1. JVM에 아래 옵션을 추가해줘야 OOM이 발생했을 때 힙 덤프 파일이 생성된다.

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump

 

2. Eclipse MAT 설치

설치 방법은 아래 링크에 정리해두었다. (Mac OS 기준)

 

Eclipse Memory Analyzer - Eclipse MAT 설치 방법 (MacOS)

배경GC 에러로 인해 서버가 다운되어서 원인을 알아보기 위해 dump 파일을 분석해볼 필요가 있었다.  Java - java.lang.OutOfMemoryError:GC overhead limit exceeded 원인실행중이던 서버가 갑자기 다운이 되었다

green-bin.tistory.com

 

3. Heap Dump 분석

Eclipse MAT으로 Heap Dump 파일을 분석해보자. MAT은 메모리 누수를 자동으로 탐지하는 기능을 제공해주고 있다.

[Report > Leak Suspects 실행]을 통해 메모리 누수가 의심되는 객체들을 자동으로 탐지하고 누수 가능성이 높은 곳의 경로를 알려준다.

 

Leak Suspects Report를 확인해보니 HashMap 객체에서 메모리 누수가 의심된다고 보고되었다. 대량의 데이터를 Map이나 ArrayList같은 자료구조에 저장할 때 메모리 누수가 많이 발생한다고 한다.  

경로를 추적해보니 회원 정보 수정 로직에서 누수가 발생하고 있었고, 로직에서 필수 검색 조건이 누락되어 약 112만 개의 전체 회원 정보가 조회되는 문제가 확인 되었다. 필수 조건 검증 로직을 추가해서 해결할 수 있도록 해결 방안 문서를 작성해두었다.

 

예방하기

OOM은 보통 무분별하게 객체를 생성하거나 객체가 참조되고 있는 상태(Reachable 상태)를 유지할 때 발생하는게 주된 원인이다.

이런 문제를 예방하기 위해서는 코드를 작성할 때 메모리 누수를 고려해서 작성하는 것이 가장 중요하다.

 

사용한 리소스 잘 반납하기

DB 커넥트, FileStream 등 리소스를 사용한 후에 반드시 close()메서드로 닫아주어야 한다.

Java7 이상이라면 try-with-resources 구문을 사용하면 자동으로 닫아준다.

try (FileReader fr = new FileReader(path);
     BufferedReader br = new BufferedReader(fr)) {
    return br.readLine();
}

 

Stream 사용하기

Stream은 파이프라이닝 기법을 통해 여러 연산을 연결하여 진행한다.

이러한 특징 때문에 중간 연산들을 저장하지 않아 메모리 사용을 최적화 할 수 있다.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// for문 사용
int sum = 0;
for(int i = 0; i < numbers.length(); i++) {
	int n = numbers[i];
	if(n % 2 == 0) {
    	sum += n * 2;
    }
}

// Stream 사용
int sum = numbers.stream()
                 .filter(n -> n % 2 == 0)
                 .map(n -> n * 2)
                 .reduce(0, Integer::sum);

 

StrinbBuilder 사용하기

문자열을 연결할 때 String 객체를 생성해서 연결하는 대신 StringBuilder를 사용해서 메모리 사용을 최적화할 수 있다.

// 나쁜 예
String result = "";
for (int i = 0; i < 100; i++) {
    result += i; // 매번 새로운 String 객체 생성
}

// 좋은 예
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100; i++) {
    sb.append(i);
}
String result = sb.toString();

 

GC 로그 남기기

GC 이력을 로그 파일로 남겨두자. 아래는 Java 8 기준 옵션이다. Java 버전별로 설정 방식이 조금 다르니 찾아보자

-XX:+PrintGC -Xloggc:/path/to/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps
  • -XX:+PrintGC: 간단한 GC 정보 출력
  • -XX:+PrintGCDetails: 상세한 GC 정보 출력
  • -XX:+PrintGCDateStamps: GC 이벤트에 날짜와 시간 추가
  • -Xloggc:<파일경로>: GC 로그를 지정된 파일에 저장

 

GC 튜닝하기

GC 튜닝은 복잡하고 신중한 접근이 필요한 작업이다. 대부분 코드 리팩토링을 통해서 메모리 성능을 개선할 수 있고, 이 방법이 GC 튜닝보다 더 안전하고 효과적인 방법이다.  GC 튜닝은 나중에 학습할 예정이다.

 

마무리

실무에서 OOM 에러를 만나보기 힘들다는 이야기를 들었었는데, 난 굉장히 빨리 만나본 것 같다. Eclipse MAT의 용도와 사용법을 알게 됐고, 직접 heap dump 분석을 통해 원인을 찾아내는 귀중한 경험을 했다.

메모리 누수의 원인이 된 코드를 수정하지 않고 서버 재기동만으로 넘어간 부분이 마음에 걸린다. 충분히 재발할 수 있는 문제이기에 원인과 해결 방안에 대해 잘 문서화해둬야겠다.

 

참조

https://sematext.com/blog/java-garbage-collection-tuning/#parallel-garbage-collector

https://docs.oracle.com/en/java/javase/11/gctuning/parallel-collector1.html#GUID-DCDD6E46-0406-41D1-AB49-FB96A50EB9CE

https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/parallel.html#parallel_collector_excessive_gc

https://incheol-jung.gitbook.io/docs/q-and-a/java/heap-dump-feat.-oom

https://www.baeldung.com/java-gc-overhead-limit-exceeded

https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/memleaks002.html

728x90
Cold Bean