菜单 学习猿地 - LMONKEY

VIP

开通学习猿地VIP

尊享10项VIP特权 持续新增

知识通关挑战

打卡带练!告别无效练习

接私单赚外块

VIP优先接,累计金额超百万

学习猿地私房课免费学

大厂实战课仅对VIP开放

你的一对一导师

每月可免费咨询大牛30次

领取更多软件工程师实用特权

入驻
267
0

UnSafe

原创
05/13 14:22
阅读数 83837

UnSafe

本文是 sun.misc.Unsafe 公共 API 的简要概述,及其一些有趣的用法。即使 Unsafe 对应用程序很有用,但(建议)不要使用它。

一、创建 Unsafe 实例

Unsafe 的构造器是私有的。它也有一个静态的 getUnsafe() 方法,但如果你直接调用 Unsafe.getUnsafe(),你可能会得到 SecurityException 异常。只愿 bootclasspath 类加载器加载的类才能使用这个方法。

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

第二种方法:Unsafe 类包含一个私有的 theUnsafe 实例,我们可以通过 Java 反射窃取该变量。

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

二、Unsafe API

sun.misc.Unsafe 类包含105个方法。实际上,对各种实体操作有几组重要方法,其中的一些如下:

2.1 Info 仅返回一些低级的内存信息

addressSize
pageSize

2.2 Objects 提供用于操作对象及其字段的方法

allocateInstance
objectFieldOffset

2.3 Classes 提供用于操作类及其静态字段的方法

staticFieldOffset
defineClass
defineAnonymousClass
ensureClassInitialized

2.4 Arrays 操作数组

arrayBaseOffset
arrayIndexScale

2.5 Synchronization 低级的同步原语

monitorEnter
tryMonitorEnter
monitorExit
compareAndSwapInt
putOrderedInt

2.6 Memory 直接内存访问方法

allocateMemory
copyMemory
freeMemory
getAddress
getInt
putInt

三、有趣的用例

3.1 避免初始化

当你想要跳过对象初始化阶段,或绕过构造器的安全检查,或实例化一个没有任何公共构造器的类,allocateInstance 方法是非常有用的。考虑以下类:

class A {
    private long a; // not initialized value

    public A() {
        this.a = 1; // initialization
    }

    public long a() { return this.a; }
}

使用构造器、反射和unsafe初始化它,将得到不同的结果。

