Java では、サンドボックスのセキュリティを維持し、コードを安全に最適化できるようにするために、ロードされるすべてのクラスを検証する必要があります。これはバイトコード レベルで行われるため、検証はJava言語の不変条件を検証するのではなく、バイトコードのルールに従ってバイトコードが意味をなすことを検証するだけであることに注意してください。
とりわけ、バイトコード検証では、命令が適切な形式であること、すべてのジャンプがメソッド内の有効な命令であること、およびすべての命令が正しい型の値で動作することを確認します。最後の 1 つは、スタック マップの出番です。
問題は、バイトコード自体には明示的な型情報が含まれていないことです。型は、データフロー分析によって暗黙的に決定されます。たとえば、iconst 命令は整数値を作成します。スロット 1 に格納すると、そのスロットには int が含まれるようになります。代わりに float を格納するコードから制御フローがマージされた場合、スロットは無効な型を持っていると見なされるようになりました。
歴史的に、バイトコード ベリファイアは、これらのデータフロー ルールを使用してすべての型を推測していました。残念ながら、後方へのジャンプはすでに推論された型を無効にする可能性があるため、バイトコードを介した単一の線形パスですべての型を推論することは不可能です。従来のベリファイアは、すべての変更が停止するまでコードを繰り返し処理することでこれを解決し、複数回のパスが必要になる可能性がありました。
ただし、Java では、検証によってクラスのロードが遅くなります。オラクルは、バイトコードを 1 回のパスで検証できる、より高速な新しい検証ツールを追加することで、この問題を解決することにしました。これを行うには、Java 7 (移行状態の Java 6) 以降のすべての新しいクラスに、型に関するメタデータを保持するように要求し、バイトコードを 1 回のパスで検証できるようにしました。バイトコード形式自体は変更できないため、この型情報は と呼ばれる属性に個別に格納されますStackMapTable
。
コード内のすべてのポイントですべての値の型を単純に格納すると、明らかに多くのスペースが必要になり、非常に無駄になります。メタデータをより小さく効率的にするために、ジャンプの対象となる位置のタイプのみをリストすることにしました。考えてみれば、シングル パス検証を行うために追加情報が必要になるのはこのときだけです。ジャンプ ターゲットの間では、すべての制御フローが線形であるため、古い推論規則を使用して位置間の型を推論できます。
タイプが明示的にリストされている各位置は、スタック マップ フレームと呼ばれます。このStackMapTable
属性には、順番に並べられたフレームのリストが含まれていますが、データ サイズを小さくするために、通常は前のフレームとの違いとして表現されます。制御フローが結合しない (つまり、CFG がツリーである) ときに発生するメソッドにフレームがない場合は、StackMapTable 属性を完全に省略できます。
これが、StackMapTable がどのように機能し、なぜ追加されたのかについての基本的な考え方です。最後の質問は、暗黙の初期フレームがどのように作成されるかです。もちろん、答えは、メソッドの開始時にオペランド スタックが空であり、ローカル変数スロットが、メソッド パラメーターの型によって指定された型を持っているということです。メソッド パラメーターの型は、メソッド記述子から決定されます。
Java に慣れている場合、バイトコード レベルでメソッド パラメーターの型がどのように機能するかについて、いくつかの小さな違いがあります。this
まず、仮想メソッドには最初のパラメーターとして暗黙的なものがあります。2 番目、boolean
、byte
、char
、およびshort
は、バイトコード レベルでは存在しません。代わりに、それらはすべて舞台裏で int として実装されます。