神刀安全网

sun.misc.Unsafe

好书推荐: JAVA编程思想(第4/四版)

Java是个安全的编程语言,它可以防止程序员犯一些低级错误,往往这些错误都是基于内存管理的。但是在Java中仍然有办法故意的犯这些错误——使用Unsafe类。 这篇文章将快速的阐述sun.misc.Unsafe类的 public API和少许有意思的用法。

Unsafe类的初始化

在使用前,我们都需要创建一个Unsafe类的实例对象,但不能通过Unsafe unsafe = new Unsafe()方式来实现, 因为Unsafe类的构造函数是private的。Unsafe类有一个getUnsafe()的静态方法,但你如果尝试调用Unsafe.getUnsafe()的话,可能会抛出SecurityException异常,因为只有信任的代码才能使用这个静态方法。

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

上面的代码说明了java是如何校验代码是否是可信任的。它仅校验了我们的代码是否被BootClassloader加载。 要让我们都代码被信任,可以在运行你的程序时使用bootclasspath参数来指定系统类和你要使用Unsafe的类路径。

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

但是这个方法太麻烦了。 Unsafe类里面有个属性就是一个它的实例对象,即theUnsafe,这个属性是private的。我们可以通过反射方式把这个属性取出来。

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

注意:忽略你的IDE的error提示。比如:eclipse会提示“Access restriction…”,但你要是运行代码,会发现一切正常。如果你觉得这个提示很烦,可以这样设置来忽略这个提示:

Preferences -> Java -> Compiler -> Errors/Warnings -> Deprecatedand restrictedAPI -> Forbiddenreference -> Warning 

Unsafe API

sun.misc.Unsafe有105个方法,可以分为多组重要的操作对象属性的方法,这里列举一些:

  • Info . 返回一些底层的内存信息.
    • addressSize
    • pageSize
  • Objects . 提供了操作对象和其属性的方法
    • allocateInstance
    • objectFieldOffset
  • Classes . 提供了操作类和静态属性的方法
    • staticFieldOffset
    • defineClass
    • defineAnonymousClass
    • ensureClassInitialized
  • Arrays . 数组操作
    • arrayBaseOffset
    • arrayIndexScale
  • Synchronization . 底层原始的同步管理
    • monitorEnter
    • tryMonitorEnter
    • monitorExit
    • compareAndSwapInt
    • putOrderedInt
  • Memory . 直接内存访问的方法
    • allocateMemory
    • copyMemory
    • freeMemory
    • getAddress
    • getInt
    • putInt

一些有意思的案例

避免实例化

allocateInstance方法在这几个场景下很有用:跳过对象的实例化阶段(通过构造函数)、忽略构造函数的安全检查(反射newInstance()时)、你需要某类的实例但该类没有public的构造函数。比如下面的类:

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 

由此想想你的单例真的能保证单例吗。

内存变更

这对C语言开发者很常用,他同时也是一种常见的跳过安全检查的方法,举个校验访问权限的例子:

class Guard {     private int ACCESS_ALLOWED = 1;       public boolean giveAccess() {         return 42 == ACCESS_ALLOWED;     } } 

客户端代码通过调用giveAccess()来校验访问权限,这种方式是很安全。不幸的是,不论谁调用这个方法,它都返回false。只有通过特殊手段来改变ACCESS_ALLOWED常量的值,从而使客户端获得访问权限。 下面是实例代码:

Guardguard = new Guard(); guard.giveAccess();  // false, no access   // bypass Unsafeunsafe = getUnsafe(); Field f = guard.getClass().getDeclaredField("ACCESS_ALLOWED"); unsafe.putInt(guard, unsafe.objectFieldOffset(f), 42); // memory corruption   guard.giveAccess(); // true, access granted 

现在所有的客户端都可以获得无限制的访问权限。 实际上,我们可以通过反射的方式达到相同的效果,但有意思的是,使用unsafe可以让我们修改任意对象,甚至我们没有获取到对象的引用。 比如:在内存中有另外一个Guard对象在当前这个guard对象的下一个内存区块中,我们可以用下面的方式来修改它的ACCESS_ALLOWED属性:

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

注意,我们并没有这个新的Guard对象的引用,16是Guard对象在32位机器中占用的空间大小。我们可以通过马上要讲到的sizeOf方法来获取对象占用内存的大小。

sizeOf

使用obectFieldOffset方法可以使我们实现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;  // 字节对齐 } 

算法是这样的:遍历所有当前类和所有父类的非静态属性,获得每个属性的偏移量,找到最大的偏移量,加上字节对齐所需的内存大小。可能不一定全,但思路是清晰的。 有个更简单的方式实现sizeOf,如果我们仅根据类的结构来读取对象占用的内存大小(该对象在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。

浅拷贝

既然已经实现了对象浅内存大小的计算功能,我们可以很容易的添加一点功能实现对象拷贝。标准的做法是让你的对象类实现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);// array地址的偏移量     return normalize(getUnsafe().getInt(array, baseOffset)); // 由于obj的地址是array数组的第一个元素,所以直接取array数组的第一个元素的值,在32位机器上是int长度。 }   static Object fromAddress(long address) {     Object[] array = new Object[] {null};     long baseOffset = getUnsafe().arrayBaseOffset(Object[].class);     getUnsafe().putLong(array, baseOffset, address);//把对象的地址放在array的第一个元素的位置     return array[0]; // 这里返回的就不是地址了,而是对象,通过getUnsafe().getInt(array, baseOffset)才是返回的地址。 } 

这种拷贝的方式可以用于任意类型的对象,对象的大小会自动的计算。值得注意的是,在拷贝以后,你需要把返回的对象强制转换为你需要的类型。

隐藏密码

