11

この単純なJavaクラスについて考えてみます。

class MyClass {
  public void bar(MyClass c) {
    c.foo();
  }
}

c.foo()行で何が起こるかについて説明したいと思います。

元の誤解を招く質問

注:これのすべてが実際に個々のinvokevirtualオペコードで発生するわけではありません。ヒント:Javaメソッドの呼び出しを理解したい場合は、invokevirtualのドキュメントだけを読んではいけません。

バイトコードレベルでは、c.foo()の要点はinvokevirtualオペコードになり、invokevirtualのドキュメントによると、多かれ少なかれ次のことが起こります。

  1. コンパイル時クラスMyClassで定義されているfooメソッドを検索します。(これには、最初にMyClassを解決することが含まれます。)
  2. 次のようないくつかのチェックを行います。cが初期化メソッドではないことを確認し、MyClass.fooの呼び出しが保護された修飾子に違反しないことを確認します。
  3. 実際に呼び出すメソッドを見つけます。特に、cのランタイムタイプを検索します。そのタイプにfoo()がある場合は、そのメソッドを呼び出して戻ります。そうでない場合は、cのランタイムタイプのスーパークラスを検索します。そのタイプにfooがある場合は、そのメソッドを呼び出して戻ります。そうでない場合は、cの実行時型のスーパークラスのスーパークラスを検索します。そのタイプにfooがある場合は、そのメソッドを呼び出して戻ります。など。適切な方法が見つからない場合は、エラーが発生します。

ステップ3だけで、呼び出すメソッドを見つけ出し、そのメソッドが正しい引数/戻り型を持っていることを確認するのに十分なようです。だから私の質問は、なぜステップ#1が最初に実行されるのかということです。考えられる答えは次のようです。

  • ステップ1が完了するまで、ステップ3を実行するための十分な情報がありません。(これは一見信じられないようですので、説明してください。)
  • #1と#2で行われるリンクまたはアクセス修飾子のチェックは、特定の悪いことが起こらないようにするために不可欠であり、これらのチェックは、実行時型階層ではなく、コンパイル時型に基づいて実行する必要があります。(説明してください。)

改訂された質問

行c.foo()のjavacコンパイラ出力のコアは、次のような命令になります。

invokevirtual i

ここで、iはMyClassのランタイム定数プールへのインデックスです。その定数プールエントリは、タイプCONSTANT_Methodref_infoであり、(おそらく間接的に)A)呼び出されるメソッドの名前(つまり、foo)、B)メソッドのシグネチャ、およびC)メソッドが呼び出されるコンパイル時クラスの名前を示します。 on(つまり、MyClass)。

問題は、なぜコンパイル時型(MyClass)への参照が必要なのかということです。invokevirtualは実行時型cで動的ディスパッチを実行するので、コンパイル時クラスへの参照を格納するのは冗長ではありませんか?

4

5 に答える 5

4

パフォーマンスがすべてです。コンパイル時の型(別名:静的型)を把握することにより、JVMは実行時型(別名:動的型)の仮想関数テーブルで呼び出されたメソッドのインデックスを計算できます。このインデックスを使用すると、ステップ3は、一定時間で実行できるアレイへのアクセスになります。ループは必要ありません。

例:

class A {
   void foo() { }
   void bar() { }
}

class B extends A {
  void foo() { } // Overrides A.foo()
}

デフォルトでは、これらのメソッドを定義するAextends Object(を介して呼び出されるため、finalメソッドは省略されますinvokespecial):

class Object {
  public int hashCode() { ... }
  public boolean equals(Object o) { ... }
  public String toString() { ... }
  protected void finalize() { ... }
  protected Object clone() { ... }
}

ここで、この呼び出しについて考えてみましょう。

A x = ...;
x.foo();

