OOME

EXEM Knowledge Base

(OutOfMemoryError에서 넘어옴)
Jump to: navigation, 찾기

목차

[편집] OOME 개요

JVM이 일정한 크기의 메모리를 할당하는데 실패하면 Out Of Memory Error, 이른바 OOME가 발생한다.

OOME의 발생 원인은 매우 다양하며, 이는 JVM이 사용하는 메모리 공간의 다양성에 기인한다. 대부분의 JVM은 그 사용 용도에 따라 메모리를 몇가지 종류로 구분해서 사용한다. 가령 Sun HotSpot JVM은 다음과 같은 세 가지 종류의 메모리 공간을 사용한다.
(참고) 통상적으로 Permanent Space는 Java Heap의 하위 영역으로 설명된다. 하지만 본 문서에서는 Java Heap = Young Generation + Old Generation으로 간주한다

  1. Java Heap: 사용자가 생성하는 Java Object들이 거주하는 공간이다. -Xms<size>와 -Xmx<size> Option에 의해 크기가 결정된다.
  2. Permanent Space: Class에 대한 메타 정보를 저장하는 공간이다. PermSizeMaxPermSize 옵션에 의해 크기가 결정된다.
  3. Native Heap: Java Object가 아닌 Native Object들이 거주하는 공간이다. Native Heap의 크기는 JVM Option으로 지정할 수 없으며, OS 차원에서 결정된다.


각 메모리 공간의 용도와 사용 방식이 틀리기 때문에 OOME 또한 매우 다양한 상황에서 발생하게 된다. OOME가 발생하는 정확한 원인을 분석하려면 각 메모리 공간의 특성을 이해하고 그에 맞는 해결책을 모색해야 한다.

(주의) 비록 Java 언어와 JVM이 자동화된 메모리 관리 기능을 제공하지만, 이것이 개발자나 관리자가 메모리 관리에 대해 무신경해도 된다는 것을 의미하지 않는다는 사실을 명심하자. Java에서도 잘못된 메모리 관리는 여전히 많은 문제를 일으키며, Garbage Collection에 의한 성능저하나 OOME에 의한 Applictaion 정지나 System Crash등이 대표적인 예이다.

[편집] Java Heap에서의 OOME

Java Heap에서 OOME가 발생하는 경우에는 다음과 같은 에러 메시지가 출력된다.

Exception in thread "main": java.lang.OutOfMemoryError: Java heap space 또는
Exception in thread main: java.lang.OutOfMemoryError: Requested array size exceeds VM limit

전자의 메시지는 Java Heap Space의 부족으로 Object를 생성하지 못하는 경우에 발생한다. 후자의 메시지는 Java Heap의 최대 크기보다 큰 Array가 요청되는 경우에 발생한다. 가령 Java Heap의 최대 크기가 256M인 상황에서 300M 크기의 Array를 생성하는 경우가 이에 해당한다.

Java Heap에서 OOME가 발생하는 이유는 다음과 같다.

  • Java Heap의 크기가 작은 경우
  • Memory Leak이 발생하는 경우
  • finalize 메소드에 의한 Collection 지연


[편집] Java Heap의 크기와 OOME

Java Heap의 최대 크기가 Application의 메모리 요구량에 비해 작게 설정된 경우에 OOME가 발생한다. Memory Leak이 발생하지 않는데도 OOME가 발생한다면 Java Heap의 크기가 부족하다고 판단할 수 있다. -Xmx<size> 옵션을 이용해서 Java Heap의 최대 크기를 키워주어야 한다.

[편집] Memory Leak과 OOME

Memory Leak이 발생하는 경우에는 Java Heap의 크기와 무관하게 OOME가 발생할 수 있다. 아무리 Java Heap의 크기를 크게 하더라도 결국 Memory Leak에 의해 Collection되지 않는 Garbage 객체들이 메모리를 다 소진하기 때문이다. Memory Leak은 대부분 Application Logic 상의 오류에 의해 발생한다. Object에 대한 참조(Reference) 관계가 복잡한 경우 조그마한 실수로 인해 사용되지 않은 Object를 계속해서 참조하게 된다. 이러한 Object들은 비록 Application에서는 사용되지 않지만 Garbage Collection에 의해 메모리 해제가 이루어지지 않기 때문에 OOME를 유발하게 된다.