一个更有意思的使用Unsafe直接访问内存的用法是:从内存中移除不想要的对象。 大部分获取用户密码的API都使用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); // ???????????? 

这样做,密码就安全了吗? 注意:这个方式仍不安全。因为安全的做法是需要通过反射的方式把String中的char数组中的每个元素置为”?”:

FieldstringValue = 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)) 

这里为了欺骗编译器,需要先把Integer对象转换为Object类型。

动态类

我可以在运行时创建类,比如根据编译后得到.class文件来创建类。其机制是把.class的内容读取到byte数组中,并传给defineClass方法。

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

从文件中读取到byte数组中:

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

当你必须动态的创建类时,这个很管用,比如:一些代理或者基于已有的代码进行面向切面的编程。

抛异常

你是否不喜欢catch异常?没问题,这么干吧:

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

这个方法抛出一个需要被主动catch的异常,但是你的代码可以不去catch或者重新抛出异常,它就行抛出了一个运行时异常一样。(把IOException()当做运行时一样抛出,但调用方不需要catch)

快速的序列化

这个更有用些。 每个人知道java标准的Serializable功能性能很差,它还需要你的类必须要有一个无参的构造函数。 Externalizable 要好一点,但他需要为类的序列化过程进行定义。 热门高性能的库,例如 kryo 有依赖,这可能不适合低内存的场景。 但是完整的序列化和反序列过程可以很容易用unsafe来实现。

序列化:

  • 通过反射方式构建对象的schema,只需构架你一次就行了
  • 使用Unsafe的getLong、getInt、getObject等方法来获取实际的属性值
  • 添加一个类用来存储这个对象
  • 把对象写入到文件或者其他输出中

你也可以添加压缩功能来减少空间消耗。

反序列化:

  • 创建一个序列化类的实例。这时候allocateInstance方法就很有用了,因为它不需要任何构造函数
  • 构建schema,和序列化中的步骤一样
  • 从文件或者其他输入中读取所有的属性
  • 使用Unsafe的putLong、putInt、putObject方法等来填充前面创建的对象实例

实际上,有很多细节没有一一展开,但这个思路就是这样的。这个序列化反序列确实很快。顺便说一下,在kryo中有一些使用Unsafe的尝试: http://code.google.com/p/kryo/issues/detail?id=75

大数组

你可能知道 Integer.MAX_VALUE是java数组的最大长度。通过直接的内存配分,我们可以创建出很大的java数组,其上限是堆空间的大小。

下面是 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;     } } 

可以这样使用SuperArray:

long SUPER_SIZE = (long)Integer.MAX_VALUE * 2; SuperArrayarray = 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包中有部分使用。

它并不是分配在java堆中,也不受java gc的管理,要小心使用 Unsafe.freeMemory(),因为它不做任何边界检查,任何非法访问都会导致jvm crash。

它在数学计算中还是有用的,可以通过代码操作大数组数据。它对于实时系统开发的程序员来说很有意思,可以打破大数组gc上的限制。

并发

Unsafe compareAndSwap的并发简单来说就是,它说原子操作,可用于实现高并发的无锁数据结构。

例如,在多个线程间共享自增数据的问题:

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

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

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

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; ExecutorServiceservice = Executors.newFixedThreadPool(NUM_OF_THREADS); Countercounter = ... // 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;     } } 

输出为:

Counterresult: 99542945 Timepassedin ms: 679 

运行很快,但是完全没有线程管理,所以结果说不准确的。

下一步,实现一个最简单的java同步counter:

class SyncCounter implements Counter {     private long counter = 0;       @Override     public synchronized void increment() {         counter++;     }       @Override     public long getCounter() {         return counter;     } } 

输出为:

Counterresult: 100000000 Timepassedin ms: 10136 

基础的同步功能还是起到了作用,但是耗时太长了。

下面让我们尝试使用 ReentrantReadWriteLock:

class LockCounter implements Counter {     private long counter = 0;     private WriteLocklock = new ReentrantReadWriteLock().writeLock();       @Override     public void increment() {         lock.lock();         counter++;         lock.unlock();     }       @Override     public long getCounter() {         return counter;     } } 

出为:

Counterresult: 100000000 Timepassedin ms: 8065 

仍然说正确的,耗时也好一些。如果使用原子操作会怎么样:

class AtomicCounter implements Counter {     AtomicLongcounter = new AtomicLong(0);       @Override     public void increment() {         counter.incrementAndGet();     }       @Override     public long getCounter() {         return counter.get();     } } 

输出为:

Counterresult: 100000000 Timepassedin ms: 6552 

AtomicCounter效果要更好一些。

最后,看看Unsafe原始的 compareAndSwapLong方法是否真那么神奇:

class CASCounter implements Counter {     private volatile long counter = 0;     private Unsafeunsafe;     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;     } 

输出为:

Counterresult: 100000000 Timepassedin ms: 6454 

嗯,好像跟原子操作差不多,难道原子操作就是用这个这个方式?(答案:是)

实际上,这个例子很简单,但说明了Unsafe的强大之处。

如我所说,原始的CAS(CompareAndSwap)操作可以实现无锁的数据结构,其背后的机制是:

  • 有一定的状态
  • 拷贝一份
  • 修改它
  • 执行CAS
  • 如果执行CAS失败了,则这个过程

事实上,CAS比你想象的复杂得多,因为有很多问题,比如: ABA问题 ,指令重拍问题等。

如果你真得很感兴趣,可以看看这个牛x的ppt: 无锁HashMap

注意:在counter变量上增加volatile,可以避免出现无限循环的风险。

彩蛋

Unsafe文档中park方法的注释有个我见过的最长的语句(。。。我也翻译不了了):

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.

结论

虽然,Unsafe有一堆好用的用法,但永远不要使用它。

Reference

翻译自:http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » sun.misc.Unsafe

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址