コラム: ネイティブスレッドとJavaスレッドのマッピング



ネイティブスレッドとJavaスレッドのマッピング

本コラムの対象:

「JavaVMが発行しているシステムコールを見てみよう!」という気になる方。
ネイティブな世界に足を踏み入れている方、または踏み入れようとしている方。

0.はじめに

 ある程度の規模を持ったJavaアプリケーションを作成すると、あるときパフォーマンス問題が眼前に立ちはだかることがあります。 さて、どう料理しましょうか。
 hprofなどのプロファイラを使用することがまず考えられますが、場合によってはJavaVMが発行しているシステムコールの種類や数をOS側から観察することが最短経路であることもあります。
後者の道を選んだとき、さらに次の問題が立ちはだかります。「JavaVMプロセス内でCPUリソースを浪費しているスレッドは特定 できた。ではこのスレッドは、どのJavaスレッドに対応するのか?」

 このコラムでは、オペレーティングシステムからJavaアプリケーションおよびJavaVMの動作を観察するために必要となる「Javaスレッド に対応したネイティブスレッドを見つける方法」を紹介します。

 このコラムの構成は以下の通りです。

今回の内容
◆ 1.SolarisとJavaVMのスレッドモデル
Solarisオペレーティングシステム(以下、Solaris)マルチスレッド・プロセス・アーキテクチャ とJavaVMスレッドモデルの基本的な知識をおさらいします。

◆ 2.ネイティブスレッドからJavaスレッドを見つける方法
特定のネイティブスレッドに対応するJavaスレッドを見つける方法を紹介します。



1.SolarisとJavaVMのスレッドモデル

 まずこの章では、Solarisのマルチスレッド・プロセス・アーキテクチャとJavaVMのスレッドモデルについておさらいします。
 網羅的な説明はそのボリューム上不可能ですので、最低限知っていなければならないことにとどめていることに注意してください。
 

1-1.Solarisマルチスレッド・プロセス・アーキテクチャ


 Solarisのスレッドモデルはカーネルレベルとユーザレベルの2段階で構成されており、おおよそ下の図のようになっています。


Solarisスレッドモデル


 カーネルスレッドとはカーネルがスケジューリング対象とする唯一のスレッドであり、逆に言うとユーザスレッドはカーネルからは見ることができません。  ユーザプロセスはLWP(ライトウェイトプロセス:軽量プロセス)によりカーネルスレッドとリンクされることで、カーネルのスレッドスケジューリングに その身を委ねます。(※)

 ここでLWPとユーザスレッドはn対nの関係でリンクしており、ユーザスレッドはカーネルスレッドのスケジューリング とは独立した優先度メカニズムを持っています。つまりユーザスレッドのスケジューリングは(カーネルレベルではなく)スレッドライブラリのレベルで行われます。
 このLWPをはさんだ2層スレッドモデルにより、カーネルは数百~数千のユーザスレッドを持つプロセスをスケジューリングするという過重労働 から開放され、ほどよい並列実行性を保つことができるようになります。

 さて、上の議論によりユーザスレッドはLWPを経由してカーネルからCPUリソースを割り当てられることが分かったわけですが、これはつまり ユーザスレッドがシステムコールの実行をカーネルに要求したときにそのシステムコールを実行するのはLWPであることを意味します。
psコマンドやprstatコマンド、trussコマンドでCPUリソースの消費率やシステムコールの実行ログを調べたとき、その最小単位がLWP単位である理由がここにあります。

 OS側からパフォーマンスの計測をするということはつまり高い負荷をもつLWPを特定するということであり、こうして我々の関心は「このLWPに対応するJavaスレッドは誰か?」 というものに移っていきます。

 (※) 中にはLWPと対応しないカーネルスレッドが存在しますが、これはカーネル自身もマルチスレッドで動作しているためであり、LWPを持たないカーネルスレッド はカーネル自身の処理を行なうスレッドだと言うことができます。


1-2.JavaVMスレッドモデル


 前節のSolarisスレッドモデルを理解しさえすれば、JavaVMのスレッドモデルは非常に簡単です(下図)。


JavaVMスレッドモデル


 この図のとおり、Javaスレッド(java.lang.Thread)はユーザスレッドと1対1対応をなします。

 問題はユーザスレッドとLWPの関係ですが、これはSolarisのバージョンとJavaVMのバージョン、JavaVMのオプションにより変化します。 Solaris8上でJava1.4をデフォルトオプションで使用した場合、ユーザスレッドとLWPとの関係は多対多となり、 その比率は1対1となります(ユーザスレッドが直接LWPに結び付けられるわけではなく、各ユーザスレッドはLWPを渡り歩くかもしれません)。

 他のSolaris/JavaVMのバージョン、オプション指定時の動作については、Sunが発行しているドキュメント を参照してください。

2.ネイティブスレッドからJavaスレッドを見つける方法

 前置きが長くなりました。本題です。
 実際に特定のネイティブスレッド、つまりLWPに関連するJavaスレッドを見つける方法を紹介します。
