OOME

OOME概要

 

JVMが一定の大きさのメモリを割り当てるのに失敗した場合、OOME(Out Of Memory Error)が発生します。

 

OOMEの発生原因は、非常に多様であり、これはJVMが使用するメモリ空間の多様性に起因します。ほとんどのJVMは、その使用用途に応じて、メモリをいくつかの種類に区分して使用します。例えば、Sun HotSpot JVMは、次の3つの種類のメモリ空間を使用します。 (参考)通常Permanent SpaceはJava Heapのサブ領域として説明されます。しかし、ここでは、Java Heap = Young Generation + Old Generationとみなします。

 

1. Java Heap:ユーザーが作成したJava Objectが居住する空間です。 
   -Xmsと-Xmx Optionによってサイズが決定されます。
2. Permanent Space:Classのメタ情報を格納する空間です。 
   PermSizeとMaxPermSizeオプションに応じてサイズが決定されます。
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が発生した場合
 ・Application LogicによるMemory Leak
 ・JDK BugやWAS BugによるMemory Leak

・finalizeメソッドによるCollection遅延

 

Java HeapのサイズとOOME

 

Java Heapの最大サイズはApplicationのメモリ要求量に比べて小さく設定されている場合にOOMEが発生します。Memory Leakが発生しないのにもかかわらずOOMEが発生した場合は、Java Heapのサイズが不足していると判断することができます。-Xmxオプションを使用して、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)を検索する必要yがあります。

 

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 bytes for . Out of swap space? 또는
java.lang.OutOfMemoryError: (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オプションを使用して指定されます。-Xssオプションを使用して指定された空間は、個々のThreadが使用する空間です。もしN個のThreadが有効な場合、N*だけのメモリ容量が必要となります。

 

ほとんどの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を追跡することができます。この情報はすぐに解決策を意味するものではありませんが、追加的な分析のためのヒントになることがあります。