您的位置:首页 > Web前端

Java Magic. Part 4: sun.misc.Unsafe

2016-02-06 13:44 597 查看

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

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

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

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,这个操作每个类只需要一次。

使用
Unsafe
getLong
,
getInt
,
getObject
来获取实际的值

添加一个类的identifier

把这些通通写到文件或者别的什么里面去

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

Deserialization:

创建一个待反序列化的类,通过
allocateInstance
,因为这个可以不适用任何构造函数

创建一个schema,和序列化里面的操作一样啦。

从文件中获取所有输出

使用
Unsafe
putLong
,
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

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.



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


Conclusion

尽管
Unsafe
有很牛逼的用法,但是还是不推荐使用
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: