Java Magic. Part 4: sun.misc.Unsafe

Java Magic. Part 4: sun.misc.Unsafe

@(Base)[JDK, Unsafe, magic, 黑魔法]

转载请写明:原文地址

系列文章:

-Java Magic. Part 1: java.net.URL
-Java Magic. Part 2: 0xCAFEBABE
-Java Magic. Part 3: Finally
-Java Magic. Part 4: sun.misc.Unsafe

英文原文

Java是一个safe programming language,它采取了很多措施来避免programmer做傻事。例如:内存管理。但是在Java中也提供了一种方式,让你可以让你做这些傻事,使用Unsafe类。

Unsafe instantiation

在我们使用使用Unsafe之前,我们必须先获取一个Unsafe的实例。当然我们不能直接Unsafe unsafe = new Unsafe()这样获取,因为Unsafe 的构造函数是私有的。但是呢有一个共有的getUnsafe()方法,但是如果你直接调用这个静态方法的话,你可能只能收到一个SecurityException。因为这个Unsafe类只被用于授信的类。下面我们看下这段代码是怎么写的。

public static Unsafe getUnsafe() {
    Class cc = sun.reflect.Reflection.getCallerClass(2);
    if (cc.getClassLoader() != null)
        throw new SecurityException("Unsafe");
    return theUnsafe;
}

这就是java代码如何验证代码是否授信。他会检查你的代码是由哪个classLoader载入的。

JDK的包都是由Primary ClassLoader载入的

当然我们可以让我们的代码也变成授信的。使用bootclasspath当启动程序的时候,如下操作:

java -Xbootclasspath:/usr/jdk1.7.0/jre/lib/rt.jar:.com.mishadoff.magic.UnsafeClient

但是这尼玛也太讨厌了吧,还有别的办法吗?

Unsafe类有一个私有域叫做theUnsafe。我们可以通过反射来获取这个引用。

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

这个时候一般的IDE都会提示你错误,"Access restriction"。我们可以配置忽略掉:

Preferences -> Java -> Compiler -> Errors/Warnings -> Deprecated and restricted API -> Forbidden reference -> Warning

Unsafe API

sun.misc.Unsafe包有105个方法。但是只有下面几个方法可能对你来说比较重要。

  • 获取信息类。获取底层的内存信息:

    • addressSize
    • pageSize
  • 操作对象。提供了一些列操作对象和字段的方法:

    • allocateInstance
    • objectFieldOffset
  • 操作class文件。提供了一些操作class文件的方法:

    • staticFieldOffset
    • defineClass
    • defineAnonymousClass
    • ensureClassInitialized
  • 操作数组。

    • arrayBaseOffset
    • arrayIndexScale
  • 同步机制。提供了底层的原子的同步机制

    • monitorEnter
    • tryMonitorEnter
    • monitorExit
    • compareAndSwapInt
    • putOrderedInt
  • 直接内存操作

    • allocateMemory
    • copyMemory
    • freeMemory
    • getAddress
    • getInt
    • putInt

一些有趣的例子

Avoid initialization

allocateInstance方法可以用于当你先个跳过对象的初始化方法,或者构造方法里面的安全检查的时候。考虑如下示例:

class A {
    private long a; // not initialized value

    public A() {
        this.a = 1; // initialization
    }

    public long a() { return this.a; }
}

我们分别通过直接调用构造函数,调用反射类库,和unsafe来实例化:

A o1 = new A(); // constructor
o1.a(); // prints 1

A o2 = A.class.newInstance(); // reflection
o2.a(); // prints 1

A o3 = (A) unsafe.allocateInstance(A.class); // unsafe
o3.a(); // prints 0

你可以考虑下你的单例模式还好么:)

Memory corruption

这是一个对c程序员有用的例子。另外,这个例子也是一个通用的跳过安全检查的例子

我们看如下代码:

class Guard {
    private int ACCESS_ALLOWED = 1;

    public boolean giveAccess() {
        return 42 == ACCESS_ALLOWED;
    }
}

Client的代码会被安全验证。有趣的是,这个giveAccess()函数永远返回false,除非你有能力改变私有域ACCESS_ALLOWED。

显然我们可以改变:

Guard guard = new Guard();
guard.giveAccess();   // false, no access

// bypass
Unsafe unsafe = getUnsafe();
Field f = guard.getClass().getDeclaredField("ACCESS_ALLOWED");
unsafe.putInt(guard, unsafe.objectFieldOffset(f), 42); // memory corruption