JDK Bug나 WAS Bug에 의해서도 Memory Leak이 발생할 수 있다. JDK가 제공하는 라이브러리나 WAS가 제공하는 라이브러리에서 로직 오류로 인한 Memory Leak 가능성이 있기 때문이다. Application Logic에서 Memory Leak이 검출되지 않는 경우에는 JDK나 WAS의 Bug를 의심해볼 필요가 있으며 각 Vendor가 제공하는 Bug Database(예: Sun Bug Database)를 통해 검색 가능하다.

[편집] finalize 메소드에 의한 Collection 지연과 OOME

특정 Class에 finalize 메소드가 정의되어 있는 경우, 이 Class Type의 Object는 Garbage Collection 발생시 즉각적으로 Collection 되지 않는다. 대신 Finalization Queue에 들어간 후 Finalizer에 의해 정리가 된다. Finalizer는 Object의 finalize 메소드를 실행한 후 메모리 정리 작업을 수행한다. 만일 finalize 메소드를 수행하는데 오랜 시간이 걸린다면 그 만큼 객체가 오랫동안 메모리를 점유하게 된다. 이로 인해 OOME가 발생할 확률이 높아진다. 이런 이유 때문에 finalize 메소드는 되도록 사용하지 말아야 한다.

[편집] Object Allocation Profiling

Java Heap의 메모리 부족 문제를 정확하게 분석하려면 Object Allocation Profiling을 수행해야 한다. 즉, 어떤 Object가 어느 개수만큼 생성되었으며 얼마나 많은 메모리를 차지하는지 분석할 필요가 있다. HProf는 모든 JVM이 표준으로 제공하는 Profiler로, 간단한 Object Allocation Profiling 기능을 제공한다.

 java -Xrunhprof:heap=sites [Main Class] 

또는 다음과 같이 doe(dump on exit) 옵션을 비활성화해서 시간순으로 Profiling을 수행할 수도 있다.

 java -Xrunhprof:heap=sites,doe=n [Main Class]
 ...
 Control+Break (혹은 다른 Console에서 kill -3 [pid])

Java Process에서 Signal을 보내서 Dump를 생성하는 방법은 Thread Dump를 참조한다.

아래에 HProf를 이용한 Object Allocation Profiling 결과의 간단한 Sample이 있다. 아래 Sample에서는 byte[] 유형의 객체가 20%의 Heap 공간을 사용하는 것을 알 수 있다.

        percent          live          alloc'ed  stack class
rank   self  accum     bytes objs     bytes  objs trace name
   1 20.36% 20.36%    190060   16    190060    16 300000 byte[]
   2 14.92% 35.28%    139260 1059    139260  1059 300000 char[]
   3  5.27% 40.56%     49192   15     49192    15 300055 byte[]
   4  5.26% 45.82%     49112   14     49112    14 300066 byte[]
   5  4.32% 50.14%     40308 1226     40308  1226 300000 java.lang.String
   6  1.62% 51.75%     15092  438     15092   438 300000 java.util.HashMap$Entry
   7  0.79% 52.55%      7392   14      7392    14 300065 byte[]
   8  0.47% 53.01%      4360   16      4360    16 300016 char[]
   9  0.47% 53.48%      4352   34      4352    34 300032 char[]
  10  0.43% 53.90%      3968   32      3968    32 300028 char[]
  11  0.40% 54.30%      3716    8      3716     8 300000 java.util.HashMap$Entry[]
  12  0.40% 54.70%      3708   11      3708    11 300000 int[]

[편집] Permanent Space에서의 OOME

Permanent Space에서 OOME가 발생하면 다음과 같은 에러 메시지가 출력된다

Exception in thread "main": java.lang.OutOfMemoryError: Perm Gen space'

