Java Platform Debugger Architecture

概要

Java Platform Debugger Architecture (JPDA) は、2 つのインタフェース (JVM TI と JDI)、1 つのプロトコル (JDWP)、およびそれらのコンポーネントを結び合わせる 2 つのソフトウェアコンポーネント (バックエンドとフロントエンド) で構成されています。JVM TI は、JVMDI に置き換わるものとして J2SE 5.0 に導入された新しいインタフェースです。そのような構成には、次のようないくつかの目的があります。

背景

Java Platform Debugger Architecture」を参照してください。

モジュール性

ここでは、JPDA のモジュール化された構造の詳細を説明します。それぞれの説明では、標準的な JPDA の使用法を取り上げます。また、リファレンス実装について説明し、インタフェースの代替実装およびクライアントについても説明します。

JVM TI のモジュール性

Java Virtual Machine Tool Interface (JVM TI) では、仮想マシン (VM) で動作する Java プログラミング言語のアプリケーションをデバッグできるようにするため、その VM から提供される機能について記述されています。JPDA では、JVM TI は VM によって実装され、クライアントは JPDA のバックエンドになります。JPDA のリファレンス実装では、JVM TI は Java HotSpot VM によって実装され、クライアントはバックエンドのリファレンス実装 (JDK に付属の、jdwp.so や jdwp.dll などのネイティブ共用ライブラリとして提供される) になります。

Java HotSpot VM 以外の多くの VM も、JVM TI を実装しています。バックエンドのリファレンス実装は、ほかのいくつかのプラットフォームに移植されました。さらに、バックエンド以外にも JVM TI のクライアントがあります。もっとも有名なのは、ネイティブコードと Java プログラミング言語コードの両方のデバッグを可能にするアプリケーションエージェント (ネイティブレベルの制御と情報を必要とする) です。バックエンドのクリーンルーム実装を意識する必要はありません。そのようなバックエンドを作成することも可能ですが、相当な労力を必要とします。

JDWP のモジュール性

Java Debug Wire Protocol (JDWP) は、debuggee とデバッガの間でやり取りされるデバッグ情報および要求の形式を記述しています。JPDA では、フロントエンド (デバッガのプロセス内にある) とバックエンド (debuggee のプロセス内にある) との間に通信チャネルがあります。そのチャネル上を流れるデータの形式が、JDWP によって記述されています。JPDA のリファレンス実装では、バックエンドのリファレンス実装 (前述) がこのチャネルの debuggee 側を提供し、フロントエンドのリファレンス実装 (tools.jar にある JDK の Java プログラミング言語コンポーネント) がこのチャネルのデバッガ側を提供します。

一部の VM では、JVM TI の実装に問題があります。そのような VM では、JDWP が直接実装されます。クライアント側では、Java プログラミング言語で作成されていないアプリケーションは、JDI を使用するアプリケーションとして不適格なことがあります。アプリケーションによっては、JDWP のクライアントとして実装することもあります。

JDI のモジュール性

Java Debug Interface (JDI) は、Java プログラミング言語のアプリケーションをデバッグするための pure Java プログラミング言語インタフェースを提供します。JPDA では、JDI は、デバッガのプロセスから見た debuggee プロセスの仮想マシンのリモートビューです。JDI は、フロントエンド (前述) によって実装されますが、そのクライアントになるのはデバッガ役のアプリケーション (IDE、デバッガ、トレースツール、モニターツールなど) です。

JDI は、アプリケーションの静的なビューを提供するようにシステムによって実装されることがあります。また、JDWP のフロントエンドとはまったく違うメカニズムで情報を収集したり VM を制御したりするように実装されることもあります。

動作の概要

これまでは、各インタフェースを使用するさまざまな方法について説明してきました。このセクションでは、標準的な JPDA が全体としてどのように動作するかを見ていきます。説明の中では、個々の呼び出しやコードの詳細について、実例を取り上げます。そのような実例は、理解できなくても問題ありません。実例を具体的にするために紹介してあるだけなためです。

各インタフェースを橋渡しするのは、要求とイベントという 2 つのアクティビティーです。要求は、デバッガ側から出されるもので、情報の照会、リモート側の VM やアプリケーションの状態変更の設定、およびデバッグ状態の設定が含まれています。イベントは、debuggee 側から出されるもので、リモート側の VM やアプリケーションの状態変化を示しています。

1 つの実例を調べてみましょう。IDE のスタック表示でユーザーが局所変数をクリックし、その値を要求したとします。IDE は、JDI を使用してその値を取得します。具体的には、getValue メソッドを呼び出します。次に例を示します。

    theStackFrame.getValue(theLocalVariable)
ここで、theStackFramecom.sun.jdi.StackFrame であり、theLocalVariablecom.sun.jdi.LocalVariable です。

