sun.misc.Unsafe の魔力

sun.misc.Unsafe とは

sun.misc.Unsafe はネイティブメソッドを利用して Java のメモリモデルを書き換えるAPIを公開しています。

OpenJDKのJavaDocが以下で確認できます。

http://www.docjar.com/docs/api/sun/misc/Unsafe.html

sun.misc.Unsafe のインスタンスを取得する

Unsafe なので、簡単には利用できないようになっています。

コンストラクタはプライベートな上、getUnsafe() では呼び出し元のクラスの getClassLoader()null の場合(つまりブートストラップ・クラスローダ)からしかインスタンスを取得できないようになっています。

private static final Unsafe theUnsafe;

private Unsafe() { }

public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass();
    if(!VM.isSystemDomainLoader(var0.getClassLoader())) {
        throw new SecurityException("Unsafe");
    } else {
        return theUnsafe;
    }
}

しかし、セキュリティマネージャが許せば、以下のようにリフレクションでstaticなフィールドのインスタンスを無理やり取得することができます。

Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);

またはプライベートなコンストラクタを無理やり呼んでインスタンスを取得することもできます。

Constructor<Unsafe> unsafeConstructor = Unsafe.class.getDeclaredConstructor();
unsafeConstructor.setAccessible(true);
Unsafe unsafe = unsafeConstructor.newInstance();

コンストラクタを呼ばずにオブジェクトを取得する

以下のようなクラスがあった場合、どうやっても例外となりインスタンスを得ることはできません。

public class Demo {
    public Demo() {
        throw new IllegalStateException();
    }
}

これは以下のようにリフレクション使っても当然例外になります。

Demo demo = Demo.class.newInstance();

こんな時 Unsafe を使うと、コンストラクタをバイパスしてインスタンス化できます。

Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);

Demo demo = (Demo) unsafe.allocateInstance(Demo.class);

assertThat(demo, is(notNullValue()));

static final なクラスフィールドを書き換える

final なフィールドでも、リフレクション使えば(セキュリティマネージャが許せば)書き換えてしまうことができます。 例えば以下のクラスの period フィールドを変更したい場合、

public class Demo {
    public final Period period;
    public Demo() {
        period = Period.ofDays(5);
    }
}

以下のように簡単に書き換えてしまうことができます。

Demo demo = new Demo();
Field f = Demo.class.getDeclaredField("period");
f.setAccessible(true);
f.set(demo, Period.ofDays(9));

assertThat(demo.period, is(Period.ofDays(9)));

しかし、以下のように static final な場合はリフレクションでは歯が立ちません。

public class Demo {
    public static final Period period = Period.ofDays(5);
}

こんな時 Unsafe を使えば書き換えができてしまいます。

Demo demo = new Demo();

Field f = Demo.class.getDeclaredField("period");
unsafe.putObject(unsafe.staticFieldBase(f), unsafe.staticFieldOffset(f), Period.ofDays(9));

assertThat(demo.period, is(Period.ofDays(9)));

リフレクションでもできますが、static ではないフィールドの書き換えは以下のようになります。

Field f = Demo.class.getDeclaredField("period");
unsafe.putObject(demo, unsafe.objectFieldOffset(f), Period.ofDays(9));

チェック例外を throw する

リフレクションを使ってインスタンスを生成する場合、Class.newInstance() を使いますが、対象のコンストラクタがチェック例外を投げる場合があります。

しかし newInstance() は未知のコンストラクタのチェック例外を他の例外でラップすることなくスローしてきます。

こんなことがなぜ可能かというと、Unsafe が登場しまして、以下のように処理されています。

try {
    return tmpConstructor.newInstance((Object[])null);
} catch (InvocationTargetException e) {
    Unsafe.getUnsafe().throwException(e.getTargetException());
    // Not reached
    return null;
}

つまり、以下のように IOException などのチェック例外を、throws 句を使わずにスローすることができます。

public void throwException() {
    unsafe().throwException(new IOException());
}

他にもいろいろ

メモリを確保したり、確保したメモリに書き込んだり、開放したり。

public native long allocateMemory(long bytes);
public  void copyMemory(long srcAddress, long destAddress, long bytes);
public native  void freeMemory(long address);

クラスのバイト配列からクラス定義したり、

public native Class defineClass(String name, byte[] b, int off, int len);

ロードアベレージ直接取得したり、

public native int getLoadAverage(double[] loadavg, int nelems)

オブジェクトのモニタロックを操作したり、

public native  void monitorEnter(Object o);
public native  void monitorExit(Object o);

色々あります。