Post

OutOfMemoryError; Java Heap Space

OOM 에러 발생 문제상황

 api 중 하나가, 로그성 테이블의 데이터를 읽어와서 엑셀파일(byte array)을 만들어서 반환해주는 기능을 갖고 있었다. 처음에는 요구사항이 생겨서 큰 생각없이 빨리 만들어주었던 api 인데, 이 api 로 인해서 OutOfMemoryError : Java Heap Space 를 맞이하게 되었다. 해당 api의 코드를 디버깅 해 본 결과, 당연히 메모리가 부족할 수 밖에 없는 로직이었다.
 DB 테이블의 로우 갯수가 4만개, 32만개나 되는데, (1)그 데이터를 모두 메모리에 올린 다음, 그 데이터로 (2)엑셀 sheet를 만들고, 그 엑셀 sheet를 Client에서 받아 볼 수 있도록, (3)byte array로 변경하면서 3중으로 메모리를 먹고 있었던 것이다. 그래서 한 번 api 호출 시, Heap Size 가 600Mb 나 증가하는 것을 확인하였다..

문제상황 분석 방법

이러한 OOM 에러를 맞이했을 때 꼭 봐야할 것들을 나열하면 아래와 같다. 내 경우에는, 분석해보니 MemoryLeak의 문제는 아니였다.

  1. GC log
  2. Heap Usage
  3. 메모리 덤프를 이용한 Memory leak 확인

Java의 메모리 영역, Heap 메모리 구조, GC에 대한 정리

Java 메모리 영역(Runtime Data Area)

Java Runtime Data Area

  • PC Register: 스레드별로 보유하고 있는 스레드가 어떤 부분을 어떤 명령으로 실행해야 할 지에 대한 기록을 하는 부분. 현재 수행 중인 JVM 명령의 주소를 가짐
  • JVM 스택 : 스레드별 보유. 이 영역에는 지역 변수와 부분 결과를 저장하며, 메서드 호출 및 리턴에 관련된 정보가 보관
  • Heap : 모든 클래스의 인스턴스와 배열이 할당. JVM이 시작될 때 생성되며 가비지 컬렉터에 의해 관리됨
  • 메서드 영역(Method Area = Class Area = Static Area) : 모든 JVM의 스레드가 공유하며, 각 클래스의 구조 정보를 저장하는 영역.
    런타임 상수 풀, 필드, 메서드 데이터, 메서드와 생성자의 코드, 클래스와 인터페이스 인스턴스의 초기화를 위한 특수 메서드(special method)들에 대한 정보들이 들어 있음
  • Native Method Stack : 자바 언어 이외의 네이티브 언어를 호출할 경우 타 언어의 스택 정보를 여기에 저장함.

Heap 메모리 구조

Java Heap Memory Structure 일반적으로 우리가 개발하는 객체들은 힙 영역에 저장되며, 이 힙 영역은 Eden, Survivor, Tenured, Permanent 영역으로 나뉜다.(JDK 8 부터는 permanent 영역 사라지고 일부가 meta space 영역으로 변경됨)

  • New/Young Generation
    • Eden : 객체들이 최초로 생성되는 공간
    • Survivor 0/1 : Eden에서 참조되는 객체들이 저장되는 공간
  • Old Generation : New/Young Generation에서 일정시간 참조되고 있는 살아남은 객체들이 저장되는 공간
  • Permenant Generation
    • 생성된 객체의 주소값이 저장된 공간
    • Reflection을 사용하여 동적으로 클래스가 로딩되는 경우에 사용됨. 내부적으로 Reflection 기능을 자주 사용하면 이 영역에 대한 고려가 필요

