#chiroito ’s blog

Java を中心とした趣味の技術について

Java のメモリ上にあるバイトコードをクラスファイルとして出力

開発者は Java 言語でソースコードを書き、そのソースコードは Javac というコンパイラでバイトコードに変換されクラスファイルとなり、クラスファイルは Jar ファイルとしてまとめられ、 Java コマンドでアプリケーションとして実行します。

アプリケーション上で実行されるバイトコードはコンパイルされたものと一緒であることが普通です。ASM などのバイトコードを操作するツールなど一部のフレームワークや Java の機能を使うと、このバイトコードは変更されます。操作結果を確認したい場合などそのバイトコードを見たいことは多々あると思いますので、取り出す方法を紹介します。

手順

取り出すためには JVM のデバッガである jhsdb というツールを使います。これは JDK に標準で含まれているツールです。javaコマンドと一緒に$JAVA_HOME/binに含まれています。これにhsdbという引数を付けて実行することでツールをGUIで起動します。

$ jhsdb hsdb

f:id:chiroito:20200719133335p:plain

デバッグをする方法は、プロセスへの接続、コアファイルを与えるなどいろいろあります。今回はプロセスIDを指定してプロセスへ接続します。 [File] - [Attach to HotSpot Process... Alt-A] を実行します。

f:id:chiroito:20200719133453p:plain

接続したいアプリケーションのプロセスIDを取得します。これはjhsdbと同じく Java に標準で含まれる jcmd を使うのが良いでしょう。これも$JAVA_HOME/binに含まれています。以下の様に実行すると、プロセスID と実行しているクラスが表示されます。今回はSampleApplicationへ接続しますのでプロセスIDは 82884 になります。

$ jcmd
82884 SampleApplication
157688 jdk.jcmd/sun.tools.jcmd.JCmd
75868 jdk.hotspot.agent/sun.jvm.hotspot.SALauncher hsdb

プロセスID を入力して[OK]を押すとそのプロセスへ接続します。接続が完了すると、以下の様に JVM で動いているスレッドの一覧が表示されます。

f:id:chiroito:20200719134049p:plain

今回の目的はクラスファイルを得ることです。その役割である JVM 上にロードされているクラスを見るクラスブラウザを起動しましょう。[Tools] - [Class Browser] を選択します。

f:id:chiroito:20200719134333p:plain

クラスブラウザが起動すると、JVM に読み込まれているクラスが一覧で表示されます。上の方にあるテキストフィールドに取得したいクラス名を入力することで絞ることができます。取得したいクラスが見つかったらそのクラス名をクリックします。

f:id:chiroito:20200719134555p:plain

クラスをクリックすると、そのクラスのスーパークラス、フィールド、メソッド、コンスタントプールの一覧が表示されます。 また、上の方にはクラスファイルを出力するためのCreate .class Fileがあります。これをクリックすることでクラスファイルが出力されます。

f:id:chiroito:20200719135205p:plain

出力先はjhsdbを起動しているディレクトリ直下です。パッケージ構造をディレクトリとした形で出力されます。移行した画面に表示されているクラス名を選択すると先ほどの画面に戻れます。

得られたクラスファイルは JD や IntelliJ に含まれているデコンパイラを使うことで開けます。注意したいのはフィールドやメソッドを自動生成したことを示す ACC_SYNTHETIC というフラグです。デコンパイラによってはこのフラグがあるフィールドやメソッドを無視する事もあるので注意してください。

JFR Under the hood : JFRイベントの書き込み -Java編-

結論

C/C++ 側で Java のjdk.jfr.internal.EventWriterが作成されます。このインスタンスにはThreadLocalに作られたJava用のバッファへのポインタを保持しています。イベントが書込まれるとEventWriterはそのイベントをこのバッファへ書込みます。この書き込み処理は各イベントに commit メソッドを使って行われます。このメソッドは親クラスであるEventクラスでは空実装ですが、jdk.jfr.FlightRecorderクラスがイベントを読み込む際にバイトコードを操作されて、commitメソッドはイベントが持っているフィールドを先ほどのバッファに書くように変更されます。

今回はこの様なイベントクラスを使って、その変化を確認していきます。

@Label("JfrEvent")
public class JfrEvent extends Event {

    @Label("id")
    private int id;

    @Label("name")
    private String name;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

JFR では、先ほどのイベントクラスを C/C++ レベルで書き換えます。

  • @src/hotspot/share/jfr/instrumentation/jfrEventClassTransformer.cpp

またイベントに合わせたイベントハンドラはASM と言うツールを使ってバイトコードを動的に作成します。このイベントハンドラはjdk.jfr.internal.handlers.EventHandlerを継承しています。

  • @src/jdk.jfr/share/classes/jdk/jfr/internal/EventHandlerCreator.java
  • @src/jdk.jfr/share/classes/jdk/jfr/internal/EventWriterMethod.java

イベントのクラスは以下の様に修正されます。

public class JfrEvent extends Event {
    private int id;
    private String name;
    private static EventHandler eventHandler;
    private transient long startTime;
    private transient long duration;

    public String getName() {
        return this.name;
    }

    public JfrEvent() {
    }

    static {
        FlightRecorder.register(JfrEvent.class);
    }

    public void begin() {
        this.startTime = EventHandler.timestamp();
    }

    public void end() {
        this.duration = EventHandler.duration(this.startTime);
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getId() {
        return this.id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public void commit() {
        if (this.isEnabled()) {
            if (this.startTime == 0L) {
                this.startTime = EventHandler.timestamp();
            } else if (this.duration == 0L) {
                this.duration = EventHandler.timestamp() - this.startTime;
            }

            if (this.shouldCommit()) {
                ((EventHandler1754_1595079330631_153352)eventHandler).write(this.startTime, this.duration, this.id, this.name);
            }

        }
    }

    public boolean isEnabled() {
        return eventHandler.isEnabled();
    }

    public boolean shouldCommit() {
        return eventHandler.shouldCommit(this.duration);
    }
}

また、このイベントクラス用のイベントハンドラは以下の様に作られます。

public final class EventHandler1754_1595079330631-153352 extends EventHandler {
    private final StringPool stringPool3 = this.createStringFieldWriter();

    private EventHandler1754_1595079330631_153352(boolean registered, EventType eventType, EventControl eventControl) {
        super(registered, eventType, eventControl);
    }

    public void write(long startTime, long duration, int id, String name) {
        EventWriter eventWriter;
        try {
            do {
                eventWriter= EventWriter.getEventWriter();
                if (!eventWriter.beginEvent(super.platformEventType)) {
                    break;
                }
                eventWriter.putLong(startTime);
                eventWriter.putLong(duration);
                eventWriter.putEventThread();
                eventWriter.putStackTrace();
                eventWriter.putInt(id);
                eventWriter.putString(name, this.stringPool3);
            } while(!eventWriter.endEvent());
        } catch (Throwable t) {
            EventWriter eventWriter2 = EventWriter.getEventWriter();
            if (eventWriter2 != null) {
                eventWriter2.reset();
            }
            throw t;
        }
    }
}

JFR Under the hood : StartFlightRecordingのVM引数をJFRへ適用する処理

結論

VM起動引数として指定された-XX:StartFlightRecording=xxx=xxx,yyy=yyyという形式のパラメータは、VMを起動する途中で、JFRのパラメータを初期化するメソッド(JfrOptionSet::initialize)でjcmd の方法に変換されます。その後、VMはJFRのパラメータを設定するメソッド(JfrOptionSet::configure)で内部的に jcmd JFR.configure を実行することで設定を適用します。

VMを起動する処理

VMを起動する処理の一部としてjdk/src/hotspot/share/jfr/recorder/service/jfrOptionSet.cppにあるon_create_vm_2メソッドが実行されます。 この処理では、主に以下の処理が実行されます。

  1. JFRのパラメータを初期化JfrOptionSet::initialize
  2. 設定JfrOptionSet::configure

VM を作成する処理の一部

bool JfrRecorder::on_create_vm_2() {
(略)
  Thread* const thread = Thread::current();
  if (!JfrOptionSet::initialize(thread)) {
    return false;
  }
  if (!register_jfr_dcmds()) {
    return false;
  }
(略)
    if (!validate_recording_options(thread)) {
      return false;
    }
    if (!JfrOptionSet::configure(thread)) {
      return false;
    }
(略)
}

1. JFR のパラメータを初期化

この処理では、主に以下の処理が実行されます。

  • デフォルト値の読み込み
  • VM起動引数をjcmdの形式に変換
  • 過去の形式の古いパラメータ(ObsoleteOption)が使われていたらメッセージを出力
bool JfrOptionSet::initialize(Thread* thread) {
  register_parser_options();
  if (!parse_flight_recorder_options_internal(thread)) {
    return false;
  }
(略)
}

2. 設定

jcmd を使って設定します。jcmd JFR.configureの実体であるJfrConfigureFlightRecorderDCmdクラスのインスタンスに入力されたパラメータを指定して内部的にコマンドを実行(execute)します。

bool JfrOptionSet::configure(TRAPS) {
(略)
  // delegate to DCmd execution
  JfrConfigureFlightRecorderDCmd configure(&st, false);
(略)
  configure._stack_depth.set_is_set(_dcmd_stackdepth.is_set());
  configure._stack_depth.set_value(_dcmd_stackdepth.value());
(略)
  configure.set_verbose(false);
  configure.execute(DCmd_Source_Internal, THREAD);
(略)
}

どこに設定は格納されているのか?

設定は Java と C/C++ の範囲でそれぞれ格納されています。Java は jdk.jfrモジュールのjdk/jfr/internal/Optionsクラス、C/C++ はhotspot/share/jfr/recorder/jfrRecorder.cpp に格納されます。

jcmd JFR.configure では Options クラスに格納します。Options クラスでは setter で自分の持つフィールドに格納すると共に、JNI で C/++ のメソッドを呼び出して jfrRecorderにも格納します。

Optionsで設定を変更するメソッド

public static synchronized void setGlobalBufferCount(long globalBufCount) {
    jvm.setGlobalBufferCount(globalBufCount);
    globalBufferCount = globalBufCount;
}

jfrRecorder.cppでC/C++ で設定値が格納される変数

jlong JfrOptionSet::_max_chunk_size = 0;
jlong JfrOptionSet::_global_buffer_size = 0;
jlong JfrOptionSet::_thread_buffer_size = 0;
jlong JfrOptionSet::_memory_size = 0;
jlong JfrOptionSet::_num_global_buffers = 0;
jlong JfrOptionSet::_old_object_queue_size = 0;
u4 JfrOptionSet::_stack_depth = STACK_DEPTH_DEFAULT;
jboolean JfrOptionSet::_sample_threads = JNI_TRUE;
jboolean JfrOptionSet::_retransform = JNI_TRUE;

JNIのマッピングは hotspot/share/jfr/jni/jfrJniMethodRegistration.cpphotspot/share/jfr/jni/jfrJniMethod.cppで行われてます。