Akira's Tech Notes

Java/JVM | GNU/Linux | Emacs/Lisp | 知的好奇心駆動

header-icon
ネイティブでない日本語で思い付くことや気になることをダラダラ書く、体裁とかは気にしない。読みづらいと感じた時に随時更新する。

[小道具][Java]スタックサイズ監視スクリプト

/proc/<pid>/smaps プロセスメモリマッピングファイルからjavaスレッドのスタック使用状 況をいい感じに出力するスクリプトを作りました。

1 環境

  • java version “1.8.0_45”
  • x86_64 gnu/linux
  • /proc/<pid>/smaps にスタックセグメントが次のように [stack:スレッドID] ラベルが出 力されていること(Linux 3.4 以降)

    7f3984233000-7f3984331000 rw-p 00000000 00:00 0                          [stack:2988]
    Size:               1016 kB
    Rss:                  92 kB
    Pss:                  92 kB
    Shared_Clean:          0 kB
    Shared_Dirty:          0 kB
    Private_Clean:         0 kB
    Private_Dirty:        92 kB
    Referenced:           92 kB
    Anonymous:            92 kB
    AnonHugePages:         0 kB
    Swap:                  0 kB
    KernelPageSize:        4 kB
    MMUPageSize:           4 kB
    Locked:                0 kB
    VmFlags: rd wr mr mw me ac
    

2 スクリプト

スクリプトの元ネタは以前の記事を参考してください。 jvmのスタックサイズについて

#!/bin/sh

###########################################################################
# jvm_stacksize.sh - take a jvm stack size snapshot
#
#    Authors: Akira Wakana <jalen.cn@gmail.com>
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, version 3 of the License.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see http://www.gnu.org/licenses
#
# Usage:
#    $jvm_stacksize.sh <JVM ProcessID>
###########################################################################

PID=$1

ps ${PID} | grep [j]ava > /dev/null || {
    echo "-----------------------------------------------------------------------"
    ps -ef  | grep [j]ava
    echo "-----------------------------------------------------------------------"
    echo -n "please input the java process id: "
    read PID
}

printf "[ PID ]\t[StackSize(kB)]\t[GuardPage(kB)]\t[UsedSize(kB)]\t[Thread Name]\n"

# jstackの出力結果からスレッドIDと名前を抽出する
jstack ${PID} | grep nid | sed -e "s/^\"\(.*\)\".*nid=\(0x[0-9|a-z]*\).*$/\2,\1/" | sort | while read line
do
    # スレッドIDを切り出す
    pid_hex=`echo "${line}" | awk -F"," '{print $1}'`

    # スレッド名を切り出す
    thread_name=`echo "${line}" | awk -F"," '{print $2}'`

    # スレッドIDを10進数に変換
    pid=`printf '%d\n' ${pid_hex}`

    # /proc/<pid>/smaps ファイルからスタックサイズ、ガードページサイズを取得する
    # 次のsmaps出力サンプルに★コメントの部分は取得詳細ロジックとなります。
    #
    #     7ff751076000-7ff751079000 ---p 00000000 00:00 0
    #     Size:                 12 kB    ★説明: StackRedPages(4Kb) + StackYellowPages(8Kb) = 12Kb
    #     Rss:                   0 kB
    #     Pss:                   0 kB
    #     Shared_Clean:          0 kB
    #     Shared_Dirty:          0 kB
    #     Private_Clean:         0 kB
    #     Private_Dirty:         0 kB
    #     Referenced:            0 kB
    #     Anonymous:             0 kB
    #     AnonHugePages:         0 kB
    #     Swap:                  0 kB
    #     KernelPageSize:        4 kB
    #     MMUPageSize:           4 kB
    #     Locked:                0 kB
    #     VmFlags: mr mw me ac          ★説明:書き込み不可
    #     7ff751079000-7ff751177000 rw-p 00000000 00:00 0                          [stack:21275]
    #     Size:               1016 kB   ★説明: ここからNormal Stackページ
    #     Rss:                 108 kB
    #     Pss:                 108 kB
    #     Shared_Clean:          0 kB
    #     Shared_Dirty:          0 kB
    #     Private_Clean:         0 kB
    #     Private_Dirty:       108 kB
    #     Referenced:          108 kB
    #     Anonymous:           108 kB
    #     AnonHugePages:         0 kB
    #     Swap:                  0 kB
    #     KernelPageSize:        4 kB
    #     MMUPageSize:           4 kB
    #     Locked:                0 kB
    #     VmFlags: rd wr mr mw me ac
    guard_page=`cat /proc/${PID}/smaps | grep -B16 "stack:${pid}" | grep -e "^Size:" | awk '{print $2}'`
    stack_page=`cat /proc/${PID}/smaps | grep -A15 "stack:${pid}" | grep -e "^Size:" | awk '{print $2}'`
    used_size=`cat /proc/${PID}/smaps  | grep -A15 "stack:${pid}" | grep -e "^Rss:"  | awk '{print $2}'`
    stack_size=`expr ${guard_page} + ${stack_page}`
    printf "%7d\t%15s\t%15s\t%14s\t%s\n" "${pid}" "${stack_size}" "${guard_page}" "${used_size}" "${thread_name}"
done

3 テスト用プログラム

package test01;

public class TestTask implements Runnable {

    @Override
    public void run() {
        add1(1);
    }

    private int add1(int a) {
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return add2(a++);
    }

    private int add2(int a) {
        //add1 -> add2 --> add1 無限ループの状態で
        //スタックを食い潰す
        return add1(a--);
    }

