• UnSafe


    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/

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

  • 相关阅读:
    gray-code——找规律
    [LeetCode] Decode Ways 解码方法个数、动态规划
    操作系统之面试常考(转)
    国内90%以上的 iOS 开发者,对 APNs 的认识都是错的
    vim配置为IDE环境(超详细,极力推荐 git)
    curl的使用(from 阮一峰)
    图片鉴黄服务提供商
    转: 【理念篇】关于数据驱动运维的几点认识
    业务调度链的染色数据上报和关联
    ITIL的考核管理体系
  • 原文地址:https://www.cnblogs.com/binarylei/p/10074776.html
Copyright © 2020-2023  润新知