Java是以安全著称,但在Java中有一个类是一个Bug级别的存在,那就是Unsafe
. 前面已经说过Unsafe
在java中的使用,此处我们直接说用法:
避免初始化 当你想跳过对象初始化的阶段,或者绕过构造函数的检查,去实例化没有任何公共构造函数的类,可以使用allocateInstance
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class A { private long a; public A () { this .a = 1 ; } public long a () { return this .a; } } ``` 使用构造函数、反射和unsafe初始化它,将得到不同的结果。 ``` java public static void main (String[] args) throws IllegalAccessException, InstantiationException { A o1 = new A(); System.out.println(o1.a()); A o2 = A.class.newInstance(); System.out.println(o2.a()); A o3 = (A) UnsafeUtils.getUnsafe().allocateInstance(A.class); System.out.println(o3.a()); }
内存崩溃(Memory corruption) 我们可以使用Unsafe
去做一些绕过安全的技术:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Guard { private int ACCESS_ALLOWED = 1 ; public boolean giveAccess () { return 42 == ACCESS_ALLOWED; } } ``` 当客户端调用`giveAccess`代码是,始终返回的都是`false `.使用`Unsafe`可以绕过权限: ``` java Guard guard=new Guard(); System.out.println("修改前:" +guard.giveAccess()); Field f = guard.getClass().getDeclaredField("ACCESS_ALLOWED" ); unsafe.putInt(guard, unsafe.objectFieldOffset(f), 42 ); System.out.println("修改后:" +guard.giveAccess());
计算对象的大小 sizeOf
是返回对象的自身内存大小。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public static long sizeOf (Object o) { Unsafe u = UnsafeUtils.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(); } long maxSize = 0 ; for (Field f : fields) { long offset = u.objectFieldOffset(f); if (offset > maxSize) { maxSize = offset; } } return ((maxSize/8 ) + 1 ) * 8 ; } ``` 如果只是对象类的结构大小,那么可以更简单的实现: ```java public static long sizeOf (Object object) { return UnsafeUtils.getUnsafe().getAddress( normalize(UnsafeUtils.getUnsafe().getInt(object, 4L )) + 12L ); } private static long normalize (int value) { if (value >= 0 ) return value; return (~0L >>> 32 ) & value; }
浅拷贝(Shallow copy) 为了计算自身内存的大小,可以简单的添加拷贝的对象方法,标准的解决方案是使用Cloneable
修改代码,或者实现自定义的拷贝方法.
浅拷贝:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 public class ShallowCopy { static Unsafe unsafe = UnsafeUtils.getUnsafe(); public static Object shallowCopy (Object obj) { long size = sizeOf(obj); long start = toAddress(obj); long address = unsafe.allocateMemory(size); unsafe.copyMemory(start, address, size); return fromAddress(address); } static long toAddress (Object obj) { Object[] array = new Object[] {obj}; long baseOffset = unsafe.arrayBaseOffset(Object[].class); return normalize(unsafe.getInt(array, baseOffset)); } static Object fromAddress (long address) { Object[] array = new Object[] {null }; long baseOffset = unsafe.arrayBaseOffset(Object[].class); unsafe.putLong(array, baseOffset, address); return array[0 ]; } public static long sizeOf (Object o) { 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(); } long maxSize = 0 ; for (Field f : fields) { long offset = unsafe.objectFieldOffset(f); if (offset > maxSize) { maxSize = offset; } } return ((maxSize/8 ) + 1 ) * 8 ; } private static long normalize (int value) { if (value >= 0 ) return value; return (~0L >>> 32 ) & value; } } ``` ### 隐藏密码(Hide Password) 在`Unsafe`类中可以删除内存中的对象,比如密码信息。用户的密码大多数都是byte []或char []数组,为什么要使用数组呢? 这个是出于安全的考虑,因为我们可以删除不需要的数组元素,如果是一个字符串对象的话,这可以像一个对象在内存中保存,删除该对象只是删除了引用,真正的数据还保存在内存中。 ``` java String password = new String("l00k@myHor$e" ); String fake = new String(password.replaceAll("." , "?" )); System.out.println(password); System.out.println(fake); unsafe.copyMemory( fake, 0L , null , toAddress(password), sizeOf(password)); System.out.println(password); System.out.println(fake); ``` <!-- ### 多继承(Multiple Inheritance) Java中没有多继承。 ``` 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方法。可以动态调用java的class 文件 ``` java byte[] classContents = getClassContent(); Class c = unsafe.defineClass(null , classContents, 0 , classContents.length,null ,null ); Object res = c.getMethod("test" ).invoke(c.newInstance(), null ); System.out.println(res);
1 2 3 4 5 6 7 8 9 10 private static byte [] getClassContent() throws Exception { File f = new File("D:\\work\\open\\rzframe\\rz-common\\src\\test\\java\\com\\rz\\frame\\cas\\AppTest.class" ); FileInputStream input = new FileInputStream(f); byte [] content = new byte [(int )f.length()]; input.read(content); input.close(); return content; }
大数组(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
实际上,这是堆外内存(off-heap memory)技术,在java.nio包中部分可用。
这种方式的内存分配不在堆上,且不受GC管理,所以必须小心Unsafe.freeMemory()的使用。它也不执行任何边界检查,所以任何非法访问可能会导致JVM崩溃。
这可用于数学计算,代码可操作大数组的数据,可打破GC在大数组上延迟的限制。