Java Part 2: sun.misc.Unsafe

翻译自:unsafe

Unsafe 实例

首先,我们需要获取到 Unsafe 对象的一个实例。并没有这样一种 Unsafe unsafe = new Unsafe() 简单的方式来得到该实例。因为 Unsafe 类的构造器是私有的。虽然有一个静态的 getUnsafe() 方法,但是如果直接调用的话,会抛出 SecurityException 异常。只能这个方法只能在受信的代码内调用。

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

java 如何检查受信代码:只要就检查代码是否被主 classloader 加载就可以了。

我们可以让我们的代码变成是受信的,只要在运行程序时使用可选的 bootclasspath

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

另一种方法是使用反射从 Unsafe 类上得到它私有的 Unsafe 实例。

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

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

Interesting use cases

避免初始化

allocateInstance 方法在我们需要跳过对象初始化阶段;或绕过构造器中的安全检查;或想实例化某个没有 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

Just think what happens to all your Singletons.

Memory corruption

This one is usual for every C programmer. By the way, its common technique for security bypass.

假设某有检查访问规则的简单类:

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

客户代码十分安全并且调用 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

现在,所有的用户都可以无限制的访问了。

实际上,使用反射也可以得到同样的功能。但是,这种方式可以修改任意的类,即使是没有引用到的对象。

例如,有另外一个 Guard 对象,内存地址是当前 Guard 对象的下一个。我们可以修改其的 ACCESS_ALLOWED 属性,使用如下代码:

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

显然,我们没有任何对这个对象的引用。16 则是 Guard 对象在 32 为架构下的大小。可以手工计算其的大小或者使用 sizeOf 方法。

sizeOf

使用 objectFieldOffset 方法,我们可以实现 c 风格的 sizeof 函数。该实现返回对象的浅 (shallow) 大小 :

public static long sizeOf(Object o){
  Unsafe u = getUnsafe();
  HashSet fields = new HashSet();
  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
}

算法如下:遍历所有的 非静态 field ,包括父类的,获取每个 field 的偏移量,找出最大的偏移量,并且添加填充。

还有一个更简单的 sizeOf 方法,只要读取对象类结构中的 size 大小。JVM 1.7 32 bit 中该值在 offset 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 的代理选项。

Shallow copy

通过我们实现的计算对象浅大小,我们可以添加复制对象的功能。标准的解决方法需要用 Cloneable 修改你的代码,或你可以实现自定义自己对象的复制函数,但是并不能成为通用的函数。

static Object shallowCopy(Objecto bj){
  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(Objecto bj){
  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[]。为什么是数组呢?

这主要是由于安全的原因,我们可以在不需要这些数组时,直接把数组原属置空。如果我们以字符串取回密码,则可以像对象一样保存在内存中,并且执行解引用操作置空该字符串。
但是,该对象仍然保存在内存中,直到 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);// ????????????

Feel safe.
UPDATE:That way is not really safe. For real safety we need to nullify backed char array via reflection:

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

Thanks toPeter Verhasfor pointing out that.

Multiple Inheritance

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

One problem that we must do it with pre-casting to object. To cheat compiler.

Dynamic classes

我们可以在运行时创建类,例如读取编译好的 .class 文件。然后执行读 class 内容到 byte 数组,并且传递给 defineClass 方法。

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

And reading from file defined as:

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

This can be useful, when you must create classes dynamically, some proxies or aspects for existing code.

Throw an Exception

不喜欢受检查的异常?没问题。

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

这个方法抛出一个受检查异常,但是你的代码可以不强制或重抛出它。就像运行时异常一样。

Fast Serialization

This one is more practical.

众所周知,标准的 java Serializable 执行序列化的性能非常低。而且还还需要一个公有无参数的构造器。

Externalizable 会好一点, 但是这需要定义要序列化类的 schema。

出名的高性能库,例如kryo,令人惊讶的低内存要求。

但是,完全序列化循环,可以简单的通过 unsafe 获得。

Serialization:

  • Build schema for object using reflection. It can be done once for class.
  • Use Unsafe methods getLong,getInt,getObject, etc. to retrieve actual field values.
  • Add class identifier to have capability restore this object.
    Write them to the file or any output.

You can also add compression to save space.

Deserialization:

  • Create instance of serialized class. allocateInstance helps, because does not require any constructor.
  • Build schema. The same as 1 step in serialization.
  • Read all fields from file or any input.
  • Use Unsafe methods putLong,putInt,putObject, etc. to fill the object.

Actually, there are much more details in correct inplementation, but intuition is clear.

This serialization will be really fast.

By the way, there are some attempts in kryo to use Unsafe http://code.google.com/p/kryo/issues/detail?id=75

Big Arrays

显然,Integer.MAX_VALUE 常量就是 java 数组的最大值。直接使用内存分配,我们可以创建大小只受到堆大小限制的数组。

例子实现:

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 intget(long idx){
    return getUnsafe().getByte(address+idx*BYTE);
  }

  public long size(){
    return size;
  }
}

