Unsafe

来源:http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/
Java是一门安全的编程语言,并且防范程序员犯大量的愚蠢的错误,它们中的大部分是内存管理。但我们可以使用Unsafe类来故意犯这类错误。
这篇文章会快速概述Unsafe的公开API和一些有趣的例子。

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;
}

这就是当代码被信任时如何验证的,它仅仅是测试我们的代码是否在使用主要类加载器。
我们可以使我们的代码“被信任”,当运行你的代码时使用bootclasspath选项并且指定你使用Unsafe类的路径到系统类中。

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);

提示:忽略你的编译器。例如,eclipse显示”Access restriction…”,但是你可以运行你的代码,所有的都可以正常运行。如果那些错误让你觉得恼人,你可以按下面的步骤来忽略在Unsafe中出现的错误:

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

Unsafe API
sun.misc.Unsafe类由105方法组成。实际上只有一小部分重要的类来操作着不同的实体对象。下面就是它们的一部分:
1. Info。仅仅返回一些低水平的内存信息:
addressSize
pageSize
2. Objects。提供操作对象方法与域的方法。
allocateInstance
objectFieldOffset
3. Classes。提供操作类与静态域的方法。
staticFieldOffset
defineClass
defineAnonymousClass
ensureClassInitialized
4. Arrays。数组的操作。
arrayBaseOffset
arrayIndexScale
5. Synchronization。低水平同步基础
monitorEnter
tryMonitorEnter
monitorExit
compareAndSwapInt
putOrderedInt
6. Memory。直接内存访问方法。`
allocateMemory
copyMemory
freeMemory
getAddress
getInt
putInt

有趣的使用案例

避免初始化
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(); // 构造器
o1.a(); // 打印出1

A o2 = A.class.newInstance(); // 反射
o2.a(); // 打印出1

A o3 = (A) unsafe.allocateInstance(A.class); // unsafe
o3.a(); // 打印出0

去考虑一下对你的单例模式会发生什么。

内存损坏
对于C语言编程人员来说这很常见。顺便说一句,这是常见的安全旁路技术。
考虑到一些检查访问规则的简单类:

class Guard {
    private int ACCESS_ALLOWED = 1;

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

客户代码是很安全的并调用giveAccess去检查访问规则。不幸的是,对于客户来说它总是返回错误。只有有特权的用户才能以某种方式改变ACCESS_ALLOWED 的值从而获得访问权。
事实上,这不准确,下面的代码解释了这一点:

Guard guard = new Guard();
guard.giveAccess();   // false, 访问失败

// bypass
Unsafe unsafe = getUnsafe();
Field f = guard.getClass().getDeclaredField("ACCESS_ALLOWED");
unsafe.putInt(guard, unsafe.objectFieldOffset(f), 42); // 内存损坏

guard.giveAccess(); // true, 访问获得许可

现在所有客户都可以获得无限访问权。
实际上,相同的功能也可以通过反射获得。但有趣的是我们可以改变任何对象,即使没有其引用。
例如,假如有另外一个Guard对象在内存里,它位于当前对象下一个位置。我们可以使用下面的代码来改变它的ACCESS_ALLOWED

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

注意到,我们没有任何这个对象的任何引用。16是Guard 对象在32位电脑里的大小。我们可以手动计算或者使用sizeOf方法,,它正在被定义。

sizeOf

使用objectFieldOffset方法我们可以实现C语言式的sizeOf功能。这个实现返回了对象的浅大小(shallow size )。

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();
    }

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

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

算法如下:遍历所有非static域,包括超类,获得每个域的偏移,找到最大的并加上间隙。也许我漏掉了一些,但思路是清晰的。
对于一个对象,更简单的sizeOf可以被实现如果我们仅仅从类的结构读取大小,它位于JVM 1.7 32 bit,有12位的偏移。

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

normalize 是一个将无符号的整形转化成无符号长整形的方法,可以使地址正确的使用。

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

令人惊讶的是,这个方法返回了与前面方法相同的结果。
事实上,为了获得更好、更安全和更精确的sizeOf方法,最好使用java.lang.instrument包,但它要求你为虚拟机指定agent。

浅拷贝
实现了计算浅对象的大小,我们可以简单的添加复制对象的功能。标准的解决办法需要使用Cloneable来改变你的代码,或者你可以在你的对象里实现私制的方法,但它不是多功能的方法
浅拷贝:

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将转换你的对象内存地址。反之亦然。

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];
}

这个复制函数可以用来复制任何类型对象,它的大小会被动态的计算。注意到,在复制对象后你需要将它投射到特定类型。

隐藏密码
在Unsafe中直接访问内存一个有趣的用途是将不想要的对象移出内存。
大部分的获取用户密码的APIs ,有byte[]或char[]的签名,为什么是数组?
这完全是出于安全的考虑,因为在不需要后我们可以将数组元素设为null,如果我们通过例如String获取密码,它有可能会作为一个对象被保存在内存里,设null只会解除引用,那个对象会一直存在直到被GC回收。
这个技巧创建了相同大小的假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); // ????????????

感到不安全。
更新:上面所说的方法不是真正安全。为了真正安全我们需要通过反射来清零备份的字符数组。

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] = '?';
}

多重继承
在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))

一个问题是我们需要先将其投射到Object类,为了欺骗编译器。

动态类
我们可以在运行时创建类,例如从编译的.class文件。为了执行刚刚所说,需要将类的内容到byte数组中,并正确的传递到defineClass方法中。

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

从文件读取如下所示:

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;
}

当你需要动态创建类,这可能很有用,如为存在的代码创建代理。

快速序列化
这个很实用。
大家都知道标准的Java序列化速度很慢,它要求类由一个公开的无参构造器。
Externalizable要好点,但它需要位要序列化的类定义规划。
流行的高性能库,像kryo有依赖,对于低内存的设备来说那是不可接受的。
但是全序列化的循环可以通过unsafe类轻易实现。
序列化:
1. 使用反射为对象构建规划,它可以一次实现。
2. 使用Unsafe的getLong、getInt、getObject等方法来获取真正的域值。
3. 添加类标识符来获取存储对象的能力。
4. 将他们写到文件或者任何输出。
你也可以添加压缩来节约空间。
反序列化
1. 创建序列化类的实例。allocateInstance可以帮你,因为它不需要任何构造器。
2. 创建规划。与序列化中一样。
3. 从文件或输入读取所有的域。
4. 使用Unsafe类的getLong、getInt、getObject等方法去填充对象。
实际上,在正确的实现中还有很多细节,但直觉是明确的。
这个序列化将是真正的快。
顺便提一下,在kyro中有一些使用Unsafe的尝试

大数组
正如你所知道的那样,Integer.MAX_VALUE是数组元素个数的最大值。使用直接内存分配我们可以创建一个由堆大小决定的数组。
这里是一个SuperArray的实现。

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()来照管它。它也不进行任何边界检查,因此任何非法访问都有可能造成虚拟机崩溃。
它对匹配计算很有用处,在那里代码可能使用大量的数组资料。同时,即时程序员也对它很有兴趣,在那里GC对大型数组的延迟可以打破限制。

并发
关于使用Unsafe. compareAndSwap方法来实现并发就几句话,它是原子的并且可以用来实现高性能的所无关的数据结构。
例如,考虑增加被很多线程使用的共享对象的值的问题。
首先我们定义Counter借口:

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

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

class CounterClient implements Runnable {
    private volatile 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 = ... // 创建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 volatile long counter = 0;

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

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

输出:

Counter result: 99542945
Time passed in ms: 679

运行很快,但压根没有线程管理,因此是不准确的。第二次尝试使用Java式的同步:

class SyncCounter implements Counter {
    private volatile long counter = 0;

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

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

输出:

Counter result: 100000000
Time passed in ms: 10136

基本的同步总是有用的,但时间掌握很糟糕。让我们用ReentrantReadWriteLock来试试:

class LockCounter implements Counter {
    private volatile 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怎么样?

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来看是否它有特权来使用它:

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基本可以被用来实现所无关的数据结构,直觉背后是简单的:
1. 有一些状态
2. 创建它的副本
3. 修改它
4. 执行CAS
5. 如果失败就重复
实际上,在现实中它比你想象的要困难,有很多问题如ABA问题、指令重排序等。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值