guard.giveAccess(); // true, access granted

如上操作,所有的client都可以无限制的使用了。

当然上面的操作反射也可以实现。但是非常有趣的是,我们这样操作甚至可以无需获取对象的引用。

例如,如果我们还有一个Guard对象在这段内存后面。我们可以直接操作他:

unsafe.putInt(guard, 16 + unsafe.objectFieldOffset(f), 42); // memory corruption

上述代码我们就直接操作了这个对象,注意哈,16是Guard对象在32位架构下的大小。其实我们可以直接使用sizeOf方法来获取Guard对象的大小。好吧,我们接下来就介绍sizeOf方法

sizeOf

我们使用objectFieldOffset可以简单实现类似于c的sizeOf函数。看如下代码:

public static long sizeOf(Object o) {
    Unsafe u = getUnsafe();
    HashSet<Field> fields = new HashSet<Field>();
    Class c = o.getClass();
    while (c != Object.class) {
        for (Field f : c.getDeclaredFields()) {
            if ((f.getModifiers() & Modifier.STATIC) == 0) {
                fields.add(f);
            }
        }
        c = c.getSuperclass();
    }

    // get offset
    long maxSize = 0;
    for (Field f : fields) {
        long offset = u.objectFieldOffset(f);
        if (offset > maxSize) {
            maxSize = offset;
        }
    }

    return ((maxSize/8) + 1) * 8;   // padding
}

译者注:getDeclaredFields并不能获取父类的字段,所以这个娃还操作了父类。

算法思想如下:遍历所有非静态的字段(包括所有父类的中),我们计算每一个字段的大小。可能我有的地方写错了,但是思路也就大致如此。

当然还有一个更简单的方法获取size,我们可以直接读取对象上的类结构,在JVM 1.7 32位架构上的偏移量是12。

public static long sizeOf(Object object){
    return getUnsafe().getAddress(
        normalize(getUnsafe().getInt(object, 4L)) + 12L);
}

下面的normalize函数的作用是,把一个有符号int转换为一个无符号的long。

private static long normalize(int value) {
    if(value >= 0) return value;
    return (~0L >>> 32) & value;
}

有趣的是,这个方法会返回和我们上一个sizeOf函数相同的结果

实际上哈,如果你真的想使用sizeOf方法,我建议你还是使用java.lang.instrument包,但是这个需要一个JVM agent。

Shallow copy

有了计算对象大小的函数,我们同样可以很容易搞出一个拷贝对象的函数。通常的做法是,需要你的类使用Cloneable接口。

Shallow copy:

static Object shallowCopy(Object obj) {
    long size = sizeOf(obj);
    long start = toAddress(obj);
    long address = getUnsafe().allocateMemory(size);
    getUnsafe().copyMemory(start, address, size);
    return fromAddress(address);
}

toAddressfromAddress分别从获取某个对象的地址,和某个地址中直接读读取出对象。

static long toAddress(Object obj) {
    Object[] array = new Object[] {obj};
    long baseOffset = getUnsafe().arrayBaseOffset(Object[].class);
    return normalize(getUnsafe().getInt(array, baseOffset));
}

static Object fromAddress(long address) {
    Object[] array = new Object[] {null};
    long baseOffset = getUnsafe().arrayBaseOffset(Object[].class);
    getUnsafe().putLong(array, baseOffset, address);
    return array[0];
}

这个浅拷贝的函数可以拷贝任意类型。这个大小也会动态计算。但是需要你做一个简单的类型转换

Hide Password

还有一个有趣的Unsafe使用场景是,从内存中删除那些不在需要的对象。

大部分获取用户密码的API都是使用byte[]或者char[]来存储,为什么使用数组?

这是出于安全原因,因为我们我们可以把数组元素清空(在我们不需要他们的时候)。如果我们使用String来存储,那么当我们不用的时候设置这个引用为null,这时候只是简单的清空引用,等待GC。

下面就是个trick用来清理string对象:

String password = new String("l00k@myHor$e");
String fake = new String(password.replaceAll(".", "?"));
System.out.println(password); // l00k@myHor$e
System.out.println(fake); // ????????????

getUnsafe().copyMemory(
          fake, 0L, null, toAddress(password), sizeOf(password));

System.out.println(password); // ????????????
System.out.println(fake); // ????????????

有没有觉得安全很多~

UPDATE: 这其实这个其实并不是真正的安全了。我们必须使用反射来清除原来String中的char数组

