タツノオトシゴのブログ

主にJavaに関するものです。

JavaFXによるJVMのヒープサイズのグラフ化

需要はないいけど、JavaFXによるJavaVMのヒープサイズの可視化するツールを作ります。
こんな機能は「jconsole」やGCログを出して「GCViewer」で見ればいいじゃんと思いますが、設定とか面倒だったり、気軽にできなかったり、JDKの入っていないJREのみの環境の時に使用できたら便利と思い作りました。

仕様・要件

  • メモリの使用量などは、MXBeanを使用して取得する。
  • リアルタイムに取得して、可視化する。
    • 一定時間(1秒ごと)に情報を取得する。
    • 古い情報は捨てるようにする。
  • グラフ化はJavaFXのLineChartを利用します。
    • 使用済みのヒープ容量(useed)と確保済みのヒープ容量(committed)を表示します。
  • オプション機能として、GCの実行や、監視の停止・再開などができるようにする。


プログラム

FXML

今回は、FXMLで画面をデザインします。Scene Builderで適当に配置していきます。

  • 今回は、ボタンの活性化・非活性化などを制御するため、buttonにもidを振ります。
  • 更新対象のラベルにもidを振ります。
<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import javafx.collections.*?>
<?import javafx.scene.chart.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.*?>

<VBox fx:id="myNode" prefWidth="637.0" xmlns:fx="http://javafx.com/fxml" fx:controller="proj.green.srcgen.gui.MemoryGraphPaneController">
  <!-- TODO Add Nodes -->
  <children>
    <AnchorPane prefHeight="124.0" prefWidth="569.0">
      <children>
        <Button fx:id="stopButton" layoutX="97.0" layoutY="28.0" mnemonicParsing="false" onAction="#handleStopWatch" text="停止" />
        <Button fx:id="startButton" layoutX="29.0" layoutY="28.0" mnemonicParsing="false" onAction="#handleStartWatch" text="再開" />
        <Button fx:id="executeGcButton" layoutX="170.0" layoutY="28.0" mnemonicParsing="false" onAction="#handleExecuteGc" text="GC実行" />
        <Label fx:id="initHeapSizeLabel" layoutX="478.0" layoutY="19.0" text="Label" />
        <Label fx:id="maxHeapSizeLabel" layoutX="479.0" layoutY="38.0" text="Label" />
        <Text layoutX="284.0" layoutY="30.0" strokeType="OUTSIDE" strokeWidth="0.0" text="初期ヒープサイズ(-Xms)" />
        <Text layoutX="285.0" layoutY="49.0" strokeType="OUTSIDE" strokeWidth="0.0" text="最大ヒープサイズ(-Xmx)" />
        <Text layoutX="284.0" layoutY="73.0" strokeType="OUTSIDE" strokeWidth="0.0" text="現在までの最大使用量(used)" />
        <Text layoutX="285.0" layoutY="95.0" strokeType="OUTSIDE" strokeWidth="0.0" text="現在までの最大確保量(committed)" />
        <Label fx:id="maxUsedHeapSizeLabel" layoutX="478.0" layoutY="62.0" text="Label" />
        <Label id="maxCommittedSizeLabel" fx:id="maxCommittedHeapSizeLabel" layoutX="479.0" layoutY="84.0" text="Label" />
      </children>
    </AnchorPane>
    <LineChart fx:id="memoryChart" prefWidth="662.0">
      <xAxis>
        <CategoryAxis id="" fx:id="xAxis" label="経過時間" side="BOTTOM">
          <categories>
            <FXCollections fx:factory="observableArrayList" />
          </categories>
        </CategoryAxis>
      </xAxis>
      <yAxis>
        <NumberAxis fx:id="yAxis" label="メモリ(MB)" side="LEFT" />
      </yAxis>
    </LineChart>
  </children>
</VBox>

Controllerの作成

  • initizlize()メソッドで、JavaFX関連のオブジェクトや各インスタンス変数の初期化を行います。
    • 今回はグラフデータは、固定値ではないので、グラフデータの値「javafx.scene.chart.XYChart

インスタンス変数で持ちます。

    • MXBeanのインスタンス変数も取得しておきます。
    • また、ヒープの初期サイズ(-Xms)や最大サイズ(-Xmx)は変わらないので、initialize()メソッドで取得しておきます。
  • startMemoryWatch()メソッドで監視の開始をします。
    • グラフのリアルタイム描画には、「javafx.animation.Timeline」を使用します。このクラスは指定した一定間隔で実行します。
    • 処理の定期実行をするならば「java.util.TimerTask」を利用すればよいかもしれませんが、Chartクラスに関しては「JavaFX関連以外のスレッドから更新すると例外「java.lang.IllegalStateException」が発生」します。
  • addChartData()メソッドで、実際のグラフデータを更新します。
    • MXBeanからヒープ情報を取得します。
    • 一定個数(過去10回分)を超えた情報は、削除します。XYChart.Dataクラスはリストなので、先頭のデータを削除します。
    • 公式サイト「http://docs.oracle.com/javafx/2/charts/bar-chart.htm」に説明が少しあります。
  • メッセージダイアログの表示は、別ライブラリ「jfxmessagebox」を使用してします。