2-1.高負荷をもつLWPの特定


 負荷の高いLWPを特定する方法を解説しきることは本コラムの趣旨から外れますので、説明に最低限必要なことのみを説明します。

 負荷の高いLWPを特定する最も簡単な方法は、prstatコマンドを使うことです。prstatコマンドを「-L」オプション付きで実行すると、LWP単位でプロセス情報 を参照することができます。



> prstat -L
   PID USERNAME  SIZE   RSS STATE  PRI NICE      TIME  CPU PROCESS/LWPID
   542 nozaki    128M   34M sleep   58    0   0:00.03 6.8% java/27
    95 root     7728K 6768K sleep   58    0   0:00.25 0.5% picld/8
    95 root     7728K 6768K sleep   58    0   0:00.12 0.2% picld/10
   547 nozaki    132M   42M sleep   58    0   0:00.06 0.2% java/4
   545 nozaki     71M   31M sleep   58    0   0:00.04 0.2% java/4

  ~略~


 第1カラムがプロセスID、第10カラムが、プロセス名とLWP番号です。上の例では、プロセスID=542であるjavaプロセスのLWP27が、 CPUを6.8%使用していることが分かります。
 以降、このLWP番号に注目して、ネイティブスレッドとJavaスレッドを追いかけることになります。

2-2.ネイティブレベルのスタックトレース取得

 prstatコマンドで得たプロセスIDを使用し、pstackコマンドによりLWPのスタックトレースを取得します。


> pstack 542
542:    /usr/j2sdk1.4.1_01/bin/java -Xms64m -Xmx128m -Duser.tmp=/export/home/s
-----------------  lwp# 1 / thread# 1  --------------------
 ff29f008 lwp_cond_wait (2cff0, 2cfd8, 0)
 ff29654c _lwp_cond_wait (2cff0, 2cfd8, 351294, 1, 1bb9c, fe18ba14) + c
 fe0d30a0 __1cHMonitorEwait6Mil_i_ (2cfa8, 0, 0, 0, fe438d7c, 0) + 364

  ~略~

-----------------  lwp# 27 / thread# 26  --------------------
 ff29f008 lwp_cond_wait (2f3c78, 2f3c60, 0)
 ff29654c _lwp_cond_wait (2f3c78, 2f3c60, 0, fa414480, 0, 0) + c
 fe0e7ac8 __1cNObjectMonitorEwait6MxipnGThread__v_ (2f3c28, 0, 5400, 57f0, 2f3a58, 5270) + 61c
 fe0e70b8 __1cSObjectSynchronizerEwait6FnGHandle_xpnGThread__v_ (ebc815fc, 0, 0, 2f3a58, ee6d0000, 0) + 180
 fe0e6db8 JVM_MonitorWait (2f3ae4, ebc816f4, 0, 0, 0, 0) + 130
 fa40bbc8 ???????? (ee2c5590, 0, ebc8176c, fa4141e8, 0, ebc81690)
 fa405c64 ???????? (ee2c5590, 3, ebc817fc, fa415064, 0, ebc81710)
 fa405c64 ???????? (ee2c5590, f63b72c8, ebc8188c, fa415030, 0, ebc81798)
 fa405fd8 ???????? (ee2e0230, ebc81900, ebc81904, fa4154b0, 0, ebc81830)
 fa405c64 ???????? (ebc81908, 0, 0, fa415030, 35d1c8, ebc818a8)
 fa400118 ???????? (ebc81994, ebc81c08, a, f63e7a58, fa40aae0, ebc81b28)
 fe0d4c10 __1cJJavaCallsLcall_helper6FpnJJavaValue_pnMmethodHandle_pnRJavaCallArguments_pnGThread__v_ (ebc81c00, ebc81a60, ebc81b20, 2f3a58, 2f3a58, ebc81a74) + 25c
 fe0e695c __1cJJavaCallsMcall_virtual6FpnJJavaValue_nLKlassHandle_nMsymbolHandle_4pnRJavaCallArguments_pnGThread__v_(fe424000, 3293a0, ebc81b14, ebc81b10, ebc81b20, 2f3a58) + 190
 fe0e67bc __1cJJavaCallsMcall_virtual6FpnJJavaValue_nGHandle_nLKlassHandle_nMsymbolHandle_5pnGThread__v_ (ebc81c00, ebc81bfc, ebc81bf0, ebc81be8, ebc81be0, 2f3a58) + 60
 fe0e6744 __1cMthread_entry6FpnKJavaThread_pnGThread__v_ (2f3a58, 2f3a58, 672930, 3293a0, 33dabc, fe0e642c) + 1d8
 fe0e6454 __1cKJavaThreadDrun6M_v_ (2f3a58, 0, 0, 0, 0, 0) + 254
 fe0d33e4 _start   (2f3a58, ff37f688, 1, 1, ff37e000, 0) + 20
 ff36b728 _thread_start (2f3a58, 0, 0, 0, 0, 0) + 40

 各スタックフレームの一行目に注目します。「lwp# ○○」となっている数字(10進数)が、先ほどprstatコマンドで取得したLWP番号にあたります。
 prstatコマンドによりCPU負荷の面から着目したいLWPが特定できている場合はネイティブレベルのスタックトレース情報は必須なものではありませんが、 特定のシステムコールを発行しているLWPに着目したい場合などはpstackで得られるスタックトレースが有用な情報源となります。

