この章では、ハングアップまたはループしているプロセスをトラブルシューティングする具体的な手順に関する情報と指針を提供します。
ハングアップまたはループしているプロセスが関与する問題が発生することがあります。ハングアップはさまざまな理由で発生する可能性がありますが、多くの場合、アプリケーションコード、API コード、またはライブラリコード内のデッドロックが原因です。HotSpot 仮想マシンのバグが原因でハングアップが発生することもあります。
ハングアップのように見えて、実はループであることが判明する場合もあります。たとえば、VM プロセス内のバグによって 1 つ以上のスレッドが無限ループに入ると、使用可能な CPU サイクルがすべて消費される可能性があります。
ハングアップを診断する最初の手順は、VM プロセスがアイドル状態なのか、それとも使用可能な CPU サイクルをすべて消費しているかを調べることです。そのためには、オペレーティングシステムのユーティリティーを使用する必要があります。プロセスがビジー状態で、使用可能な CPU サイクルをすべて消費しているように見える場合は、デッドロックではなくループしているスレッドが問題である可能性が高いです。Solaris OS では、たとえば、コマンド prstat -L -p
VM プロセスがループしているように見える場合、最初の手順はスレッドダンプを取得しようとすることです。スレッドダンプを取得できれば、多くの場合、どのスレッドがループしているかがはっきりします。ループしているスレッドを識別できたら、スレッドダンプ内のトレーススタックから、そのスレッドがループしている場所 (および場合によっては理由) に関する手掛かりを得ることができます。
アプリケーションコンソール (標準入出力) を使用できる場合は、Ctrl + \ キーの組み合わせ (Solaris OS または Linux の場合) または Ctrl + Break キーの組み合わせ (Windows の場合) を押して、HotSpot VM にスレッドの状態を含むスレッドダンプを出力させることができます。Solaris OS および Linux では、SIGQUIT をプロセスに送信して (kill -QUIT
-XX:+PrintClassHistogram コマンド行オプションを指定して Java プロセスを起動すると、Ctrl + Break ハンドラによってヒープヒストグラムが生成されます。
スレッドダンプを取得できる場合は、RUNNABLE 状態のスレッドのスレッドスタックから始めるのが適切です。スレッドダンプの形式、およびスレッドダンプ内のスレッドが取り得る状態の表については、「2.15.1 スレッドダンプ」を参照してください。場合によっては、どのスレッドが継続的にビジーに見えるかを特定するため、スレッドダンプを連続して取得する必要がある場合があります。
アプリケーションコンソールを使用できない (プロセスがバックグラウンドプロセスとして実行されているか、VM の出力が未知の場所に送信されている) 場合は、jstack ユーティリティーを使用してスタックスレッドを取得できます。ループしているプロセスのスタックダンプを強制するには、jstack -F pid オプションを使用します。このユーティリティーの出力については、「2.11 jstack ユーティリティー」を参照してください。Java スレッドがループしている証拠がスレッドダンプに示されない場合も、jstack ユーティリティーを使用するようにしてください。
jstack ユーティリティーの出力を確認するときは、まず RUNNABLE 状態のスレッドに注目してください。これは、ビジー状態でループしていると思われるスレッドが取る可能性がもっとも高い状態です。場合によっては、どのスレッドがループしているかを完全に把握するために、jstack を数回にわたって実行する必要があります。スレッドが常に RUNNABLE 状態であるように見える場合は、-m オプションを使用してネイティブフレームを出力すると、スレッドが実行している処理について追加のヒントを得ることができます。スレッドが RUNNABLE 状態のままで継続的にループしているように見える場合、この状況はより詳しい調査を必要とする潜在的 HotSpot VM バグを示している可能性があります。
VM が Ctrl + \ に応答しない場合、これはアプリケーションコードやライブラリコードの問題ではなく、VM のバグを示している可能性があります。この場合は、(-F オプションに加えて) -m オプションを指定した jstack を使用して、すべてのスレッドのスレッドスタックを取得します。この出力には、VM 内部のスレッドのスレッドスタックが含まれます。このスタックトレースで、待機しているように見えないスレッドを識別します。たとえば Solaris OS では、__lwp_cond_wait、__lwp_park、___pollsys などの関数、またはほかのブロック関数に入っていないスレッドを識別します。VM のバグによってループが発生しているように見える場合は、できるだけ多くのデータを収集して、バグレポートを提出してください。データ収集の詳細は、第 7 章「バグレポートの提出」を参照してください。
アプリケーションがハングアップし、プロセスがアイドル状態になっているように見える場合は、最初の手順はスレッドダンプを取得してみることです。アプリケーションコンソールを使用できる場合は、Ctrl + \ キー (Solaris OS または Linux の場合) または Ctrl + Break キー (Windows の場合) を押して、HotSpot VM にスレッドダンプを出力させることができます。Solaris OS および Linux では、SIGQUIT をプロセスに送信して (kill -QUIT
ハングアップしたプロセスがスレッドダンプを収集できる場合、その出力はターゲットプロセスの標準出力に出力されます。スレッドダンプの出力後、HotSpot VM はデッドロック検出アルゴリズムを実行します。デッドロックが検出された場合は、デッドロックに関与したスレッドのスタックトレースとともにそれが出力されます。この出力の例を以下に示します。
Found one Java-level deadlock: ============================= "AWT-EventQueue-0": waiting to lock monitor 0x000ffbf8 (object 0xf0c30560, a java.awt.Component$AWTTreeLock), which is held by "main" "main": waiting to lock monitor 0x000ffe38 (object 0xf0c41ec8, a java.util.Vector), which is held by "AWT-EventQueue-0" Java stack information for the threads listed above: =================================================== "AWT-EventQueue-0": at java.awt.Container.removeNotify(Container.java:2503) - waiting to lock <0xf0c30560> (a java.awt.Component$AWTTreeLock) at java.awt.Window$1DisposeAction.run(Window.java:604) at java.awt.Window.doDispose(Window.java:617) at java.awt.Dialog.doDispose(Dialog.java:625) at java.awt.Window.dispose(Window.java:574) at java.awt.Window.disposeImpl(Window.java:584) at java.awt.Window$1DisposeAction.run(Window.java:598) - locked <0xf0c41ec8> (a java.util.Vector) at java.awt.Window.doDispose(Window.java:617) at java.awt.Window.dispose(Window.java:574) at javax.swing.SwingUtilities$SharedOwnerFrame.dispose(SwingUtilities.java:1743) at javax.swing.SwingUtilities$SharedOwnerFrame.windowClosed(SwingUtilities.java:1722) at java.awt.Window.processWindowEvent(Window.java:1173) at javax.swing.JDialog.processWindowEvent(JDialog.java:407) at java.awt.Window.processEvent(Window.java:1128) at java.awt.Component.dispatchEventImpl(Component.java:3922) at java.awt.Container.dispatchEventImpl(Container.java:2009) at java.awt.Window.dispatchEventImpl(Window.java:1746) at java.awt.Component.dispatchEvent(Component.java:3770) at java.awt.EventQueue.dispatchEvent(EventQueue.java:463) at java.awt.EventDispatchThread.pumpOneEventForHierarchy(EventDispatchThread.java:214) at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:163) at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:157) at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:149) at java.awt.EventDispatchThread.run(EventDispatchThread.java:110) "main": at java.awt.Window.getOwnedWindows(Window.java:844) - waiting to lock <0xf0c41ec8> (a java.util.Vector) at javax.swing.SwingUtilities$SharedOwnerFrame.installListeners(SwingUtilities.java:1697) at javax.swing.SwingUtilities$SharedOwnerFrame.addNotify(SwingUtilities.java:1690) at java.awt.Dialog.addNotify(Dialog.java:370) - locked <0xf0c30560> (a java.awt.Component$AWTTreeLock) at java.awt.Dialog.conditionalShow(Dialog.java:441) - locked <0xf0c30560> (a java.awt.Component$AWTTreeLock) at java.awt.Dialog.show(Dialog.java:499) at java.awt.Component.show(Component.java:1287) at java.awt.Component.setVisible(Component.java:1242) at test01.main(test01.java:10) Found 1 deadlock.
デフォルトのデッドロック検出は、synchronized キーワードを使用して取得されたロック、および java.util.concurrent パッケージを使用して取得されたロックとともに動作します。Java VM のフラグ -XX:+PrintConcurrentLocks を設定すると、スタックトレースにロックの所有者のリストも表示されます。
デッドロックが検出された場合は、デッドロックを理解するために出力を詳細に調べる必要があります。上の例では、スレッド main がオブジェクト <0xf0c30560> をロックしており、スレッド AWT-EventQueue-0 によってロックされている 0xf0c41ec8 に入るのを待機しています。しかし、スレッド AWT-EventQueue-0 は main によってロックされている 0xf0c30560 に入るのを待機しています。
スタックトレースの詳細から、デッドロックを見つけるのに役立つ情報が得られます。
スレッドダンプが出力されたが、デッドロックが見つからなかった場合、この問題は通知されないモニターでスレッドが待機しているというバグである可能性があります。これは、タイミングの問題か、一般的なロジックのバグである可能性があります。
この問題についてさらに調べるには、スレッドダンプ内の各スレッドと、Object.wait() でブロックされた各スレッドを調べます。スタックトレース内の呼び出し側フレームは、wait() メソッドを呼び出しているクラスとメソッドを示しています。行番号情報付きでコードがコンパイルされている場合 (デフォルト) は、これによって調査すべきコードに関する手掛かりが提供されます。ほとんどの場合、この問題をより詳しく診断するには、アプリケーションロジックまたはライブラリに関してある程度の知識が必要です。一般に、アプリケーションでの同期動作、特にモニターがいつどこで通知されるかに関する詳細と条件について理解している必要があります。
VM が Ctrl + \ または Ctrl + Break に応答しない場合は、ほかのなんらかの理由で VM がデッドロックまたはハングアップした可能性があります。その場合は、jstack ユーティリティー (「2.11 jstack ユーティリティー」を参照) を使用してスレッドダンプを取得します。ハングアップしているプロセスのスタックダンプを強制するには、jstack -F pid オプションを使用します。これは、アプリケーションにアクセスできない場合や、出力が未知の場所に送信されている場合にも当てはまります。
jstack の出力で、BLOCKED 状態になっている各スレッドを調べます。場合によっては、最上位フレームにスレッドがブロックされている理由 (たとえば、Object.wait や Thread.sleep) が示されていることがあります。スタックの残りの部分から、スレッドが実行している処理についてのヒントが得られます。これは、行番号情報付きでソースがコンパイルされている場合 (デフォルト) に特に当てはまり、その場合はソースコードを相互参照できます。
スレッドが BLOCKED 状態になっていて、理由がはっきりしない場合は、-m オプションを使用して混合したスタックを取得します。混合したスタックの出力を使用して、スレッドがブロックされた理由を特定できるはずです。同期化メソッドまたはブロックに入ろうとしてスレッドがブロックされた場合は、スタックの最上位付近に ObjectMonitor::enter のようなフレームが表示されます。次に例を示します。
----------------- t@13 ----------------- 0xff31e8b8 ___lwp_cond_wait + 0x4 0xfea8c810 void ObjectMonitor::EnterI(Thread*) + 0x2b8 0xfeac86b8 void ObjectMonitor::enter2(Thread*) + 0x250 :
RUNNABLE 状態のスレッドもブロックされる可能性があります。混合したスタックの最上位フレームに、スレッドが実行している処理が示されているはずです。
チェックすべき特定スレッドの 1 つは VMThread です。これは、ガベージコレクションのような操作を実行するために使用される特別なスレッドです。その初期のフレームで VMThread::run() を実行しているスレッドとして識別できます。Solaris OS では通常、t@4 です。Linux では、C++ 分解名 _ZN8VMThread4loopEv を使用して識別できるはずです。
一般に VM スレッドは、VM 操作の実行を待機している、VM 操作に備えてすべてのスレッドを同期している、または VM 操作を実行している、の 3 つの状態のいずれかを取ります。ハングアップがアプリケーションやクラスライブラリのデッドロックではなく、HotSpot VM のバグであると疑われる場合は、VM スレッドに特別な注意を払ってください。
VM スレッドが SafepointSynchronize::begin でスタックしているように見える場合、これは VM がセーフポイントに移行する問題を示している可能性があります。セーフポイントは、VM 内で実行されているすべてのスレッドがブロックされ、ガベージコレクションなどの特殊な操作が完了するのを待機していることを示します。
VM スレッドが function でスタックしているように見えるが、function が doit で終了する場合、これは VM に問題があることを示している可能性もあります。
一般に、コマンド行からアプリケーションを実行できるが、VM が Ctrl + \ または Ctrl + Break に応答しない状態になる場合は、VM のバグ、スレッドライブラリの問題、または別のライブラリのバグを発見した可能性が高くなります。これが発生した場合は、クラッシュダンプを取得し (その実行方法については、「7.4 コアダンプの収集」を参照)、できるだけ多くの情報を収集して、バグレポートまたはサポートコールを提出してください。
ハングアップしたプロセスとの関連で説明すべきもう 1 つのツールは、Solaris OS の pstack ユーティリティーです。Solaris 8 および 9 OS では、このユーティリティーはターゲットプロセス内の LWP のスレッドスタックを出力します。Solaris 10 OS および JDK 5.0 リリース以降では、pstack の出力は jstack -m の出力に似ています (ただし、同じではありません)。Solaris 10 OS の pstack 実装は、jstack と同様に完全修飾クラス名、メソッド名、および BCI を出力します。また、行番号情報付きでソースがコンパイルされた場合 (デフォルト) は、行番号も出力します。これは、/proc ファイルシステムの機能を実行する Solaris OS 上のほかのユーティリティーに習熟している開発者や管理者にとって便利です。
Linux には、pstack と同等のツールとして lsstack があります。このユーティリティーは、一部のディストリビューションに含まれているほか、sourceforge.net Web サイトからも取得されます。これを執筆している時点では、lsstack はネイティブフレームのみを報告していました。
Solaris 8 OS 上のデフォルトのスレッドライブラリは多くの場合、T1 ライブラリと呼ばれます。このスレッドライブラリは m:n スレッドモデルを実装しました (m 個のユーザースレッドが n 個のカーネルレベルスレッド (LWP) にマップされます)。Solaris 8 OS には、代替の新しいスレッドライブラリも付属しており、/usr/lib/lwp にあります。代替スレッドライブラリは多くの場合、T2 ライブラリと呼ばれ、Solaris 9 および 10 OS ではデフォルトのスレッドライブラリになりました。古いリリース (特に 1.4.0 より前) の J2SE には、たとえば、スレッドライブラリ内のバグ、LWP の同期化に関する問題、LWP の枯渇など、デフォルトのスレッドライブラリにいくつかの問題がありました。LWP の枯渇は、RUNNABLE 状態のユーザースレッドはあるが、使用可能なカーネルレベルスレッドがないというシナリオです。
前述の問題は過去のものですが、Solaris 8 OS 上に JDK ソフトウェアを配備すると、今でもデフォルトで T1 ライブラリが使用されることに注目するようにしてください。JDK リリースでは "バインドされたスレッド" が使用され、ユーザースレッドがそれぞれ 1 つのカーネルスレッドにバインドされるため、LWP 枯渇タイプの問題は発生しません。しかし、スレッドライブラリの問題だと思われる問題 (ハングアップなど) が発生した場合は、LD_LIBRARY_PATH に /usr/lib/lwp を追加することで、T2 ライブラリを使用するように HotSpot VM に指示できます。T2 ライブラリが使用されているかどうかをチェックするには、pldd