And sample usage:

long SUPER_SIZE = (long)Integer.MAX_VALUE * 2;
SuperArray array = new SuperArray(SUPER_SIZE);
System.out.println("Array size:"+array.size());// 4294967294

for(inti=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

In fact, this technique uses off-heap memory and partially available in java.nio package.

以这种方式分配的内存并不在堆中,并且也不在 GC 的管理之下。所以,需要小心的使用 Unsafe.freeMemory() 释放。也不执行任何的界限检查,所以任何非法的访问会导致 JVM 挂机。

It can be useful for math computations, where code can operate with large arrays of data. Also, it can be interesting for realtime programmers, where GC delays on large arrays can break the limits.

Concurrency

Unsafe.compareAndSwap 方法是原子的,可以用于实现高性能无锁的数据结构。

例如,看看在多线程共享对象上增加值的问题。

First we define simple interface Counter:

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

Then we define worker threadCounterClient, that usesCounter:

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

And this is testing code:

int NUM_OF_THREADS = 1000;
int NUM_OF_INCREMENTS = 100000;
ExecutorService service = Executors.newFixedThreadPool(NUM_OF_THREADS);
Counter counter = ... // creating instance of specific counterlong 
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));

First implementation is not-synchronized counter:

class StupidCounter implements Counter{
  private long counter=0;

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

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

Output:

Counter result: 99542945
Time passed in ms: 679

运行的速度很快,但是根本没有任何内存的管理,所以,结果会是不精确的。

Second attempt, add easiest java-way synchronization:

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

Output:

Counter result: 100000000
Time passed in ms: 10136

悲观的同步总是执行。但是执行时间很慢。Let's try ReentrantReadWriteLock:

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

Output:

Counter result: 100000000
Time passed in ms: 8065

同样正确,而且时间也更短。但是 atomic 会是怎么样的?

class AtomicCounter implements Counter {
    AtomicLong counter = new AtomicLong(0);

    @Override
    public void increment() {
        counter.incrementAndGet();
    }

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

Output:

Counter result: 100000000
Time passed in ms: 6552

Atomic Counteris even better. Finally, try Unsafe primitive compareAndSwapLong to see if it is really privilegy to use it.

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

Output:

Counter result: 100000000
Time passed in ms: 6454

很好,看起来和 atomics 先等。是不是 atomics 使用 Unsafe? (YES)

In fact this example is easy enough, but it shows some power of Unsafe.

CAS 原语可以被用于实现无锁的数据结构。这后面的想法很简单:

  • Have some state
  • Create a copy of it
  • Modify it
  • Perform CAS
  • Repeat if it fails

Actually, in real it is more hard than you can imagine. There are a lot of problems like ABA Problem, instructions reordering, etc.

If you really interested, you can refer to the awesome presentation about lock-free HashMap

UPDATE:Addedvolatilekeyword tocountervariable to avoid risk of infinite loop.

Kudos to Nitsan Wakart

Bonus

Documentation for park method from Unsafe class contains longest English sentence I've ever seen:

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.

Conclusion

Although,Unsafe has a bunch of useful applications, never use it.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容