2-3.Java言語レベルのスタックトレース取得


 prstatコマンド、あるいはpstackコマンドにより、注目すべきLWPとその番号が分かりました。さてこのLWPに対応するjavaスレッドは誰でしょうか。 javaのスタックトレース情報を取得し、LWPとjavaスレッドとのマッピングを取ります。

 JavaVMプロセスにSIGQUITシグナルを送信すると、JavaVMはスタックトレースを標準出力に出力します。
 このときJavaVMの起動オプションに、シグナルハンドラを無効化する「-rs」オプションが指定されていると、 スタックトレースを出力せずにcoreダンプを行ないますので注意してください。


Full thread dump Java HotSpot(TM) Client VM (1.4.1_01-b01 mixed mode):

"ED_receive_2" daemon prio=5 tid=0x303e58 nid=0x1d in Object.wait() [eb981000..eb981994]
    at java.lang.Object.wait(Native Method)
    - waiting on  (a jp.co.Acroquest.gms.event.io.buffering.PriorityFileBufferedIOStream)
    at java.lang.Object.wait(Object.java:426)
    
  ~略~
    
"ED_receive_1" daemon prio=5 tid=0x2f3dd8 nid=0x1b in Object.wait() [ebb81000..ebb81994]
    at java.lang.Object.wait(Native Method)
    - waiting on  (a jp.co.Acroquest.gms.event.io.buffering.PriorityFileBufferedIOStream)
    at java.lang.Object.wait(Object.java:426)
    at jp.co.Acroquest.gms.event.io.buffering.PriorityFileBufferedIOStream.read(PriorityFileBufferedIOStream.java:142)
    - locked  (a jp.co.Acroquest.gms.event.io.buffering.PriorityFileBufferedIOStream)
    at jp.co.Acroquest.gms.event.io.BufferedContentsDispatcher$DispatchThread.readLoop(BufferedContentsDispatcher.java:237)
    at jp.co.Acroquest.gms.event.io.BufferedContentsDispatcher$DispatchThread.run(BufferedContentsDispatcher.java:302)

"ED_send_1" daemon prio=5 tid=0x2f3a58 nid=0x1a in Object.wait() [ebc81000..ebc81994]
    at java.lang.Object.wait(Native Method)
    - waiting on  (a jp.co.Acroquest.gms.event.io.buffering.PriorityFileBufferedIOStream)

 取得したスタックトレースから、各スレッドの先頭行にある「nid=0xXX」という16進数に注目します。  この16進数が、prstatコマンドやpstackコマンドで取得したLWP番号(10進数)の16進数表現となります。 今回の例では、CPUを6.8%消費しているLWP番号27のスレッドは、Javaの世界では○○メソッドを実行しているスレッドであることが分かります。

 あとは実際のJavaコードを調査することになります。メソッド内にネストしたループ構造があったり、パフォーマンス上 ご法度とされているコードを見つけることができればしめたもの。あとは料理するだけです。
 そこまで上手くいかなくとも、少なくともOSに高い負荷をかけているスレッドは特定できます。 パフォーマンス解析を行う上で、これは非常に大きな収穫です。
4.まとめ

 さて、これでJavaの世界とネイティブの世界とをつなぐことができました。ここから本番、あなたの腕の見せ所です。
 ネイティブワールドで得られた膨大なパフォーマンス解析情報を元に、JavaVM内でくすぶっているJavaスレッドににカツをいれてやりましょう!

Page Top

注意:本文書の内容に誤りがあり、またこの文書によって不利益を被っても、
Acroquest Technology 株式会社は一切関知いたしません。

  • 現象別Index
  • 原因別Index

Find Bugsバグ詳細

Find Bugs Bug Descriptions日本語版

RSSで更新情報を取得する

RSSとは、ホームページの更新情報を配信する為のフォーマットです。
RSSを利用すると、登録したページの情報が更新された場合に、更新情報を自動的に受け取る事ができます。

詳細

弊社小森が執筆致しました

Javaでオブジェクト指向開発

Javaプログラミング言語習得において、新人プログラマーの最初の障害は「オブジェクト指向の壁」です。
本書は、Javaのソフトウェア開発を中心に事業を発展させてきたAcroquest社の新人教育セミナーを加筆・書籍化したもので、大卒の新人に対して、ゼロからJava言語を教えてきた実績をフィードバックしています。

メールマガジン配信中

Javaトラブルシューティングのメルマガをはじめました!是非ご購読ください

詳細