```text
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

想想所有单例发生了什么。

3.2 内存崩溃(Memory corruption)

这对于每个 C 程序员来说是常见的。顺便说一下,它是绕过安全的常用技术。

考虑下那些用于检查“访问规则”的简单类:

class Guard {
    private int ACCESS_ALLOWED = 1;

    public boolean giveAccess() {
          return 42 == ACCESS_ALLOWED;
    }
}

客户端代码是非常安全的,并且通过调用 giveAccess() 来检查访问规则。可惜,对于客户,它总是返回 false。只有特权用户可以以某种方式改变 ACCESS_ALLOWED 常量的值并且得到访问(giveAccess()方法返回 true,译者注)。

实际上,这并不是真的。演示代码如下:

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 方法(它的定义,如下节)。

3.3 sizeOf

使用 objectFieldOffset 方法可以实现 C- 风格(C-style)的 sizeof 方法。这个实现返回对象的自身内存大小(译者注:shallow size)。

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
}

算法如下:通过所有非静态字段(包含父类的),获取每个字段的偏移量(offset),找到偏移最大值并填充字节数(padding)。我可能错过一些东西,但思路是明确的。

如果我们仅读取对象的类结构大小值,sizeOf 的实现可以更简单,这位于 JVM 1.7 32 bit 中的偏移量 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选项。

3.4 浅拷贝(Shallow copy)

为了实现计算对象自身内存大小,我们可以简单地添加拷贝对象方法。标准的解决方案是使用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 和 fromAddres s将对象转换为其在内存中的地址,反之亦然。

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

这个拷贝方法可以用来拷贝任何类型的对象,动态计算它的大小。注意,在拷贝后,你需要将对象转换成特定的类型。

3.5 隐藏密码(Hide Password)

在 Unsafe 中,一个更有趣的直接内存访问的用法是,从内存中删除不必要的对象。

检索用户密码的大多数 API 的签名为 byte[] 或 char[],为什么是数组呢?

这完全是出于安全的考虑,因为我们可以删除不需要的数组元素。如果将用户密码检索成字符串,这可以像一个对象一样在内存中保存,而删除该对象只需执行解除引用的操作。但是,这个对象仍然在内存中,由GC决定的时间来执行清除。

创建具有相同大小、假的String对象,来取代在内存中原来的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); // ????????????

感觉很安全。

修改:这并不安全。为了真正的安全,我们需要通过反射删除后台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] = '?';
}

3.6 多继承(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))

有一个问题,我们必须预先强制转换对象,以欺骗编译器。

3.7 动态类(Dynamic classes)

我们可以在运行时创建一个类,比如从已编译的.class文件中。将类内容读取为字节数组,并正确地传递给 defineClass 方法。

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

从定义文件(class文件)中读取(代码)如下:

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

当你必须动态创建类,而现有代码中有一些代理, 这是很有用的。

3.8 抛出异常(Throw an Exception)

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

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

该方法抛出受检异常,但你的代码不必捕捉或重新抛出它,正如运行时异常一样。

3.9 快速序列化(Fast Serialization)

这更有实用性。

大家都知道,标准 Java 的 Serializable 的序列化能力是非常慢的。它同时要求类必须有一个公共的、无参数的构造器。

Externalizable 比较好,但它需要定义类序列化的模式。

流行的高性能库,比如 kryo 具有依赖性,这对于低内存要求来说是不可接受的。

unsafe 类可以很容易实现完整的序列化周期。

序列化:

  • 使用反射构建模式对象,类只可做一次。
  • 使用 Unsafe 方法,如 getLong、getInt、getObject 等来检索实际字段值。
  • 添加类标识,以便有能力恢复该对象
  • 将它们写入文件或任意输出
  • 你也可以添加压缩(步骤)以节省空间。

反序列化:

  • 创建已序列化对象实例,使用 allocateInstance 协助(即可),因为不需要任何构造器。
  • 构建模式,与序列化的步骤1相同。
  • 从文件或任意输入中读取所有字段。
  • 使用 Unsafe 方法,如 putLong、putInt、putObject 等来填充该对象。
  • 实际上,在正确的实现过程中还有更多的细节,但思路是明确的。

这个序列化将非常快。

顺便说一下,在 kryo 中有使用 Unsafe 的一些尝试 http://code.google.com/p/kryo/issues/detail?id=75

3.9 大数组(Big Arrays)

正如你所知,Java 数组大小的最大值为 Integer.MAX_VALUE。使用直接内存分配,我们创建的数组大小受限于堆大小。

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

简单用法:

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

实际上,这是堆外内存(off-heap memory)技术,在 java.nio 包中部分可用。

这种方式的内存分配不在堆上,且不受 GC 管理,所以必须小心 Unsafe.freeMemory() 的使用。它也不执行任何边界检查,所以任何非法访问可能会导致 JVM 崩溃。

这可用于数学计算,代码可操作大数组的数据。此外,这可引起实时程序员的兴趣,可打破 GC 在大数组上延迟的限制。

3.10 并发(Concurrency)

几句关于 Unsafe 的并发性。compareAndSwap 方法是原子的,并且可用来实现高性能的、无锁的数据结构。

比如,考虑问题:在使用大量线程的共享对象上增长值。

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

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

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

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

第一个无锁版本的计数器:

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式同步:

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

激进的同步有效,但耗时长。试试 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;
    }
}

输出:

Counter result: 100000000
Time passed in ms: 8065

仍然正确,耗时较短。atomics的运行效果如何?

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原始的compareAndSwapLong,看看它是否真的只有特权才能使用它?

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

看起来似乎等价于 atomics。atomics 使用 Unsafe?(是的)

实际上,这个例子很简单,但它展示了 Unsafe 的一些能力。

如我所说,CAS 原语可以用来实现无锁的数据结构。背后的原理很简单:

  • 有一些状态
  • 创建它的副本
  • 修改它
  • 执行CAS
  • 如果失败,重复尝试

实际上,现实中比你现象的更难。存在着许多问题,如 ABA 问题、指令重排序等。

如果你真的感兴趣,可以参考 lock-free HashMap 的精彩展示。

参考:

  1. 《sun.misc.Unsafe》:http://ifeve.com/sun-misc-unsafe/

每天用心记录一点点。内容也许不重要,但习惯很重要!

发表评论

0/200
267 点赞
0 评论
收藏