Field stringValue = String.class.getDeclaredField("value");
stringValue.setAccessible(true);
char[] mem = (char[]) stringValue.get(password);
for (int i=0; i < mem.length; i++) {
  mem[i] = '?';
}

Multiple Inheritance

Java并不支持多继承。当然我们也可以不停地做强制类型转换。

long intClassAddress = normalize(getUnsafe().getInt(new Integer(0), 4L));
long strClassAddress = normalize(getUnsafe().getInt("", 4L));
getUnsafe().putAddress(intClassAddress + 36, strClassAddress);

上面这个小例子就是从String强制转换成Int(如果直接强转是有异常的)

Dynamic classes

我们可以在运行时期创建一个classes对象。如下代码:

byte[] classContents = getClassContent();
Class c = getUnsafe().defineClass(
              null, classContents, 0, classContents.length);
    c.getMethod("a").invoke(c.newInstance(), null); // 1

下面是reading file:

private static byte[] getClassContent() throws Exception {
    File f = new File("/home/mishadoff/tmp/A.class");
    FileInputStream input = new FileInputStream(f);
    byte[] content = new byte[(int)f.length()];
    input.read(content);
    input.close();
    return content;
}

这个技巧非常有用,如果你想动态创建代理或者切面都可以。

Throw an Exception

不喜欢checkedException? 没问题!

getUnsafe().throwException(new IOException());

这个方法会抛出一个受检的异常,但是你的代码不会被强制要求必须catch。

Fast Serialization

这个例子非常有用哟。

所有人都知道,JAVA自带的序列化方法非常的慢,而且还强制你的类有一个无参的构造函数。

Externalizable会好一点,但是可能需要你自己定义class的schema。

有一个流行的高性能序列化的库kryo

但是以上说的,我们通通可以使用Unsafe来处理

Serialization:

  • 通过反射创建一个object的schema,这个操作每个类只需要一次。
  • 使用UnsafegetLong,getInt,getObject来获取实际的值
  • 添加一个类的identifier
  • 把这些通通写到文件或者别的什么里面去

当然最后你还可以压缩一下来减少存储

Deserialization:

  • 创建一个待反序列化的类,通过allocateInstance,因为这个可以不适用任何构造函数
  • 创建一个schema,和序列化里面的操作一样啦。
  • 从文件中获取所有输出
  • 使用UnsafeputLong,putInt,putObject来设置实际的值

思路大致如此,但是实际的操作中还有很多很多的细节。

不过,这么操作起来,确实很快。可以参考kryo对Unsafe的使用,这里

Big Arrays

大家都知道java数组的最大值就是Integer.MAX_VALUE。我们可以使用直接内存分配的技术来创建无限制大小的数组。

下面是示例代码:

class SuperArray {
    private final static int BYTE = 1;

    private long size;
    private long address;

    public SuperArray(long size) {
        this.size = size;
        address = getUnsafe().allocateMemory(size * BYTE);
    }

    public void set(long i, byte value) {
        getUnsafe().putByte(address + i * BYTE, value);
    }

    public int get(long idx) {
        return getUnsafe().getByte(address + idx * BYTE);
    }

    public long size() {
        return size;
    }
}

下面是一个使用示例:

long SUPER_SIZE = (long)Integer.MAX_VALUE * 2;
SuperArray array = new SuperArray(SUPER_SIZE);
System.out.println("Array size:" + array.size()); // 4294967294
for (int i = 0; i < 100; i++) {
    array.set((long)Integer.MAX_VALUE + i, (byte)3);
    sum += array.get((long)Integer.MAX_VALUE + i);
}
System.out.println("Sum of 100 elements:" + sum);  // 300

实际上,这使用了java堆外内存的技术,这个在java.nio包中有用到。

直接内存操作的技术可以让我们在堆外分配内存,并且逃离GC的管理,所以你必须小心使用,并且使用Unsafe.freeMemory来释放内存。这个函数也不会做任何的边界检查,所以很容易导致JVM崩溃。

这个技巧在数学计算上非常有用。可以存取大量的数组,这个对一些realtime的programmer非常有用,如果你无法忍受大对象的GC的话,你可以自行手动操作。

Concurrency

还有一点点内容就是Unsafe.compareAndSwap指令,所有的原子变量都是使用它来构建高性能的数据结构。

例如我们有一个简单的Counter接口:

interface Counter {
    void increment();
    long getCounter();
}

下面我们定义个一个Client来操作:

class CounterClient implements Runnable {
    private Counter c;
    private int num;

