얼마전 특정 사이트의 was에서 oome가 발생하였다. 처음보는 에러에다가 특정 소스의 line이 명확하게 나오지 않아 바로 해결할 수 가 없었다.

맨 처음 발생했을 때 의 oome 메시지는

Exception in thread “main”: java.lang.OutOfMemoryError: Java heap space

구글링 해보니 heap영역의 메모리를 더이상 확보할 수 없다는 의미로 해석이 된다.

그렇다면 heap영역의 크기를 늘리면 되지 않을까? 라는 판단을 하여 heap영역의 크기를 늘리기로 하였다.

heap영역을 늘리기 위해선 tomcat에서 설정을 해야 하는데, $CATALINA_HOME의 bin으로 가면 setenv.sh라는 파일이 있다.(없으며 생성하자.)

<setenv.sh>

#!/bin/sh
export JAVA_OPTS="-Djava.library.path=/usr/local/lib -XX:PermSize=256m -XX:MaxPermSize=1024m -Xms1536m -Xmx1536m -Djava.awt.headless=true"
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$CATALINA_HOME/lib:/usr/local/lib"

Xms는 heap의 최소 메모리 즉, 처음 jvm이 실행되었을 때 할당되는 메모리 size이다.

Xmx는 heap이 최대로 사용할 수 있는 메모리 size이다.

32bit의 운영체제에서 jvm은 최대 1.2 ~ 1.5GB 까지만 메모리를 사용할 수 있었기 때문에 1536이라는 수치로 옵션을 설정하게 되었다.

Xms와 Xmx을 동일값으로 부여한 이유는 크기의 동적인 변경에 의한 오버 헤드를 최소화 하기 위함이다.(Sun HotSpt JVM에서 권장)

해당 설정한 이후 약 20일 이후에 다시 oome가 발생하였다.

소스상에 문제가 있는건 아닌가 의문이 들었고 세밀한 분석을 위해, jvm옵션을 더 추가하게 되었다.

#!/bin/sh
export JAVA_OPTS="-Djava.library.path=/usr/local/lib -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/cmuc/tomcat.hprof -XX:PermSize=256m -XX:MaxPermSize=1024m -Xms1536m -Xmx1536m -Djava.awt.headless=true"
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$CATALINA_HOME/lib:/usr/local/lib"

추가한 옵션은 -XX:+HeapDumpOnOutOfMemoryError와 -XX:HeapDumpPath=/home/cmuc/tomcat.hprof 

-XX:+HeapDumpOnOutOfMemoryError 옵션은 oome가 발생했을 때 heapdump를 생성하도록 하는 옵션으로

-XX:HeapDumpPath에서 설정한 경로에 heapdump파일을 생성한다.

 

그렇게 생성된 heapdump파일을 이클립스의 MAT로 분석할 수 있는데 소스상에 특별한 문제점은 찾을 수 없었다.

 

그렇다면 GC에 문제가 있는 것일까?라는 의문점을 갖게 되었고,

GC에 대한 정보를 찾다가 네이버 개발자들이 만든 https://d2.naver.com/helloworld/ 의 블로그에서 GC 튜닝관련 정보들을 얻을 수 있었다.

GC튜닝을 하기 전에 먼저 GC가 정상적으로 이루어지는지 확인하기 위해

jvm 명령어인 jstat -gcutil(가비지 컬럭센 통계 데이터의 개요)을 통해 GC 수행 횟수 및 GC 수행하는게 걸리는 시간을 분석할 수 있었다.

<jstat -gcutil의 컬럼 설명>

컬럼명 설명
S0 Young영역의 Survivor 영역 0의 사용률
S1 Young영역의 Survivor 영역 1의 사용률
E Young영역의 Eden영역의 사용률
O Old영역의 사용률
P Permanent 영역의 사용률
YGC Young 영역의 Minor GC 총 수행 횟수
YGCT Young 영역의 Minor GC 총 수행 시간
FGC Old 영역의 Full GC 총 수행 횟수
FGCT Old 영역의 Full GC 총 수행 시간
GCT 전체 GC 총 수행 시간

Minor GC의 평균 수행 시간이 (YGCT/YGC)로 0.5초 미만이면 양호한 상태이다.

Full GC의 평균 수행 시간이 (FGCT/FGC)로 1초 미만이면 양호한 상태이다.

Full GC의 평균 수행 시간이 길 경우 Stop the World 시간이 길어지게 되므로 GC튜닝이나 heap메모리 튜닝이 필요할 것이다. 현재 운용되는 프로세스에서는 모두 양호한 상태이기 때문에 따로 튜닝을 하지 않았다.

 

자.. 그러면 어째서 OOME가 발생하는 것일까..

-----------------------------------------------------------------------------------------------------------------------------------

두 번째 발생했을 때의 로그를 다시 확인해보니

Exception in thread “main”: java.lang.OutOfMemoryError: (Native method) 으로 나오는 것이 확인되었다.

해당 에러는 JVM에 설정된 것 보다 큰 native 메모리가 호출 될 때 발생된다.

 

일단 jvm 메모리 구조를 보자.(출처 http://www.nextree.co.kr/p3878/)

Native Heap Area OOME는 위에 설명처럼 Native Object들이 거주하는 공간에 더 이상 가용할 수 있는 메모리가 없기 때문에 발생하는 에러이다. 또한 이번 에러 로그에는 특정 소스의 몇 번째 라인에서 발생하는지 출력이 되었다.

해당 라인은 클라이언트에서 특정 url이 요청 되면 사진파일을 ServletOutputStream 객체에 flush()메소드를 호출하는 부분이었는데, 소스를 보니 flush()를 여러번 호출하도록 되어있었다.

여러 구글링을 해보니 OutputStream의 close()메소드가 마지막에 모두 flush를 한다고 명시되어있으며 flush()를 많이 사용할 경우 문제가 발생할 수 있다고 설명이 되어있었다.

flush()메소드를 모두 제거한 후 jmeter로 해당 url을 사이트 환경과 비슷하게 맞춘 후 테스트한 결과 oome는 발생하지 않았다.

추후 사이트 적용하여 모니터링 할 예정이다.

+ Recent posts