次に、フロントエンドは、この要求を通信チャネル (たとえば、ソケット) 経由で、debuggee プロセスが動作しているバックエンドに送ります。そのとき、フロントエンドは、その要求を JDWP に準拠したバイトストリームの形式に変換します。具体的に言うと、フロントエンドは GetValues コマンド (バイト値 1) をStackFrame コマンドセット (バイト値 16) で送り、そのあとにスレッド ID、フレーム ID などが続きます。

バックエンドは、そのバイトストリームを解析し、JVM TI を介して VM に照会を送ります。具体的には、要求された値が整数だとすると、次のような JVM TI 関数の呼び出しを実行します。

    error = jvmti->GetLocalInt(frame, slot, &intValue);
バックエンドは、ソケット経由で応答パケットを返送します。そのパケットには intValue の値が入っており、JDWP に準拠したデータ形式になっています。フロントエンドは、応答パケットを解析し、その値を getValue メソッド呼び出しの値として返します。最後に、IDE は、返された値を表示します。

デバッグ状態を変更する要求も、同様の方法で処理されます。たとえば、ブレークポイントを設定するという要求は、同様のステップで処理されます。もちろん、呼び出される JDI メソッドや、送信される JDWP コマンドや、呼び出される JVM TI 関数は違います。さらに、フロントエンドとバックエンドは、単にデータをやり取りする以上のことを行います。アクティビティーを追跡およびスケジューリングし、情報を変換、フィルタリング、およびキャッシュします。したがって、ブレークポイントを設定する要求は、値を取得する照会とはかなり違った仕方で処理されますが、通信の手順は同じです。

デバッグしているアプリケーションがこのブレークポイントに達すると、何が起こるのでしょうか。今度は、イベントの出番になります。仮想マシンは、JVM TI インタフェースを介してイベントを送ります。具体的には、仮想マシンは、イベント処理関数を呼び出して、ブレークポイントを渡します。

バックエンドは、イベント処理関数を次のように設定しています。

static void Breakpoint(jvmtiEnv *jvmti_env,
                       JNIEnv* jni_env, jthread thread,
                       jmethodID method, jlocation location)
{ ...
このバックエンド関数は、関心のあるイベントをフィルタリングし、そのイベントをキューに入れ、ブレークポイントイベント用に定義された JDWP 形式のソケットを介してそのイベントを送信するという、一連のアクティビティーを開始します。フロントエンドは、そのイベントをデコードして処理し、最終的には JDI イベントを生成します。具体的には、JDI イベントは、com.sun.tools.jdi.event.BreakpointEvent として公開されます。その後、IDE は、そのイベントをイベントキューから取り出して取得します。
    theEventQueue.remove()
ここで、theEventQueue com.sun.jdi.event.EventQueue です。IDE は、JDI を介して多くの照会呼び出しを実行することにより、表示を更新すると予想されます。

移殖

仮想マシンの各実装には、それぞれ独自の JVM TI 実装が必要です。JVM TI の実装では、VM のデータ構造に深く踏み込む必要があり、イベントを取得するために VM 実装の中にフックを設定する必要があります。JVM TI サポートのない VM に JVMDT を追加するのは、かなりの作業になります。VM の複雑さと、実装する JVM TI のオプション機能の量に応じて、3 か月から 12 か月のプロジェクトになると考えられます。JVM TI サポートが組み込まれている VM を新しいプラットフォームに移殖する作業は、VM の JVM TI 以外の部分の移殖が中心になります。JVM TI にかかる付加的な作業は比較的少量です。

バックエンドのリファレンス実装を新しいプラットフォームに移すには、多くの場合、ソースにわずかの変更 (数行のみ) を加えるか、ソースをまったく変更せずに、再コンパイルするだけで済みます。同じプラットフォーム上で新しい VM を使用する場合は、バックエンドのバイナリコードは多くの場合そのまま動作します。ただし、それは Java プログラミング言語のコードではないため、内容を理解することはできません。このドキュメントではライセンスの問題には触れていません。

フロントエンドの実装は Java プログラミング言語で作成されているため、どのプラットフォームまたは VM でも動作します。ただし、一部のシステムでは、コネクタコードの機能の一部を拡張する必要がある場合もあります。たとえば、フロントエンドのリファレンス実装に含まれている起動ツールでは、仮想マシンが Java SE の規則を使って起動されることが前提です。JDI のユーザーが自分たちの希望に合わせて起動ツールの構文を決めることもできますが、一般に、デバッガアプリケーションでは、その構文が JDI 実装の側で決められていると想定します。別の種類の通信チャネル (たとえば、シリアル接続) が必要な場合は、JDK 5.0 で導入されたサービスプロバイダインタフェースを使用して、その機能も追加する必要があります。


Copyright © 1993, 2013, Oracle and/or its affiliates. All rights reserved.