    public CounterClient(Counter c, int num) {
        this.c = c;
        this.num = num;
    }

    @Override
    public void run() {
        for (int i = 0; i < num; i++) {
            c.increment();
        }
    }
}

下面是一段测试代码:

int NUM_OF_THREADS = 1000;
int NUM_OF_INCREMENTS = 100000;
ExecutorService service = Executors.newFixedThreadPool(NUM_OF_THREADS);
Counter counter = ... // creating instance of specific counter
long before = System.currentTimeMillis();
for (int i = 0; i < NUM_OF_THREADS; i++) {
    service.submit(new CounterClient(counter, NUM_OF_INCREMENTS));
}
service.shutdown();
service.awaitTermination(1, TimeUnit.MINUTES);
long after = System.currentTimeMillis();
System.out.println("Counter result: " + c.getCounter());
System.out.println("Time passed in ms:" + (after - before));

第一个实现是没有任何同步手段的Counter:

class StupidCounter implements Counter {
    private long counter = 0;

    @Override
    public void increment() {
        counter++;
    }

    @Override
    public long getCounter() {
        return counter;
    }
}

输出是:

Counter result: 99542945
Time passed in ms: 679

运行的非常快,但是结果是错误的。下一个例子我们使用java内置的synchronization:

class SyncCounter implements Counter {
    private long counter = 0;

    @Override
    public synchronized void increment() {
        counter++;
    }

    @Override
    public long getCounter() {
        return counter;
    }
}

输出:

Counter result: 100000000
Time passed in ms: 10136

结果总是正确,但是执行时间有点让人蛋碎。下面我们使用读写锁:

lass LockCounter implements Counter {
    private long counter = 0;
    private WriteLock lock = new ReentrantReadWriteLock().writeLock();

    @Override
    public void increment() {
        lock.lock();
        counter++;
        lock.unlock();
    }

    @Override
    public long getCounter() {
        return counter;
    }
}

这读写锁用法有点问题

输出:

Counter result: 100000000
Time passed in ms: 8065

结果正确,效率高了一点。如果使用原子变量呢?

class AtomicCounter implements Counter {
    AtomicLong counter = new AtomicLong(0);

    @Override
    public void increment() {
        counter.incrementAndGet();
    }

    @Override
    public long getCounter() {
        return counter.get();
    }
}

输出:

Counter result: 100000000
Time passed in ms: 6552

原子变量AtomicCounter效果更好一点,最后我们用Unsafe的方法来试验一下:

class CASCounter implements Counter {
    private volatile long counter = 0;
    private Unsafe unsafe;
    private long offset;

    public CASCounter() throws Exception {
        unsafe = getUnsafe();
        offset = unsafe.objectFieldOffset(CASCounter.class.getDeclaredField("counter"));
    }

    @Override
    public void increment() {
        long before = counter;
        while (!unsafe.compareAndSwapLong(this, offset, before, before + 1)) {
            before = counter;
        }
    }

    @Override
    public long getCounter() {
        return counter;
    }

输出:

Counter result: 100000000
Time passed in ms: 6454

哦?结果和原子变量类似,是不是原子变量就是使用Unsafe来完成操作的呢?(答案是肯定的)

显然这些sample都很简单,但是我们也可以从中看出Unsafe的威力。

像我说过,CAS操作可以用来实现lock-free的数据结构,例如:

  • Have some state
  • Create a copy of it
  • Modify it
  • Perform CAS
  • Repeat if it fails

实际上,这些东西实现起来非常困难,远超你的想象,而且其中有非常多的问题,例如ABA Problem, instructions reordering, 等等。

如果你真的非常感兴趣,你可以看看这篇文章:Lock-Free HashMap

Bonus

Unsafepark方法,有一段非常长的英文注释:

Block current thread, returning when a balancing unpark occurs, or a balancing unpark has already occurred, or the thread is interrupted, or, if not absolute and time is not zero, the given time nanoseconds have elapsed, or if absolute, the given deadline in milliseconds since Epoch has passed, or spuriously (i.e., returning for no "reason"). Note: This operation is in the Unsafe class only because unpark is, so it would be strange to place it elsewhere.

译者注:park方法是用来挂起线程的,在java.concurrent.locks包下面的AQS同步框架下应用广泛

Conclusion

尽管Unsafe有很牛逼的用法,但是还是不推荐使用

posted @ 2016-02-06 13:44  马宇申  阅读(...)  评论(...编辑  收藏