好书推荐: JAVA编程思想(第4/四版)
Java是个安全的编程语言,它可以防止程序员犯一些低级错误,往往这些错误都是基于内存管理的。但是在Java中仍然有办法故意的犯这些错误——使用Unsafe类。 这篇文章将快速的阐述sun.misc.Unsafe类的 public API和少许有意思的用法。
在使用前,我们都需要创建一个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
sun.misc.Unsafe有105个方法,可以分为多组重要的操作对象属性的方法,这里列举一些:
addressSize
pageSize
allocateInstance
objectFieldOffset
staticFieldOffset
defineClass
defineAnonymousClass
ensureClassInitialized
arrayBaseOffset
arrayIndexScale
monitorEnter
tryMonitorEnter
monitorExit
compareAndSwapInt
putOrderedInt
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方法来获取对象占用内存的大小。
使用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来实现。
序列化:
你也可以添加压缩功能来减少空间消耗。
反序列化:
实际上,有很多细节没有一一展开,但这个思路就是这样的。这个序列化反序列确实很快。顺便说一下,在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比你想象的复杂得多,因为有很多问题,比如: 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有一堆好用的用法,但永远不要使用它。
翻译自:http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/