sun.misc.unsafe

Java中大部分错误都是基于内存管理方面的。如果想破坏,可以使用Unsafe这个类。

实例化Unsafe:

下面两种方式是不行的

private Unsafe() {} //私有构造方法

@CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        if(!VM.isSystemDomainLoader(var0.getClassLoader())) {//如果不是JDK信任的类去实例化,则抛出异常
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }

所以,简单方式就是通过反射去实例化Unsafe

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

 

避免初始化

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

import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class App
{
    public static void main( String[] args ) throws Exception {
        Field f = Unsafe.class.getDeclaredField("theUnsafe"); //Internal reference
        f.setAccessible(true);
        Unsafe unsafe = (Unsafe) f.get(null);
        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
    }
}

class A {
    private long a; // not initialized value
    public A() {
        this.a = 1; // initialization
    }
    public void a() {
        System.out.println(this.a);
    }
}

使用直接获取内存的方式实现浅克隆

如何实现浅克隆?在clone(){...}方法中调用super.clone(),对吗?这里存在的问题是首先你必须继续Cloneable接口,并且在所有你需要做浅克隆的对象中实现clone()方法,对于一个懒懒的程序员来说,这个工作量太大了。

浅克隆

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

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

隐藏密码

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

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

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

动态类(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;
}

大数组

正如你所知,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在大数组上延迟的限制。

结论(Conclusion)

即使Unsafe对应用程序很有用,但(建议)不要使用它。

原创文章,转载请注明: 转载自并发编程网 – ifeve.com本文链接地址: Java Magic. Part 4: sun.misc.Unsafe

 

posted @ 2017-04-10 17:40  孤独时光  阅读(209)  评论(0编辑  收藏  举报