xiaobaoqiu Blog

Think More, Code Less

揭秘sun.misc.Unsafe

Java最初被设计为一种安全的受控环境.尽管如此,Java HotSpot还是包含了一个“后门”,它提供了一些可以直接操控内存和线程的低层次操作.这个后门类就是sun.misc.Unsafe,它被JDK广泛用于自己的包中,如java.nio和java.util.concurrent.但是丝毫不建议在生产环境中使用这个后门.因为这个API十分不安全、不轻便、而且不稳定.这个不安全的类提供了一个观察HotSpot JVM内部结构并且可以对其进行修改.

1.源代码及文档

源码:http://www.docjar.com/html/api/sun/misc/Unsafe.java.html

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

2.Unsafe身影

典型的在concurrent包中的Atomic系列的实现中,比如compareAndSet等Atomic实现就是借助于Unsafe类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class AtomicInteger extends Number implements java.io.Serializable {
    private static final Unsafe unsafe = Unsafe.getUnsafe();    //
    private static final long valueOffset;

    static {
      try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
      } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;

    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
}

3.Unsafe初始化

sun.misc.Unsafe这个类是如此地不安全,以至于JDK开发者增加了很多特殊限制来访问它.它的构造器是私有的,工厂方法getUnsafe()的调用器只能被Bootloader加载.

在使用Unsafe之前,我们需要创建Unsafe对象的实例.这并不像Unsafe unsafe = new Unsafe()这么简单,因为Unsafe的构造器是私有的.它也有一个静态的getUnsafe()方法,但如果你直接调用Unsafe.getUnsafe(),你可能会得到SecurityException异常.只能从受信任的代码中使用这个方法:

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

Unsafe类包含一个私有的、名为theUnsafe的实例,我们可以通过Java反射窃取该变量:

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

4.Unsafe API

sun.misc.Unsafe类包含105个方法.实际上,对各种实体操作有几组重要方法:

4.1 Info-仅返回一些低级的内存信息

addressSize
pageSize

4.2 Objects-提供用于操作对象及其字段的方法

allocateInstance
objectFieldOffset

4.3 Classes-提供用于操作类及其静态字段的方法

staticFieldOffset
defineClass
defineAnonymousClass
ensureClassInitialized

4.4 Arrays-操作数组

arrayBaseOffset
arrayIndexScale

4.5 Synchronization-低级的同步原语

monitorEnter
tryMonitorEnter
monitorExit
compareAndSwapInt
putOrderedInt

4.6 Memory-直接内存访问方法

allocateMemory
copyMemory
freeMemory
getAddress
getInt
putInt

5.有趣的Case

5.1 避免初始化

当你想要跳过对象初始化阶段,或绕过构造器的安全检查,或实例化一个没有任何公共构造器的类,allocateInstance方法是非常有用的.考虑以下类:

1
2
3
4
5
6
7
8
9
10
class A {
    private long a; // not initialized value

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

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

使用构造器、反射和unsafe初始化它,将得到不同的结果.

1
2
3
4
5
6
7
8
A o1 = new A(); // 构造器
o1.a(); // prints 1

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

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

另外一种情况,当构造器的代价十分昂贵,我们可以绕过构造器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ClassWithExpensiveConstructor {

    private final int value;

    private ClassWithExpensiveConstructor() {
    value = doExpensiveLookup();
    }

    private int doExpensiveLookup() {
    try {
      Thread.sleep(2000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    return 1;
    }

    public int getValue() {
    return value;
    }
}

绕过昂贵的构造器:

1
2
3
4
5
6
@Test
public void testObjectCreation() throws Exception {
    ClassWithExpensiveConstructor instance = (ClassWithExpensiveConstructor)
    unsafe.allocateInstance(ClassWithExpensiveConstructor.class);
    assertEquals(0, instance.getValue());
}

5.2 内存崩溃(Memory corruption)

这对于每个C程序员来说是常见的.顺便说一下,它是绕过安全的常用技术.

考虑下那些用于检查“访问规则”的简单类:

1
2
3
4
5
6
7
class Guard {
    private int ACCESS_ALLOWED = 1;

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

客户端代码是非常安全的,并且通过调用giveAccess()来检查访问规则.可惜,对于客户,它总是返回false.只有特权用户可以以某种方式改变ACCESS_ALLOWED常量的值并且得到访问,即giveAccess()方法返回true.

实际上,这并不是真的.演示代码如下:

1
2
3
4
5
6
7
8
9
Guard guard = new Guard();
guard.giveAccess();   // false, no access

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

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

现在所有的客户都拥有无限制的访问权限.

实际上,反射可以实现相同的功能.但值得关注的是,我们可以修改任何对象,甚至没有这些对象的引用.

例如,有一个guard对象,所在内存中的位置紧接着在当前guard对象之后.我们可以用以下代码来修改它的ACCESS_ALLOWED字段:

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

注意:我们不必持有这个对象的引用.16是Guard对象在32位架构上的大小.我们可以手工计算它,或者通过使用sizeOf方法.

5.2 sizeOf

使用objectFieldOffset方法可以实现C-风格(C-style)的sizeof方法.这个实现返回对象的自身内存大小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
}

算法如下:通过所有非静态字段(包含父类的),获取每个字段的偏移量(offset),找到偏移最大值并填充字节数(padding).

如果我们仅读取对象的类结构大小值,sizeOf的实现可以更简单,这位于JVM 1.7 32 bit中的偏移量12.

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

normalize是一个为了正确内存地址使用,将有符号的int类型强制转换成无符号的long类型的方法.

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

真棒,这个方法返回的结果与我们之前的sizeof方法一样.

实际上,对于良好、安全、准确的sizeof方法,最好使用 java.lang.instrument包,但这需要在JVM中指定agent选项.

5.3 浅拷贝(Shallow copy)

为了实现计算对象自身内存大小,我们可以简单地添加拷贝对象方法.标准的解决方案是使用Cloneable修改你的代码,或者在你的对象中实现自定义的拷贝方法,但它不会是多用途的方法.

浅拷贝:

1
2
3
4
5
6
7
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);
}

toAddress和fromAddress将对象转换为其在内存中的地址,反之亦然.

1
2
3
4
5
6
7
8
9
10
11
12
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];
}

