본문 바로가기
Software Architect/SWA 이야기

OutOfMemoryError의 이해

by romainefabula 2022. 5. 3.

WAS를 운영하다 보면 OutOfMemoryError가 발생하는 경우가 있다. 이런 일이 무슨 이유로 발생하고, 어떻게 원인을 찾고, 문제를 어떻게 해결하는지 알아보려고 한다.

 

왜 발생하나?

자바 프로그램이 실행하는 동안에는 변수 등을 생성하면서 힙메모리의 영역을 요청해서 받았다가, 사용이 끝나면 풀어주는 일을 반복한다. 그런데, 힙메모리를 모두 사용해서 더 이상 메모리를 받을 수 없을 때 OutOfMemoryError가 발생한다. 정상적인 경우라면, 프로그램에서 사용이 끝난 메모리를 반납하면 GC(Garbage Collection)이 수행되어 다시 사용할 수 있도록 되어야 한다. 그러나, GC 대상이 되지 못하게 프로그램이 메모리를 잡고 있거나, GC를 통해서 사용 가능하게 되는 메모리보다 메모리를 요청하는 속도가 더 빠르면 이런 에러가 발생한다.

OutOfMemoryError 발생시 나타나는 현상

OutOfMemoryError가 발생하면 JVM은 최대한 빨리 힙메모리를 확보하기 위해 Full GC를 수행한다. Full GC를 수행할 때는 STW(Stop The World)라는 현상이 발생하는데, WAS의 모든 프로그램 수행을 멈추고 GC만 수행해서 이렇게 부른다. Full GC는 최대한 자원을 사용하기 때문에 WAS가 실행되는 서버의 CPU 사용률도 급격히 치솟는데, 최악의 경우 100%가 계속 유지되는 경우도 많다.
Full GC가 몇 초 단위로 계속 반복되고 CPU 사용률도 100%로 계속 유지된다는 것은, 힙메모리에 Full GC를 해도 회수되는 메모리의 양이 적어서 다시 Full GC를 수행하고, 그 후에도 같은 상황이 반복된다는 의미이다. 이럴 때는 힙덤프를 생성하고, 힙덤프 생성이 끝나자마자 WAS를 재기동해야 한다.

 

발생원인 1 : 메모리 누수

프로그램 내에 메모리 누수(Memory Leak)이 있어서 메모리를 반납하지 않고 계속 새로운 메모리를 얻기만 하는 경우이다. 이런 프로그램이 있으면 메모리를 얻을 때마다 힙메모리 영역이 조금씩 줄어들다가 OutOfMemoryError가 발생하면서 WAS의 프로그램들이 메모리를 요청한 상태에서 모두 대기상태에 빠져 멈춘다.
이런 문제는 객체나 변수를 생성했다가 반납하는 구조로 만들었을 때는 발생하지 않는다. 거의 대부분이 Singleton 등의 사라지지 않고 계속 떠 있는 객체 내에 Collection 타입의 변수를 선언하고 여기에 저장한 데이터를 지우지 않고 지속적으로 추가만 하는 경우에 발생한다. 이런 변수로 힙메모리를 모두 사용하려면 아주 짧게는 몇 시간에서 길게는 몇 주까지 걸린다. 그러므로, 주기적으로 재기동을 하는 시스템은 인지하지 못할 수도 있고, 문제가 생기는 데 오랜 시간이 걸리기 때문에 메모리 누수가 원인이라고 생각하기 어려울 수 있다.

해결방법 : 많은 데이터를 저장하는 Collection에서 오래된 데이터를 지워야 하는데 이 로직을 깜빡했었다면, 필요한 데이터만 남기고 필요없는 데이터는 지우도록 프로그램을 수정한다. 대량 데이터를 그대로 유지해야 한다면, 다른 외부 저장공간에 저장하도록 수정하거나, 힙메모리를 늘려서 대량의 데이터를 저장할 수 있도록 한다.

 

발생원인 2 : 힙메모리의 과도한 사용

프로그램의 계획을 잘못 세워서 짧은 시간에 아주 많은 힙메모리를 사용하게 만든 경우이다. 구체적인 예를 들면 DB에서 대량(몇만 또는 몇십만 건)의 데이터를 조회해서 WAS에서 가공하고 클라이언트에 응답을 주는 경우이다. 보통 이런 프로그램들이 조회대상 데이터를 DB에서 모두 가져와서 한꺼번에 힙메모리에 올린 후에 가공하고 클라이언트에 응답을 보내려고 한다.

해결방법 : 클라이언트로부터 조회조건을 받아서 DB에 조회한다면 조회 기간 등에 제한을 걸어서 너무 많은 데이터를 조회하지 못하도록 한다. 파일 다운로드 등의 용도로 대량 데이터를 조회할 수밖에 없는 상황이라면 WAS이 힙메모리를 늘려 준다.

 

OutOfMemoryError 원인 분석

OutOfMemoryError의 원인을 분석하기 위해서는 OutOfMemoryError 한참 발생한 시점에 힙덤프를 생성해야 한다. 그래야 그때 어떤 프로그램들이 메모리를 많이 차지하고 있었는지 찾을 수가 있다.
힙덤프를 생성하는 방법에는 두 가지가 있다. 첫 번째는 WAS를 기동할 때 JVM 옵션(-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/logs/heapdump)을 추가해서 OutOfMemoryError가 발생하면 자동으로 지정된 위치에 힙덤프가 생성되도록 구성하는 것이다. 두 번째는 OutOfMemoryError가 발생하는 시점에 WAS 프로세스에 대해서 힙덤프 생성명령(jmap -dump:format=b,file={파일명} {WAS_PID})을 실행해서 힙덤프를 생성하는 것이다.
다음으로는 힙덤프 파일을 다운로드 받아서 분석프로그램에서 읽어 들여 분석하는 것이다. 요즘은 보통 Memory Analyzer (MAT)를 사용한다. 오래된 JVM에서 생성된 힙덤프는 분석된 결과를 보면서 문제가 되는 부분을 직접 찾아야 하지만, 비교적 최근에 나온 JVM에서 생성된 힙덤프에 대해서는 메모리 누수가 의심되는 부분까지 툴이 알려 주기 때문에 상대적으로 수월하게 원인을 찾을 수 있다. 하지만, 이것이 절대적인 정답은 아니니까 의심되는 소스들을 잘 살펴봐서 진짜 원인을 발견하는 능력을 키워야 한다.
한 가지 알아둘 것이 있는데, 생성되는 힙덤프 파일의 크기는 WAS의 힙메모리 크기와 거의 비례하게 커지기 때문에 몇 기가바이트의 힙덤프가 생길 수도 있다. MAT는 이 큰 파일을 한 번에 모두 읽어 올려야 하기 때문에 MAT를 실행하는 컴퓨터의 RAM도 넉넉해야 분석을 수행할 수 있다.

 

언제나 새롭게 생소한 것을 배울 때는 눈에 잘 안 들어오지만, 관련된 지식을 쌓은 후에 열심히 보다 보면 어느 순간 암호 같던 내용들이 어린이용 동화책을 읽는 것처럼 쉽게 이해되기 시작한다. 운영환경을 담당하는 경우가 아니라면 쉽게 접하기 어려운 문제지만, 한번 경험하면서 해결까지 해 보면 앞으로 유용하게 사용할 수 있는 좋은 기술을 하나 갖게 되는 것이다.