xの静的タイプがJVMであることを理解することにより、この呼び出しサイトで使用可能なメソッドのリストをA把握することもできます:、、、、、、、。このリストでは、は6番目のエントリです(1番目、2番目など)。このインデックスの計算は、JVMがクラスファイルをロードするときに1回実行されます。hashCodeequalstoStringfinalizeclonefoobarfoohashCodeequals

その後、JVMプロセスが必要なときはいつでも、x.foo()xが提供するメソッドのリストの6番目のエントリ( xの動的タイプがx.getClass().getMethods[5]であるかどうかを指す)にアクセスし、そのメソッドを呼び出す必要があります。この一連のメソッドを徹底的に検索する必要はありません。A.foo()A

メソッドのインデックスは、xの動的タイプに関係なく同じままであることに注意してください。つまり、xがBのインスタンスを指している場合でも、6番目のメソッドはまだですfoo(ただし、今回はを指しますB.foo())。

アップデート

[あなたのアップデートに照らして]:その通りです。仮想メソッドディスパッチを実行するために必要なすべてのJVMは、メソッドの名前と署名(またはvtable内のオフセット)です。ただし、JVMは盲目的に物事を実行しません。最初に、ロードされたcassfileが検証と呼ばれるプロセスで正しいことを確認します(ここも参照)。

検証は、JVMの設計原則の1つを表します。つまり、正しいコードを生成するためにコンパイラーに依存しません。実行を許可する前に、コード自体をチェックします。特に、ベリファイアは、呼び出されたすべての仮想メソッドが実際にレシーバーオブジェクトの静的タイプによって定義されていることを確認します。明らかに、このようなチェックを実行するには、静的タイプのレシーバーが必要です。

于 2010-04-01T23:23:18.257 に答える
1

ドキュメントを読んだ後、それは私がそれを理解する方法ではありません。ステップ2と3が入れ替わっていると思います。これにより、一連のイベント全体がより論理的になります。

于 2010-04-01T21:37:09.723 に答える
1

おそらく、#1と#2はコンパイラによってすでに発生しています。目的の少なくとも一部は、コードがコンパイルされたバージョンとは異なる可能性があるランタイム環境のクラスのバージョンでそれらが保持されていることを確認することであると思われます。

ただし、要約を確認するためにドキュメントを要約していないinvokevirtualので、RobHeiserが正しい可能性があります。

于 2010-04-01T21:43:02.320 に答える
1

答えは「B」だと思います。

#1と#2で行われるリンクまたはアクセス修飾子のチェックは、特定の悪いことが起こらないようにするために不可欠であり、これらのチェックは、実行時型階層ではなく、コンパイル時型に基づいて実行する必要があります。(説明してください。)

#1は、いくつかの重要なチェックを行う5.4.3.3メソッド解決によって記述されます。たとえば、#1は、コンパイル時タイプのメソッドのアクセス可能性をチェックし、そうでない場合はIllegalAccessErrorを返す可能性があります。

...それ以外の場合、参照されたメソッドがDにアクセスできない場合(§5.4.4)、メソッド解決はIllegalAccessErrorをスローします。..。

実行時型のみをチェックした場合(#3を介して)、実行時型は、オーバーライドされたメソッドのアクセシビリティを不法に拡大する可能性があります(別名「悪いこと」)。コンパイラがそのようなケースを防ぐ必要があるのは事実ですが、それでもJVMは不正なコード(手動で作成された悪意のあるコードなど)から自身を保護しています。

于 2010-04-01T23:08:51.253 に答える
0

このことを完全に理解するには、Javaでメソッド解決がどのように機能するかを理解する必要があります。詳細な説明をお探しの場合は、「Java仮想マシンの内部」という本をご覧になることをお勧めします。第8章「リンクモデル」の次のセクションはオンラインで入手でき、特に関連性があるようです。

(CONSTANT_Methodref_infoエントリは、そのクラスによって呼び出されるメソッドを説明するクラスファイルヘッダーのエントリです。)

これを見つけるために必要なグーグルをするように私を刺激してくれたItayに感謝します。

于 2010-04-02T01:42:58.890 に答える