Garbage Collection

  • Minor GC : New/Young Generation에서 일어나는 GC
    • 최초에 Eden에 객체가 생성됨
    • Eden 영역에 객체가 가득 차게 되면 첫 번째 GC(Minor GC)가 일어나고, Survivor0 영역에 Eden 영역의 메모리를 복사, 그 후 Survivor0 제외한 다른 영역 객체 제거
    • Eden과 Survivor0 도 가득차면, 그 중에 참조되고 있는 객체를 Survivor1으로 복사후 Survivor1를 제외한 다른 영역의 객체를 제거
    • 위 과정 중 일정 횟수 이상 참조되고 있는 객체들을 Survivor1에서 Old로 이동
    • 위 과정을 계속 반복하며, Survivor1 까지 꽉 차기 전에 계속해서 Old로 비움
  • Major GC(Full GC): Old 영역에서 일어나는 GC
    • Old 영역에 있는 모든 객체들을 검사하며 참조되고 있는지 확인
    • 참조되지 않은 객체들을 모아 한번에 제거
      • Minor GC 보다 시간이 훨씬 많이 걸리고, 실행중 GC를 제외한 모든 스레드가 중지함
      • Major GC가 일어날 때 Old 영역의 객체를 제거하며 Heap 메모리 영역 중간중간에 구멍이 생기게 되는데, 이를 없애기 위해 재구성을 함(메모리 정리 중에 다른 스레드가 메모리 사용할 수 없기에 모든 스레드가 중지)

GC 확인 방법

  1. JDK 에 포함된 명령어인 jstat 활용. jstat -gcutil <pid> 1s Jstat output 참고 Jstat Result
    • CCS : CodeCache usage 비율
    • YGC : Young Generation Garbage Collection 횟수
    • YGCT: Young Generation Garbage Collection 소요시간(sec)
    • FGC : Full GC 횟수
    • FGCT: Full GC 소요시간(sec)
    • CGC : Concurrent Garbage Collection 횟수
    • CGCT : Concurrent Garbage Collection 소요시간(sec)
    • GCT : Total GC 소요 시간(sec)
  2. JVM flag에 -Xlog:gc*(JDK17) 붙이기 JDK17 Throughput and Footprint Measurement
    1
    
    -Xlog:gc:/usr/src/gc.log
    

    위와 같이 JVM Flag에 추가해주면, /usr/src/gc.log 파일에 gc log를 남기게 됨

Heap Usage 확인 방법

JDK에 포함된 명령어인 jcmd 활용. jcmd <pid> GC.heap_info Heap Usage

메모리 덤프

  1. 수동으로 실행중인 자바 어플리케이션 메모리 덤프 하는 방법
    • jmap -dump:format=b,file=<dump file 이름>.hprof <PID>
  2. JVM Flag를 붙여서, OOM 에러 발생 시, Java Heap dump 하도록 설정
    1
    2
    
    -XX:+HeapDumpOnOutOfMemoryError
    -XX:HeapDumpPath=/usr/src
    
    • HeapDumpPath는 dump 파일의 경로가 아니라 해당 파일이 생길 디렉토리의 경로를 입력

위 방법으로 도출된 메모리 덤프를 Eclipse Memory Analyzer 를 이용해서 분석 이번 글에서는 해당 툴 사용방법까지 다루지는 않으려고 한다.

해결 방안

위의 방법들을 확인해서 어플리케이션의 GC Count, Heap Usage를 확인해보면서 아래와 같이 생각 정리를 했다.

  1. 일단 DB의 그 데이터들을 다 어플리케이션에 올리는 것 자체가 메모리를 많이 사용할 수 밖에 없음(그러나 지금은 임시적으로 해당 API가 필요함)
  2. 해당 API가 꼭 필요하다면 어플리케이션에서 사용하는 메모리를 줄이기 위해, 엑셀 생성을 클라이언트(프론트엔드) 측에서 진행하자!

엑셀 생성을 프론트 측으로 옮김으로써 서버에서 사용하는 메모리를 줄였고, API 호출할 때 GC 가 일어나는 횟수도 상당히 개선함으로써 문제를 어느정도 해결하였다.
 그리고 근본적인 해결을 위해서는 추후에 API 자체에 기간 조건을 두어 한번에 조회하는 데이터 자체를 줄이기를 진행하면 어플리케이션에서 한번에 큰 메모리를 사용하는 상황은 개선될 것으로 보인다.

느낀점

 이번 경험을 통해 GC와 Java의 메모리 구조 등에 대해서 더 잘 이해할수 있는 계기가 되었고 APM의 중요성을 체감하였다.
 갑자기 메모리를 많이 사용하여 OOM 에러가 발생하면 어플리케이션이 더이상 사용할 수 없는 먹통이 되어버리기에 그러한 상황을 미연에 방지하기 위해서는 평소에 모니터링 툴과 알람 시스템을 구축이 필요하겠다는 것을 크게 배우는 기회가 되었다.

This post is licensed under CC BY 4.0 by the author.