Permanent Space는 Class의 메타 정보를 저장하는 공간이다. 따라서 많은 수의 Class를 로딩하는 Application의 경우 Permanent Space의 크기가 작으면 OOME가 발생할 수 있다. 다음과 같은 유형의 Application들에서는 Permanent Space의 크기를 키워줄 필요가 있다.

  • 매우 많은 수의 JSP 파일을 로딩하는 Web Application. JSP 파일은 Servlet으로 변환된다. 하나의 Servlet은 하나의 Java Class에 해당한다. 따라서 이 경우 매우 많은 수의 Class가 로딩된다.
  • Reflection Mechanism을 사용해 동적으로 Class를 로딩하는 Framework. Spring과 같은 현대적인 Framework들은 Reflection Meachanism을 통해 동적으로 Class를 생성한다. 이 경우 개발자가 의도하지 않은 많은 수의 Class들이 로딩될 수 있다.

이런 문제는 대부분의 Permanent Space의 크기를 키워주면 해결된다. PermSize, MaxPermSize 옵션을 이용해 Permanent Space의 최소 크기와 최대 크기를 지정할 수 있다.

[편집] Class Loading 모니터링

Permanent Space에 Loading되는 Class의 목록을 모니터링함으로써 OOME가 발생하는 원인을 간접적으로 분석할 수 있다. 다음과 같은 방법을 통해 Class Loading을 모니터링할 수 있다.

  • -verbose:gc: Loading되는 Class들을 Standard Out을 통해 출력한다.
  • TraceClassLoading, TraceClassUnloading 옵션을 통해 Class Loading과 Unloading 작업을 Standard Out에 출력한다.
  • Platform MBean: JMX 표준을 통해 제공되는 ClassLoadingMXBean API를 이용하면 프로그래밍적으로 Class Loading 정보를 얻을 수 있다.
  • JConsole: JConsole을 이용하면 Class Loading 정보를 조회할 수 있다. JConsole은 JMX 클라이언트의 표준 샘플로 Platform MBean과 통신해서 Class Loading 정보를 얻는다.

아래에 -verbose:class 옵션에 의한 Class Loading 모니터링의 간단한 Sample이 있다. Open된 jar 파일과 Loading된 Class 목록을 확인할 수 있다.

[Opened c:\bea10\jdk150_06\jre\lib\rt.jar]
[Opened c:\bea10\jdk150_06\jre\lib\jsse.jar]
[Opened c:\bea10\jdk150_06\jre\lib\jce.jar]
[Opened c:\bea10\jdk150_06\jre\lib\charsets.jar]
[Loaded java.lang.Object from c:\bea10\jdk150_06\jre\lib\rt.jar]
[Loaded java.io.Serializable from c:\bea10\jdk150_06\jre\lib\rt.jar]
[Loaded java.lang.Comparable from c:\bea10\jdk150_06\jre\lib\rt.jar]
[Loaded java.lang.CharSequence from c:\bea10\jdk150_06\jre\lib\rt.jar]
[Loaded java.lang.String from c:\bea10\jdk150_06\jre\lib\rt.jar]
[Loaded java.lang.reflect.GenericDeclaration from c:\bea10\jdk150_06\jre\lib\rt.jar]
...

(참조) IBM JVM에서는 Thread Dump에서도 Class Loading 정보를 제공한다.

[편집] Class Reloading과 OOME

현대적인 대부분의 WAS가 Class Reloading 기능을 제공한다. Class Reloading이란 Runtime에 Class가 재생성되면 이를 JVM을 Reboot하지 않고 Reloading하는 기능을 의미한다. 일부 WAS의 경우 Class가 Reloading될 때 이전 버전의 Class를 해제하지 않는 경우가 있다. 따라서 Class Reloading이 자주 발생하면 Permanent Space가 금방 꽉차게 되고 그 결과로 OOME가 발생하게 된다. 이와 같은 경우에는 WAS가 제공하는 버그 패치를 적용하거나 WAS를 주기적으로 Restart해야 한다.

[편집] Native Heap에서의 OOME

Java Heap과 Permanent Space가 Java와 관련된 Object들이 거주하는 공간인 반면, Native Heap은 OS 레벨의 Native Object나 JNI Library 레벨의 Native Object이 거주하는 공간이다.

