Javaアプリのヒープメモリを定期的にチェックする

## リソースを有効に使うために
有限のリソースを有効に使うために、Javaアプリに割り当てるヒープメモリはどの程度がいいのだろう？と思い、まずは実際に使用している状況を簡単に確認してみようと思います。

## JMXを有効にする
Javaアプリのこういったリソース情報を得るにはJMXを使うのがいいようなので、まずはJMXを有効にします。JMXを有効にするためにはjava実行時のオプションに次の引数を渡せばいいようです。

```java
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=12345
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
```
なお、今回はセキュリティは考慮してません。本番で使う場合は検討する必要があります。


## JMX APIにアクセスする
早速JMX APIにアクセスしてみましょう。
GUIであれば、Java SDKについている jconsole を使うのが手っ取り早いようです。
が、定期的にヒープの情報をファイルに落とし、あとから傾向を掴みたいときはこの方法は不便です。
なので、コマンドラインで実行できるツールを探していたところ、 [cmdline-jmxclient](http://crawler.archive.org/cmdline-jmxclient/) なるものがあることがわかりました。実行方法・結果は以下のとおりです。

```shell-session
java -jar cmdline-jmxclient.jar - ターゲットサーバー:ポート番号 Bean名 コマンド名

04/06/2016 22:05:00 +0900 org.archive.jmx.Client HeapMemoryUsage:
committed: 270008320
init: 268435456
max: 477626368
used: 65294544
```

結果が複数行にわたって出てきてしまうため、これは個人的にちょっと使いにくいです。１行で１日１ファイルに書き出してもらう形にするため、PowerShellを使って整形することにしました。下記は上記コマンドを使って、複数台のサーバーにJMX APIでアクセスし、ヒープ情報を取得・ファイルに保存するスクリプトです。なお、このコマンドの実行結果はなぜか標準エラー出力に出力されるため、スクリプトでも標準エラー出力を見るようになっています。

```Get-Heap.ps1
$date = (Get-Date -Format d) -replace "/", ""
$jmxclient = "DRIVE:\PATH\TO\jmx\lib\cmdline-jmxclient-0.10.3.jar"
$java = "C:\Program Files\Java\jdk1.8.0_xx\bin\java.exe"

@("SERVER1", "SERVER2", "SERVER3") | ForEach-Object {
    $logfilepath = "DRIVE:\PATH\TO\jmx\logs\YOUR_LOG_NAME.$_.$date.tsv"
    $pinfo = New-Object System.Diagnostics.ProcessStartInfo
    $pinfo.FileName = $java
    $pinfo.RedirectStandardError = $true
    $pinfo.RedirectStandardOutput = $true
    $pinfo.UseShellExecute = $false
    $pinfo.Arguments = "-jar " + $jmxclient + " - " + "$_" + ":12345 java.lang:type=Memory HeapMemoryUsage"
    $p = New-Object System.Diagnostics.Process
    $p.StartInfo = $pinfo
    $p.Start() | Out-Null
    $p.WaitForExit()
    $stdout = $p.StandardOutput.ReadToEnd()
    $stderr = $p.StandardError.ReadToEnd()

    $arr = [Collections.ArrayList]($stderr -replace "(committed: |init: |max: |used: )", "" -split "`n")
    $arr.RemoveAt(0)
    $time = (Get-Date -Format s)
    $result = $arr.ToArray() -join "`t"
    if (-not (Test-Path $logfilepath)) {
        "timestamp`tcommitted`tinit`tmax`tused" | Out-File $logfilepath -Append -Encoding default
    }
    "$time`t$result" | Out-File $logfilepath -Append -Encoding default
}
```

Rubyスクリプトも置いておきます。

```get_heap.rb
require "open3"

DATE = Time.now.strftime("%Y%m%d")
LOGDIR = "DRIVE:/PATH/TO/jmx/logs/"

["SERVER1", "SERVER2", "SERVER3"].each do |server|
  LOGFILE = "#{LOGDIR}YOUR_LOG_NAME.#{server}.#{DATE}.tsv"
  JMXCLIENT = "DRIVE:/PATH/TO/jmx/lib/cmdline-jmxclient-0.10.3.jar"

  o, e, s = Open3.capture3("java -jar #{JMXCLIENT} - #{server}:7989 java.lang:type=Memory HeapMemoryUsage")
  timestamp = Time.now.strftime("%Y/%m/%d %H:%M:%S")
  result = e.gsub(/(committed: |init: |max: |used: )/,"").split("\n")
  result.shift
  file_exists = File.exists?(LOGFILE)
  File.open(LOGFILE, "a") do |f|
    f.puts "timestamp\tcommitted\tinit\tmax\tused" unless file_exists
    f.puts "#{timestamp}\t#{result.join("\t")}"
  end
end
```

## 定期的に実行する
上記をタスクスケジューラーで定期的に実行するため、バッチファイルにします。

```Get-Heap.bat
powershell Get-Heap.ps1
```

タスクスケジューラーに登録します。
普通にGUIから登録してもいいのですが、パスワードを聞かれたりシて面倒なので、PowerShellで登録も行います。

```Set-TaskScheduler.ps1
$STPrin = New-ScheduledTaskPrincipal -UserId "LOCALSERVICE" -LogonType ServiceAccount
$Action = New-ScheduledTaskAction -Execute "YOUR ACTION HERE"
$Trigger = New-ScheduledTaskTrigger -Daily -At (Get-Date)
$Task = Register-ScheduledTask -TaskName "YOUR TASK NAME" -Trigger $Trigger -Action $Action -Principal $STPrin
$Task.Triggers.Repetition.Duration = "P1D"
$Task.Triggers.Repetition.Interval = "PT1M"
$Task | Set-ScheduledTask
```

これで毎日１分間隔でヒープ情報を取ることができるようになりました。
リソース等管理系に関する情報が簡単に取れる仕組みが標準であるというのはうれしいですね。

（補足）
毎日実行するとファイルが日付分増えていきます。
Linuxであれば簡単に改廃できますが、Windowsの場合はちょっとした工夫が必要です。私はよく下記バッチを使って改廃を行なっています。

```bat
@echo off
REM --- Set Parameters
SET LOG_PATH=DRIVE:_PATH_TO_LOGFOLDER_
REM --- Days to keep logs
SET LOG_GEN=30

REM --- DELETE AN OLD LOGFILE
for /f "skip=%LOG_GEN%" %%A in ('dir /b /o-n "%LOG_PATH%"') do del /q %LOG_PATH%%%A
```


## 参考

今回もまたいろんなサイトを参考にさせていただきました。この場を借りてお礼申し上げます。