본문 바로가기

Java For All

Heap 여유 공간이 충분한데도 OOME(OutOfMemoryException)이 발생한다?


간혹 Heap의 여유 공간이 충분한데도 OutOfMemory Error가 나는 경우가 있다. 이러한 상황을 이해하려면 Java Application이 사용하는 메모리가 여러 영역으로 나뉜다는 사실을 이해해야 한다.

Java Application이 사용하는 메모리 영역은 보통 다음과 같이 분류된다.

- Permanent Space: Class 정보를 저장
- Java Heap: Object 정보를 저장
- Native Heap: JNI, Thread Stack, 기타 Native 정보를 저장

우리가 흔히 접하는 Memory 문제는 대부분 Java Heap에서 발생한다. Java Application이 할당하는 오브젝트들이 Java Heap에 거주하기 때문에 가장 많은 메모리를 필요로 하기 때문이다.

하지만 위의 제목처럼 GC Log 등을 통해 모니터링을 해 본 결과 Java Heap의 여유 공간이 충분한데도 OOEM이 난다면? 다음과 같은 세가지 문제를 의심해보아야 한다.

- Permanent Space가 부족하지 않은가?
- Native Heap이 부족하지 않은가?
- 버그가 아닌가? :)

Permanet Space와 Native Heap에서 발생할 수 있는 메모리 부족 현상에 대해서 알아보자.

- Permanent Space의 부족
GC Log를 통해 Permanent Space가 부족한 지의 여부를 간접적으로 판단할 수 있다.
(GC Log에 대한 자세한 내용은 Google 검색....)

대부분의 Application이 기본 크기만으로 필요한 클래스들을 관리할 수 있다. 하지만 복잡한 Application 들, 특히 많은 수의 Servlet/JSP/EJB/Library 들을 로딩하는 Web Application의 경우 좀 더 큰 크기의 Permanent Space를 요구하기도한다.

-verbose:class 옵션을 사용하면 어떤 클래스들이 로딩되는지 확인할 수 있다.

Permanent Space의 부족 현상으로 판명나는 경우에는 -XX:PermSize-XX:MaxPermSize 옵션을 이용해 Permanent Space의 크기를 키워주어야 한다.

- Thread Stack Size가 큰 경우
우리가 Thread를 사용할 때마다 Native Heap에는 Thread가 사용하는 메모리가 할당된다. 시스템마다 기본적으로 할당되는 크기는 다르다.

Thread Stack의 크기를 256K라고 가정해보자. 4개의 Thread는 1M를 사용하고, 400개의 Thread는 무려 100M를 사용한다. 수많은 Thread를 사용하는 대형 시스템에서는 결코 간과할 수 없는 크기가 된다.

Application 모니터링을 통해 현재 생성한(사용 중인 아닌) 전체 Thread의 개수를 파악해야 한다. 만일 지나치게 많은(수백개 ~ 수천개) Thread가 생성되었다면 지나치게 큰 크기의 Native Heap을 사용하고 필연적으로 OOEM을 유발한다.

Thread 개수를 줄이는 가장 기본적인 방법은 Thread Pool을 사용하는 것이다. Weblogic/Jeus/Websphere와 같은 WAS들이 수백~수천의 동시 사용자를 감당할 수 있는 것은 Thread Pool을 잘 사용하기 때문이다. 대량의 Client와 통신을 수행하는 Server Application을 작성하는 경우에는 반드시 Thread Pool 기법을 사용해서 실제 생성되는 Thread의 개수를 줄여야 한다.

또 다른 방법은 Thread Stack Size를 줄이는 것이다. Thread 개수를 줄일 수 없다면 -Xss128K 정도의 작은 값을 부여해서 메모리 사용량을 줄일 수 있다. 단, Stack 크기가 줄어든 만큼 Stack Overflow 에러가 날 확률이 높아진다.

(PS) Application이 사용하는 Thread 개수를 모니터링하는 가장 좋은 방법은? Thread Dump(kill -3)가 가장 손쉽고 확실한 방법이다. JDK 1.5에서 추가된 Platform MBean(MXBean)과 JConsole 또한 좋은 방법이다. JConsole에 대해서는 아래 URL을 참조한다.

http://java.sun.com/developer/technicalArticles/J2SE/jconsole.html

- File나 Socket을 지나치게 많이 오픈하는 경우
 File/Socket을 Resource Limit를 초과하여 오픈하는 경우에도 OOEM이 발생한다. 우선 File이나 Socket을 열고 닫지 않은, 이른바 Resource Leak을 의심해봐야 하며 필요한 경우 다음과 같이 File/Socket의 Resource Limit을 키워야 한다.

ulimit -n 10000 

- JNI를 사용하는 Library들...
JDK가 제공하는 Library나 3rd Party가 제공하는 Library들 중에서 JNI를 사용하는 경우 Native Heap을 사용한다. Native Heap은 Java Heap과는 전혀 독립적으로 사용된다. 따라서 Java Heap은 여유가 있음에도 OOEM이 발생하게 된다.

Max Heap(-Xmx) 크기를 500M 정도로 지정했는데 OS 모니터링을 통해 본 Java Process의 메모리 크기가 2G라면? 십중 팔구 Native Heap의 크기가 지나치게 커진 것이다. Thread Stack 크기와 더불어 JNI Heap을 의심해보아야 한다.

Oracle OCI JDBC Driver가 JNI를 사용하는 대표적인 3rd Party Library이다. OCI JDBC Driver는 Thin Driver에 비해 성능 면에서 다소 유리하지만 Native Heap 공간을 많이 사용하는 경향이 있다. 따라서 OCI Driver를 사용하는 경우에는 Process의 메모리 크기를 잘 모니터링해야 한다.

한가지 역설적인 것은 Native Heap을 좀더 크게 사용하기 위해서 Java Heap의 크기를 줄여야 하는 경우가 있다는 것이다. 즉, OutOfMemory 부족 현상이 Native Heap에서 발생하는 경우에는 Java Heap의 크기를 줄이는 것이 대안이 될 수 있다. 32bit 환경에서는 하나의 Java 프로세스가 사용할 수 있는 최대 메모리 공간은 2G로 제한된다. 이 공간을 Java Heap이 다 써버리면 Native Heap이 사용될 수 있는 공간이 그만큼 줄어든다. 그만큼 OOEM이 날 확률이 높아진다.

- 노파심에...
시스템의 물리적 메모리가 2G이다. Java Application의 성능을 극대화하기 위해 Max Heap Size를 1.8G 정도 부여할려고 한다. 바람직한가? 그럴 수도 있지만 생각치 못한 역효과가 있을 수 있다. 앞에서 소개한 몇가지 사례를 기억하자.

Java Heap에 지니치게 많은 메모리를 할당함으로써 Permanet Space나 Native Heap의
부족을 초래하게 되고 이로 인해 OOEM이나 Paging In/Out에 의한 성능 저하 현상이 유발될 수 있기 때문이다.


참조사이트>
http://blog.naver.com/ukja/120042426555