Java Magic. Part 4: sun.misc.Unsafe
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个方法。但是只有下面几个方法可能对你来说比较重要。
- 获取信息类。获取底层的内存信息:
 
addressSizepageSize
- 操作对象。提供了一些列操作对象和字段的方法:
 
allocateInstanceobjectFieldOffset
- 操作class文件。提供了一些操作class文件的方法:
 
staticFieldOffsetdefineClassdefineAnonymousClassensureClassInitialized
- 操作数组。
 
arrayBaseOffsetarrayIndexScale
- 同步机制。提供了底层的原子的同步机制
 
monitorEntertryMonitorEntermonitorExitcompareAndSwapIntputOrderedInt
- 直接内存操作
 
allocateMemorycopyMemoryfreeMemorygetAddressgetIntputInt
一些有趣的例子
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有很牛逼的用法,但是还是不推荐使用
Java Magic. Part 4: sun.misc.Unsafe的更多相关文章
- Java魔法类:sun.misc.Unsafe
		
Unsafe类在jdk 源码的多个类中用到,这个类的提供了一些绕开JVM的更底层功能,基于它的实现可以提高效率.但是,它是一把双刃剑:正如它的名字所预示的那样,它是Unsafe的,它所分配的内存需要手 ...
 - Java  sun.misc.unsafe类
		
Java是一个安全的开发工具,它阻止开发人员犯很多低级的错误,而大部份的错误都是基于内存管理方面的.如果你想搞破坏,可以使用Unsafe这个类.这个类是属于sun.*API中的类,并且它不是J2SE中 ...
 - sun.misc.unsafe
		
Java中大部分错误都是基于内存管理方面的.如果想破坏,可以使用Unsafe这个类. 实例化Unsafe: 下面两种方式是不行的 private Unsafe() {} //私有构造方法 @Calle ...
 - Java中的sun.misc.Unsafe包
		
chronicle项目:https://github.com/peter-lawrey/Java-Chronicle 这个项目是利用mmap机制来实现高效的读写数据,号称每秒写入5到20百万条数据. ...
 - Java sun.misc.Unsafe类的学习笔记
		
Java未开源的Unsafe类 Unsafe类可以为我们提供高效并且线程安全方式操作变量,直接和内存数据打交道. 获取Unsafe实体的方法 private static Unsafe getUnsa ...
 - java对象的内存布局(二):利用sun.misc.Unsafe获取类字段的偏移地址和读取字段的值
		
在上一篇文章中.我们列出了计算java对象大小的几个结论以及jol工具的使用,jol工具的源代码有兴趣的能够去看下.如今我们利用JDK中的sun.misc.Unsafe来计算下字段的偏移地址,一则验证 ...
 - Java的sun.misc.Unsafe类
		
阅读目录 前言 Unsafe类的作用 获取Unsafe对象 Unsafe类中的API 前言 以下sun.misc.Unsafe源码和demo基于jdk1.7: 最近在看J.U.C里的源码,很多都用到了 ...
 - java.util.concurrent各组件分析 一	sun.misc.Unsafe
		
java.util.concurrent各组件分析 一 sun.misc.Unsafe 说到concurrent包也叫并发包,该包下主要是线程操作,方便的进行并发编程,提到并发那么锁自然是不可缺少的, ...
 - sun.misc.Unsafe 详解
		
原文地址 译者:许巧辉 校对:梁海舰 Java是一门安全的编程语言,防止程序员犯很多愚蠢的错误,它们大部分是基于内存管理的.但是,有一种方式可以有意的执行一些不安全.容易犯错的操作,那就是使用Unsa ...
 
随机推荐
- django model 插入数据方法
			
需要插入的数据表结构如下: class UserInfo(models.Model): user_id =models.AutoField(primary_key=True) user_name=mo ...
 - [BZOJ5249][九省联考2018]IIIDX(线段树)
			
5249: [2018多省省队联测]IIIDX Time Limit: 40 Sec Memory Limit: 512 MBSubmit: 32 Solved: 17[Submit][Statu ...
 - Angular 4 辅助路由
			
1.辅助路由 2. 创建chat组件 ng g component chat 3. 组件html css: .chat{ background:green; height:100px; width:2 ...
 - ncnn编译安装-20190415
			
ncnn编译安装 1.git clone https://github.com/Tencent/ncnn 2.按照wiki说明来编译,根据需要,选择不同的编译方式.在ncnn/CMakeLists.t ...
 - Django安装与介绍
			
安装 Django是以Python为语言环境的,所以要先确保计算机上已经安装了Python. Linux ubuntu: sudo pip install Django==1.11.7 安装中指定了版 ...
 - C#连接Oracle数据库的方法(Oracle.DataAccess.Client也叫ODP.net)
			
官方下载地址(ODP.net)(中文):http://www.oracle.com/technetwork/cn/topics/dotnet/downloads/index.html 官方下载地址(O ...
 - JavaScript中的继承模式总结(九)
			
一.总结: //js中的几种继承 //原型链的问题,包含引用类型的原型属性会被实例共享,子类型无法给超类型传递参数 function SuperType() { this.colors = [&quo ...
 - appium定位方法
			
1.id定位 driver.find_element_by_id("这里是resource-id") 2.name定位 (新版本的appium 1.7 已经没有这个定位方法了) d ...
 - sublime text 3 3143
			
下载链接:https://download.sublimetext.com/Sublime%20Text%20Build%203143%20x64%20Setup.exe 注册信息:sublime t ...
 - cookie  js案例
			
//存cokie function setcookie(keys,value,time){ document.cookie=keys+"="+decodeURIComponent( ...