Native Heap에서 OOME가 발생하면 다음과 같은 에러 메시지가 출력된다.

java.lang.OutOfMemoryError: request <size> bytes for <reason>. Out of swap space? 또는
java.lang.OutOfMemoryError: <reason> <stack trace> (Native method)' 또는
java.lang.OutOfMemoryError: unable to create new native thread


첫번째 메시지는 VM Code 레벨에서 메모리 부족 현상이 발견된 경우이다. 두번째 메시지는 JNI나 Native Method 에서 메모리 부족 현상이 발견된 경우에 해당한다. 세번째 메시지는 Thread를 생성할 수 없을 때 발생한다. Thread는 Native Heap 공간의 메모리를 필요로 하기 때문에 Native Heap 공간의 메모리가 부족하면 Thread 생성시 OOME가 발생한다.

Native Heap에서 메모리 부족이 발생하는 이유는 매우 다양하다.

  • Thread Stack Space가 부족한 경우
  • Virtual Space Address가 소진된 경우
  • Swap Space가 모자란 경우
  • JNI Library에서 Memory Leak이 발생하는 경우

[편집] Thread Stack Space와 OOME

Java Thread는 Native Heap 공간에 Stack Trace를 저장할 공간을 필요로 한다. Thread Stack Space의 크기는 -Xss<size> 옵션을 통해 지정된다. -Xss<size> 옵션을 통해 지정되는 공간은 개별 Thread가 사용하는 공간이다. 만일 N개의 Thread가 활성화되면 N*<size> 만큼의 메모리 공간이 필요하다.

대부분의 OS에서 Thread Stack Size는 512K ~ 1M 사이다. 따라서 많은 수의 Thread가 활성화되면 Thread Stack Space만으로도 큰 크기의 Native Heap 메모리 공간을 소모한다.

Thread Stack Space 문제에 의한 OOME를 해소하는 방법은 다음과 같다.

  • Thread의 수를 줄인다. 동시에 수십개 이상의 Thread를 사용하는 것은 메모리의 문제 뿐만 아니라 지나친 Context Switching으로 인해 성능을 저하시키는 요인이 된다. Thread Pool 기법을 사용해서 동시 Thread의 수를 줄인다. 대부분의 WAS들이 Thread Pool 기법을 사용하고 있다.
  • Thread Stack Size를 줄인다. 대부분의 OS에서 Thread Stack Size는 512K ~ 1M이다. 만일 많은 수의 Thread가 필요한 Application이라면 Thread Stack Size를 줄임으로써 OOME를 방지할 수 있다. 많은 경우 -Xss128k 정도나 -Xss256k 정도의 크기에서도 문제없이 작동한다. 단, Stack Size가 줄어든 만큼 Stack Overflow Error가 발생할 확률은 높아진다.
  • Java Heap 크기를 줄인다. 32bit Process가 사용 가능한 메모리 공간은 OS에 따라 2G ~ 4G로 제한된다. 하나의 Java Process가 사용 가능한 공간은 [Java Heap+Permanent Space+Native Heap]으로 이루어진다. 따라서 Java Heap이 지나치게 큰 공간을 사용하는 경우 Native Heap에서 사용 가능한 공간이 줄어들게 된다. 따라서 Java Heap 크기를 줄이면 Native Heap의 메모리 부족에 의한 OOME 문제를 해결 할 수 있다. 하지마 Java Heap 크기를 지나치게 줄이면 Java Heap 부족에 의한 OOME 현상이 발생할 수 있으므로 유의해야 한다. Java Heap 크기를 줄이는 방법은 Thread Stack Space의 부족 문제 뿐 아니라 Native Heap 부족에 의한 OOME 문제를 줄이는 공통적인 해결 방법이다.
  • 64bit JVM을 사용한다. 64bit JVM에서는 32bit JVM Process가 가지는 2G ~ 4G의 제한이 없다. 따라서 Native Heap의 메모리 부족 문제가 줄어든다. 이 방법 또한 Native heap 부족에 의한 OOME 문제를 줄이는 공통적인 해결 방안이다.