    public static void main(String[] args) throws Exception {

        Thread t1 = new Thread(new TestTask());
        Thread t2 = new Thread(new TestTask());
        t1.start();
        t2.start();

        t1.join();
        t2.join();
    }

}

4 監視してみる

5 出力形式

[ PID ]	[StackSize(kB)]	[GuardPage(kB)]	[UsedSize(kB)]	[Thread Name]
  16559            1044              12            116	main
  16560            1112               4             92	GC task thread#0 (ParallelGC)
  16561            1028               4              8	GC task thread#1 (ParallelGC)
  16562            1028               4              8	GC task thread#2 (ParallelGC)
  16563            1028               4              8	GC task thread#3 (ParallelGC)
  16564           12336               4             56	VM Thread
  16565            1028              12             88	Reference Handler
  16566            1028              12             88	Finalizer
  16567            1028              12             92	Signal Dispatcher
  16568            1028              12             20	C2 CompilerThread0
  16569            1028              12              8	C2 CompilerThread1
  16570            1028              12             12	C1 CompilerThread2
  16571            1028              12              8	Service Thread
  16572            1028               4              8	VM Periodic Task Thread
  16573            1028              12            428	Thread-0
  16583            1028              12            228	Thread-1
  16613            1028              12             12	Attach Listener
項目名 説明
PID JavaスレッドのプロセスID
StackSize Javaスタック+Nativeスタックのサイズ (kB)
GuardPage スタック保護ページ (kB)
UsedSize 実際使った物理メモリサイズ (kB)
Thread Name スレッド名

バッチリ!便利な道具が1つ増えました。

6 2015/08/05 更新

前の実装は watch コマンドでスクリプトを繰り返し実行させスタックサイズの監視をしてい たのですが、繰り返しサイクルで毎回jstackでスレッドダンプを取得しているため若干JVMにオー バヘッドがかかる。特にスレッドを多数起動されたアプリケーションサーバの場合性能へ影響 が無視できないので、少しスクリプトの実装シナリオを変えて改善してみました。

基本コンセプトは

  1. jstack を1回のみ実行する、その結果(スレッドダンプ)をtmpファイルで保持する
  2. ps -H <PID> でスレッドIDを取得して、1のスレッドダンプからスレッド名を引き
  3. スレッドダンプから該当するものがない場合、スレッドダンプが古いと判断し再度 jstack で新しいスレッドダンプを取る
#!/bin/sh
###########################################################################
# jvm_stacksize.sh - take jvm stack size snapshot
#
#    Authors: Akira Wakana <jalen.cn@gmail.com>
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, version 3 of the License.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# Usage:
#    $jvm_stacksize.sh <JVM ProcessID>
###########################################################################

print_stacksize()
{
    rm -rf ${tmpdir}/stacksize.txt
    printf "[ PID ]\t[StackSize(kB)]\t[GuardPage(kB)]\t[UsedSize(kB)]\t[Thread Name]\n"
    ps h -L --format=lwp ${PID} | grep -v "${PID}" | while read pid
    do
        # スレッドID
        pid_hex=`printf '%#x\n' $pid`

        # スレッド名を切り出す
        threadinfo=`cat ${threadtdump} | fgrep " nid=${pid_hex} " | sed -e "s/^\"\(.*\)\".*nid=\(0x[0-9|a-z]*\).*$/\2,\1/"`
        if [ "${threadinfo}" == "" ]; then
            jstack ${PID} > ${threadtdump}
            threadinfo=`cat ${threadtdump} | fgrep " nid=${pid_hex} " | sed -e "s/^\"\(.*\)\".*nid=\(0x[0-9|a-z]*\).*$/\2,\1/"`
        fi
        thread_name=`echo "${threadinfo}" | awk -F"," '{print $2}'`

        # # /proc/<pid>/smaps ファイルからスタックサイズ、ガードページサイズを取得する
        guard_page=`cat /proc/${PID}/smaps | grep -B16 "stack:${pid}" | grep -e "^Size:" | awk '{print $2}'`
        stack_page=`cat /proc/${PID}/smaps | grep -A15 "stack:${pid}" | grep -e "^Size:" | awk '{print $2}'`
        used_size=`cat /proc/${PID}/smaps  | grep -A15 "stack:${pid}" | grep -e "^Rss:" | awk '{print $2}'`
        stack_size=`expr ${guard_page} + ${stack_page}`
        printf "%7d\t%15s\t%15s\t%14s\t%s\n" "${pid}" "${stack_size}" "${guard_page}" "${used_size}" "${thread_name}"
    done > ${tmpdir}/stacksize.txt
    sort -r -k4,4 ${tmpdir}/stacksize.txt
}

finally_func() {
    RET=$?
    if [ -d ${tmpdir} ]; then
        rm -rf ${tmpdir}
    fi
    exit ${RET}
}

trap finally_func EXIT

PID=$1

ps ${PID} | grep [j]ava > /dev/null || {
    echo "-----------------------------------------------------------------------"
    ps -ef  | grep [j]ava
    echo "-----------------------------------------------------------------------"
    echo -n "please input the java process id: "
    read PID
}

export PID

export tmpdir=`mktemp -d`

export threadtdump=${tmpdir}/${PID}.tdump

jstack ${PID} > ${threadtdump}

export -f print_stacksize

watch "bash -c print_stacksize"

rm -rf ${tmpdir}

一般的なアプリでは実行中スレッドの数が頻繁に変わらないので、これで満足しています。;-)

7 参考

Comments