/// TimerTaskでグラフを更新した場合例外が発生する
Exception in thread "Timer-0" java.lang.IllegalStateException: Not on FX application thread; currentThread = Timer-0
	at com.sun.javafx.tk.Toolkit.checkFxUserThread(Toolkit.java:237)
	at com.sun.javafx.tk.quantum.QuantumToolkit.checkFxUserThread(QuantumToolkit.java:397)
/*
 * MemoryGraphPaneController.java
 * created in 2013/05/25
 *
 * (C) Copyright 2003-2013 GreenDay Project. All rights reserved.
 */
package proj.green.srcgen.gui;

import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.ResourceBundle;

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.util.Duration;
import jfx.messagebox.MessageBox;

import org.apache.commons.io.FileUtils;


public class MemoryGraphPaneController {
    
    @FXML
    private VBox myNode;
    
    // ヒープのラベル情報 ////////
    @FXML
    private Label initHeapSizeLabel;
    
    @FXML
    private Label maxUsedHeapSizeLabel;
    
    @FXML
    private Label maxCommittedHeapSizeLabel;
    
    @FXML
    private Label maxHeapSizeLabel;
    ////////////////////////
    
    /// ボタン //////////////
    @FXML
    private Button startButton;
    
    @FXML
    private Button stopButton;
    
    @FXML
    private Button executeGcButton;
    ///////////////////////
    
    /// グラフ /////////////////////////
    @FXML
    private LineChart<String, Long> memoryChart;
    
    @FXML
    private CategoryAxis xAxis;

    @FXML
    private NumberAxis yAxis;
    ////////////////////////////////
    
    @FXML
    private ResourceBundle resources;
    
    // グラフデータ
    private XYChart.Series<String, Long> dataUsed;
    private XYChart.Series<String, Long> dataCommitted;
    
    private Timeline timeline;
    
    private MemoryMXBean mxbean;
    
    /** 現在までの使用済みの最大ヒープサイズ */
    private long maxUsedHeapSize;
    
    /** 現在までの確保済みの最大ヒープサイズ */
    private long maxCommittedHeapSize;
    
    @FXML
    public void initialize() {
        
        memoryChart.setTitle("Javaのメモリ使用量");
        memoryChart.setCreateSymbols(true);
        
        // グラフデータ(使用しているヒープサイズ)
        this.dataUsed = new XYChart.Series<>();
        dataUsed.setName("ヒープ(used)");
        memoryChart.getData().add(dataUsed);
        
        // グラフデータ(確保しているヒープサイズ)
        this.dataCommitted = new XYChart.Series<>();
        dataCommitted.setName("ヒープ(comitted)");
        memoryChart.getData().add(dataCommitted);
        
        // MXBeanの取得
        this.mxbean = ManagementFactory.getMemoryMXBean();
        final MemoryUsage memoryUsage = mxbean.getHeapMemoryUsage();
        
        // ラベルの初期化
        this.initHeapSizeLabel.setText(FileUtils.byteCountToDisplaySize(memoryUsage.getInit()));
        this.maxHeapSizeLabel.setText(FileUtils.byteCountToDisplaySize(memoryUsage.getMax()));
        this.maxCommittedHeapSizeLabel.setText("0 byte");
        this.maxUsedHeapSizeLabel.setText(" 0byte");
    }
    
    /**
     * GCの実行
     */
    @FXML
    private void handleExecuteGc(final ActionEvent event) {
        
        int ans = MessageBox.show(myNode.getScene().getWindow(),
                "GC(ガーベージコレクション)を実行してもよろしいですか?",
                "GCの確認",
                MessageBox.ICON_QUESTION | MessageBox.OK | MessageBox.CANCEL);
        if(ans != MessageBox.OK) {
            return;
        }
        
        if(mxbean != null) {
            mxbean.gc();
        }
    }
    
    /**
     * 監視の再開
     */
    @FXML
    private void handleStartWatch(final ActionEvent event) {
        // 監視の開始
        startMemoryWatch();
    }
    
    /**
     * 監視の停止
     */
    @FXML
    private void handleStopWatch(final ActionEvent event) {
        
        int ans = MessageBox.show(myNode.getScene().getWindow(),
                "監視を停止してもよろしいですか?",
                "監視の停止の確認",
                MessageBox.ICON_QUESTION | MessageBox.OK | MessageBox.CANCEL);
        if(ans != MessageBox.OK) {
            return;
        }
        
        // 監視の停止
        stopMemoryWatch();
    }
    
