最近、私はJVMの内部を少し掘り下げる必要がありました。 かなり興味深い経験。 上記のトピックのテキストは私の経験とはまったく一致しませんが、自分自身を絶対的な真実の所有者とは考えていません。 以下では、Javaオブジェクトに直接関連する実験の一部を読者と共有します。
試験システム
Windows XP SP3 32ビット
D:\ dev \ puzzles> java -version
Javaバージョン "1.6.0_29"
Java(TM)SEランタイム環境(ビルド1.6.0_29-b11)
Java HotSpot(TM)Client VM(ビルド20.4-b02、混合モード、共有)
準備する
すべての実験は、ユーティリティクラスUnsafeを使用して実施されました。 クラスの説明はwasm.ruから盗まれます。
あまり知られていないクラスsun.misc.Unsafeは、最初のバージョン以降、Sun Java Runtimeに含まれています。 パッケージsun。*の他のすべてのクラスと同様に、Unsafeはドキュメント化されていませんが、逆コンパイル中に表示される関数の名前(ほとんどがネイティブ)が語っています。 メモリ(allocateMemory、freeMemory、...)の操作、特定のアドレスへの値の読み書き(putLong、getLong、...)、およびより特殊なもの(throwException、monitorEnter、...)を処理するための関数が明らかにあります。
しかし、Unsafeをインスタンス化するのはそれほど簡単ではありません。 唯一のコンストラクターはプライベートであり、getUnsafe()では、呼び出し元クラスのローダーがチェックされ、クラスがブートローダーを介してロードされた場合にのみオブジェクトが返されます。 そうでない場合、SecurityExceptionが発生します。
public static Unsafe getUnsafe() { Class class1 = Reflection.getCallerClass(2); if (class1.getClassLoader() != null) throw new SecurityException("Unsafe"); else return theUnsafe; }
幸いなことに、内部変数theUnsafeもあり、Reflectionを使用してアクセスできます。 小さなヘルパークラスをまとめました。
import java.lang.reflect.Field; import sun.misc.Unsafe; public class T { public static Unsafe u; private static long fieldOffset; private static T instance = new T(); private Object obj; static { try { Field f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); u = (Unsafe) f.get(null); fieldOffset = u.objectFieldOffset(T.class.getDeclaredField("obj")); } catch (Exception ex) { throw new RuntimeException(ex); } }; public synchronized static long o2a(Object o) { instance.obj = o; return u.getLong(instance, fieldOffset); } public synchronized static Object a2o(long address) { u.putLong(instance, fieldOffset, address); return instance.obj; } public static Unsafe get() { return u; } }
アドレスでオブジェクトを取得し、オブジェクトからアドレスを取得する操作が必要になります:o2a(オブジェクトo)とa2o(長いアドレス)。
今、あなたは何かを見ることができます。
オブジェクト構造
次のコードを検討してください。
import sun.misc.Unsafe; public class V { private Integer b = 3; private int a = 2; public static void main(String[] argv) throws Exception { Unsafe u = T.get(); long obj = T.o2a(new V()); for (int i = 0; i < 28; i++) System.out.print(u.getByte(obj + i) + " "); } }
このコードは、2つのプライベートフィールド(intおよびInteger)を含むクラスです。 このクラスのインスタンスを作成し、メモリ内のアドレスを見つけ、28バイトを出力します。
私の場合の結果は次のとおりです(わかりやすくするために、出力を4バイトのグループにフォーマットしました)。
01 00 00 00 88 D7 79 32 02 00 00 00 D0 7C E4 22 01 00 00 00 E8 09 19 37 68 6F E4 22
だから私たちが見るもの:
01 00 00 00は、いわゆる魔法です。 これはオブジェクトマスクです。 たとえば、ロックに関するさまざまな情報を格納します。 コメントの詳細。
88 D7 79 32-これから先、クラスオブジェクトへの参照と言います。
02 00 00 00は、int型のプリミティブフィールドの値です。
D0 7C E4 22-繰り返しになりますが、これはInteger型のフィールドのオブジェクトへの参照です。
さらに...さらに、これはメモリ内の近くにある別のオブジェクトです。 コードを少し変更することで、これを確認できます。
import sun.misc.Unsafe; public class V { private int a = 2; private Integer b = 777; public static void main(String[] argv) throws Exception { Unsafe u = T.get(); long obj = T.o2a(new V()); for (int i = 0; i < 28; i++) System.out.print(u.getByte(obj + i) + " "); long field = u.getAddress(obj + 3 * 4); Object i = T.a2o(field); System.out.print("\nInteger: " + i); } }
結果:
01 00 00 00 98 D6 79 32 02 00 00 00 C8 FB E4 22 ... Integer: 777
この例では、Integer型のフィールドのアドレスを取得し、このアドレスにあるオブジェクトを取得しました。 その結果、仮定が裏付けられました。
次の例では、2番目のブロック(クラスアドレス)に関する期待を確認します。
import sun.misc.Unsafe; public class V { private int a = 2; private int b = 3; public static void main(String[] argv) throws Exception { Unsafe u = T.get(); long obj = T.o2a(new V()); V v = new V(); va = 5; vb = 6; for (int i = 0; i < 32; i++) System.out.print(u.getByte(obj + i) + " "); } }
結果:
01 00 00 00 08 D6 79 32 02 00 00 00 03 00 00 00 01 00 00 00 08 D6 79 32 05 00 00 00 06 00 00 00
この例では、クラスから参照フィールドを削除しました。 2つのオブジェクトを作成し、そのうちの1つでフィールドの値を変更しました。 そして、結果として、オブジェクトは近くのメモリに入り、プリミティブフィールドの値によって区別できます。
配列を使用した別の例:
import sun.misc.Unsafe; public class V { public static void main(String[] argv) throws Exception { Unsafe u = T.get(); long obj = T.o2a(new Integer[] {1, 2, 3}); for (int i = 0; i < 28; i++) System.out.print(u.getByte(obj + i) + " "); } }
01 00 00 00 B8 C9 79 32 03 00 00 00 40 78 E4 22 50 78 E4 22 60 78 E4 22 ...
配列には、実際に要素の数が格納されます。
オフセットの例:
import sun.misc.Unsafe; public class V { private Integer a = 1; public static void main(String[] argv) throws Exception { Unsafe u = T.get(); long obj = T.o2a(new V()); for (int i = 0; i < 28; i++) System.out.print(u.getByte(obj + i) + " "); } }
ここでは、クラスにフィールドを1つだけ残しました。
結果:
01 00 00 00 B0 D7 79 32 60 78 E4 22 00 00 00 00 01 00 00 00 E8 09 19 37 ...
ここでは、整数型のフィールドの値への参照の後に4バイトの空のブロックが続いていることがわかります。 すべてのオブジェクトは、8バイトの倍数に埋め込まれます。
オフセットの例:
import sun.misc.Unsafe; public class V { private int a = 2; private int b = 3; public static void main(String[] argv) throws Exception { Unsafe u = T.get(); V v = new V(); synchronized (v) { long obj = T.o2a(v); for (int i = 0; i < 32; i++) System.out.print(T.hex(u.getByte(obj + i)) + " "); } } }
ここでは、問題のオブジェクトは同期ブロックにあり、その結果、最初の4バイトの値が変更されています。
結果:
8C 84 F0 00 58 D6 79 32 02 00 00 00 03 00 00 00 ...
結論
Javaオブジェクトの構成:
4バイト-マジックマスク。
4バイト-クラスアドレス。
4バイトは配列のサイズです(もちろん配列の場合のみ)。
n * 4バイト-オブジェクトの各フィールド(プリミティブ型の値またはオブジェクトへの参照)。
これはすべて、最大8バイトの係数まで補完されます。
彼は急いで書いた。 訂正、質問、提案