这个拷贝方法可以用来拷贝任何类型的对象,动态计算它的大小.注意,在拷贝后,你需要将对象转换成特定的类型.

5.4 隐藏密码(Hide Password)

在Unsafe中,一个更有趣的直接内存访问的用法是,从内存中删除不必要的对象.

检索用户密码的大多数API的签名为byte[]或char[],为什么是数组呢?

这完全是出于安全的考虑,因为我们可以删除不需要的数组元素.如果将用户密码检索成字符串,这可以像一个对象一样在内存中保存,而删除该对象只需执行解除引用的操作.但是,这个对象仍然在内存中,由GC决定的时间来执行清除.

创建具有相同大小、假的String对象,来取代在内存中原来的String对象的技巧:

1
2
3
4
5
6
7
8
9
10
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); // ????????????

感觉很安全.

其实这并不安全.为了真正的安全,我们需要通过反射删除后台char数组:

1
2
3
4
5
6
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] = '?';
}

5.5 多继承(Multiple Inheritance)

Java中没有多继承,这是对的,除非我们可以将任意类型转换成我们想要的其他类:

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

这个代码片段将String类型添加到Integer超类中,因此我们可以强制转换,且没有运行时异常:

(String) (Object) (new Integer(666))

有一个问题,我们必须预先强制转换对象,以欺骗编译器.

5.6 动态类(Dynamic classes)

我们可以在运行时创建一个类,比如从已编译的class文件中.将类内容读取为字节数组,并正确地传递给defineClass方法.

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

从定义文件(class文件)中读取(代码)如下:

1
2
3
4
5
6
7
8
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;
}

当你必须动态创建类,而现有代码中有一些代理, 这是很有用的.

5.7 抛出异常(Throw an Exception)

不喜欢受检异常?没问题

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

该方法抛出受检异常,但你的代码不必捕捉或重新抛出它,正如运行时异常一样.

5.8 大数组(Big Arrays)

正如你所知,Java数组大小的最大值为Integer.MAX_VALUE.使用直接内存分配,我们创建的数组大小受限于堆大小.

SuperArray的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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

实际上,这是堆外内存(off-heap memory)技术,在java.nio包中部分可用.

这种方式的内存分配不在堆上,且不受GC管理,所以必须小心Unsafe.freeMemory()的使用.它也不执行任何边界检查,所以任何非法访问可能会导致JVM崩溃.

这可用于数学计算,代码可操作大数组的数据.此外,这可引起实时程序员的兴趣,可打破GC在大数组上延迟的限制.

5.9 并发(Concurrency)

关于Unsafe的并发性.compareAndSwap方法是原子的,并且可用来实现高性能的、无锁的数据结构.

比如,考虑问题:在使用大量线程的共享对象上增长值.

首先,我们定义简单的Counter接口:

1
2
3
4
interface Counter {
    void increment();
    long getCounter();
}

然后,我们定义使用Counter的工作线程CounterClient:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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));

第一个无锁版本的计数器:

1
2
3
4
5
6
7
8
9
10
11
12
13
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式同步:

1
2
3
4
5
6
7
8
9
10
11
12
13
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

激进的同步有效,但耗时长.

试试ReentrantReadWriteLock:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class 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

仍然正确,耗时较短.

atomics的运行效果如何?

1
2
3
4
5
6
7
8
9
10
11
12
13
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原始的compareAndSwapLong,看看它是否真的只有特权才能使用它?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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

看起来似乎等价于atomics,实际上,atomics正是使用Unsafe实现.

实际上,这个例子很简单,但它展示了Unsafe的一些能力.

如我所说,CAS原语可以用来实现无锁的数据结构.背后的原理很简单:

有一些状态
创建它的副本
修改它
执行CAS
如果失败,重复尝试

实际上,现实中比你现象的更难.存在着许多问题,如ABA问题、指令重排序等.

如果你真的感兴趣,可以参考lock-free HashMap的精彩展示.

6.结论

即使Unsafe对应用程序很有用,但建议不要使用它.

参考

http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/ http://java.dzone.com/articles/understanding-sunmiscunsafe