64bit JVM을 사용하는 경우, 다음과 같은 사실에 유의해야 한다.

  1. 일반적으로 32bit Application의 성능이 64bit Application에 비해 더 나은 성능을 보이는 경우가 많다. 따라서 64bit JVM을 사용하는 경우 약간의 성능 저하가 발생할 수 있다는 사실에 유의해야 한다.
  2. 과도한 Virutal Memory의 사용은 Application의 성능을 저하시키는 주요인이다. Java Application의 성능은 모든 Object들이 Physical Memory에 머물러 있을때 가장 뛰어나다. 만일 Physical Memory를 초과하는 크기의 Virtual Memory를 사용하면 Physical Memory의 일부를 Disk에 저장하는 Paging In/Out이 발생한다. Paging In/Out은 Memory Operation에 비해 매우 느리며 그만큼 Application의 성능도 저하된다.


[편집] Virtual Address Space와 OOME

32bit JVM에서 사용가능한 Virtual Address Space의 최대 크기는 OS에 따라 2G ~ 4G로 제한된다. Java Process의 경우 Java Heap과 Permanent Space를 제외한 나머지 공간만을 Native Heap이 사용할 수 있다. 가령 2G의 Virtual Address Space만이 사용가능하다고 가정하자. 이 때 Java Heap이 1G, Permanent Space가 200M를 사용한다면 Native Heap이 사용 가능한 최대 크기는 800M에 불과하다. 800M 중에는 OS가 Process 관리를 위해 사용하는 기본 메모리가 포함되기 때문에 실제로 Java Application이 사용 가능한 Native Heap의 크기는 훨씬 줄어든다. 따라서 이 경우 Native Heap 공간 부족에 의한 OOME가 발생할 확률이 높아진다.

Virtual Address Space 부족에 의한 OOME를 해결하는 방법은 Thread Stack Space에 의한 OOME 해결방안과 일맥상통한다. Java Heap 크기를 줄이거나 64bit JVM를 사용한다. Thread 수를 줄이거나 Thread Stack Size를 줄임으로써 Native Heap 공간을 확보하는 것도 방법이 될 수 있다.

[편집] Swap Sapce와 OOME

Physical Memory를 초과하는 Virtual Memory 요청이 발생하면 Paging In/Out을 통해 필요한 메모리를 확보한다. Paging In/Out을 위한 공간이 Swap 공간이다. 따라서 Swap Space가 부족하면 Paging In/Out에 실패하고 이로 인해 OOME가 발생한다.

여러 개의 Process가 Swap Space를 사용하는 경우 Swap Space 부족에 의한 OOME가 발생할 확률이 높아진다. OS가 제공하는 툴들을 통해 Swap Space와 Paging In/Out을 모니터링해야 하며, Swap Space가 부족한 경우에는 크기를 키워주어야 한다.

[편집] OOME와 Fatal Error Log

Native Heap에서 OOME가 발생하면 JVM은 심각한 상황이라고 판단하고 Fatal Error Log를 남긴다. 아래에 OOME가 발생한 상황에서의 Fatal Error Log의 Header 정보에 대한 간단한 Sample이 있다.

#
# An unexpected error has been detected by Java Runtime Environment:
#
# java.lang.OutOfMemoryError: requested 20971520 bytes for GrET in C:\BUILD_AREA
\jdk6_02\hotspot\src\share\vm\utilities\growableArray.cpp. Out of swap space?
#
# Internal Error (414C4C4F434154494F4E0E494E4C494E450E4850500017), pid=5840, ti
d=5540
#
# Java VM: Java HotSpot(TM) Client VM (1.6.0_02-b06 mixed mode)
# An error report file with more information is saved as hs_err_pid5840.log
#
# If you would like to submit a bug report, please visit:
# http://java.sun.com/webapps/bugreport/crash.jsp
#

Fatal Error Log를 통해 OOME를 유발한 Thread와 Stack Trace를 추적할 수 있다. 이 정보가 곧 해결책을 의미하지는 않지만, 추가적인 분석을 위한 힌트가 될 수 있다.