    /**
     * メモリの監視を始める
     */
    public void startMemoryWatch() {
        
        // ボタンの活性化・非活性化
        this.stopButton.setDisable(false);
        this.startButton.setDisable(true);
        this.executeGcButton.setDisable(false);
        
        // 定期的に実行し、グラフにデータを追加する
        this.timeline = new Timeline();
        timeline.setCycleCount(Timeline.INDEFINITE);
        timeline.getKeyFrames().add(new KeyFrame(
                Duration.seconds(1),
                new EventHandler<ActionEvent>() {
                     @Override
                     public void handle(ActionEvent event) {
                         addChartData();
                     }
                    
                }));
         timeline.play();
    }
    
    /**
     * グラフにデータを追加する
     */
    private void addChartData() {
        
        final SimpleDateFormat dateFormatter = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
        
        // 現在の時間の取得
        long currentTime = System.currentTimeMillis();
        String strTime = dateFormatter.format(new Date(currentTime));
        
        // 現在のヒープ情報の取得
        final MemoryUsage heapUsage = mxbean.getHeapMemoryUsage();
        long used = heapUsage.getUsed();
        long committed = heapUsage.getCommitted();
        
        if(committed > maxCommittedHeapSize) {
            this.maxCommittedHeapSize = committed;
            this.maxCommittedHeapSizeLabel.setText(FileUtils.byteCountToDisplaySize(committed));
            
        }
        
        if(used > maxUsedHeapSize) {
            this.maxUsedHeapSize = used;
            this.maxUsedHeapSizeLabel.setText(FileUtils.byteCountToDisplaySize(used));
        }
        
        // 最大データ件数を超えている場合は削除する。(古いデータをグラフから削除する)
        if(dataUsed.getData().size() > 10) {
            dataUsed.getData().remove(0);
            dataCommitted.getData().remove(0);
        }
        
        dataUsed.getData().add(new XYChart.Data<String, Long>(strTime, used/(1024*1024)));
        dataCommitted.getData().add(new XYChart.Data<String, Long>(strTime, committed/(1024*1024)));
        
    }
    
    /**
     * 監視の停止
     * <p>※Timelineのスレッドが残るためウィンドウを閉じるときなども必ず実行する。
     */
    public void stopMemoryWatch() {
        
        // ボタンの活性化・非活性化
        this.stopButton.setDisable(true);
        this.startButton.setDisable(false);
        this.executeGcButton.setDisable(true);
        
        // タイムラインの停止
        if(timeline != null) {
            timeline.stop();
            timeline = null;
        }
        
    }
    
}

Applicationの作成

  • FXMLからControllerともに、取得します。
  • 今回は、ApplicationのStageに設定しましたが、別ウィンドウのStageで表示するようなときは、Stage#setOnCloseRequest()でウィンドウのクローズイベントを追加して、Timelineスレッドの停止を行います。
    • これをしないと、アプリケーションを終了した後も、スレッドが残り続ける場合があります。
    • 今回は、TimelineというJavaFXのスレッドなので、停止処理がなくても問題ないですが、自分でRunnableの実装などでスレッドを実行する場合は停止処理が必要です。
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import jfx.messagebox.MessageBox;
import proj.green.javafx.modules.fxml.NodeAndController;
import proj.green.javafx.modules.fxml.SimpleFxml;


public class HeapSizeApplication extends Application {
    
    private MemoryGraphPaneController controller;
    
    @Override
    public void start(Stage primaryStage) throws Exception {
        
        NodeAndController<VBox, MemoryGraphPaneController> nac 
            = SimpleFxml.loadNodeAndController(VBox.class, MemoryGraphPaneController.class);
        
        this.controller = nac.getController();
        
        controller.startMemoryWatch();
        primaryStage.setScene(new Scene(nac.getNode()));
        
        primaryStage.setOnCloseRequest(new EventHandler<WindowEvent>() {
            
            @Override
            public void handle(WindowEvent event) {
                int ans = MessageBox.show(null,
                        "ウィンドウを閉じてもよろしいですか?",
                        "確認",
                        MessageBox.ICON_QUESTION | MessageBox.OK | MessageBox.CANCEL);
                if(ans != MessageBox.OK) {
                    event.consume();
                    return;
                }
                
                // タイムラインの停止
                controller.stopMemoryWatch();
            }
        });
        
        primaryStage.show();
    }
    
    @Override
    public void stop() {
        controller.stopMemoryWatch();
    }
    
    public static void main(String[] args) {
        launch(args);
    }
}