JavaFXによるJVMのヒープサイズのグラフ化
需要はないいけど、JavaFXによるJavaVMのヒープサイズの可視化するツールを作ります。
こんな機能は「jconsole」やGCログを出して「GCViewer」で見ればいいじゃんと思いますが、設定とか面倒だったり、気軽にできなかったり、JDKの入っていないJREのみの環境の時に使用できたら便利と思い作りました。
仕様・要件
プログラム
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()メソッドで監視の開始をします。
- 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ともに、取得します。
- 取得する際には、以前このサイトでも紹介した自作したFXMLLoaderを利用します。
- 今回は、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); } }