Unsafe類是java中非常特別的一個類。它名字就叫做“不安全”,提供的操作可以直接讀寫內(nèi)存、獲得地址偏移值、鎖定或釋放線程。
通過正常途徑是無法獲得Unsafe實例的,首先它的構(gòu)造方法是私有的,然后,即使你調(diào)用它的getUnsafe方法,也會拋出SecurityException。
A collection of methods for performing low-level, unsafe operations. Although the class and all methods are public, use of this class islimited because only trusted code can obtain instances of it.
任何關(guān)于Unsafe類的文章都不會推薦我們在代碼中使用它,但這并不妨礙我們了解它可以做什么。下面我們來看下利用Unsafe類我們是否可以做點有趣的事情。
獲取Unsafe實例
public static Unsafe getUnsafeInstance() throws Exception{
Field unsafeStaticField =
Unsafe.class.getDeclaredField("theUnsafe");
unsafeStaticField.setAccessible(true);
return (Unsafe) unsafeStaticField.get(Unsafe.class);
}
通過java反射機制,我們跳過了安全檢測,拿到了一個unsafe類的實例。
我找遍了Unsafe類的API,沒有發(fā)現(xiàn)可以直接獲取對象地址的方法,Unsafe中操作地址相關(guān)的方法都要求提供一個Object類型的參數(shù),用來獲取對象的初始地址。
修改和讀取數(shù)組中的值
Unsafe u = getUnsafeInstance();
int[] arr = {1,2,3,4,5,6,7,8,9,10};
int b = u.arrayBaseOffset(int[].class);
int s = u.arrayIndexScale(int[].class);
u.putInt(arr, (long)b+s*9, 1);
for(int i=0;i<10;i++){
int v = u.getInt(arr, (long)b+s*i);
System.out.print(v+“ ”);
}
打印結(jié)果:1 2 3 4 5 6 7 8 9 1 ,可以看到,成功讀出了數(shù)組中的值,而且最后一個值由10改為了1。
arrayBaseOffset: 返回當(dāng)前數(shù)組第一個元素地址相對于數(shù)組起始地址的偏移值,在本例中返回6。
arrayIndexScale: 返回當(dāng)前數(shù)組一個元素占用的字節(jié)數(shù),在本例中返回4。
putInt(obj,offset,intval): 獲取數(shù)組對象obj的起始地址,加上偏移值,得到對應(yīng)元素的地址,將intval寫入內(nèi)存。
getInt(obj,offset): 獲取數(shù)組對象obj的起始地址,加上偏移值,得到對應(yīng)元素的地址,從而獲得元素的值。
偏移值: 數(shù)組元素偏移值 = arrayBaseOffset + arrayIndexScalse * i。
獲取對象實例
/** Allocate an instance but do not run any constructor.
Initializes the class if it has not yet been. */
public native Object allocateInstance(Class cls) throws InstantiationException;
allocateInstance: 在不執(zhí)行構(gòu)造方法的前提下,獲取一個類的實例,即使這個類的構(gòu)造方法是私有的。
修改靜態(tài)變量和實例變量的值
先定義一個Test類
public class Test {
public int intfield ;
public static int staticIntField;
public static int[] arr;
private Test(){
System.out.println("constructor called");
}
}
修改Test類的實例變量
Unsafe u = getUnsafeInstance();
Test t = (Test) u.allocateInstance(Test.class);
long b1 = u.objectFieldOffset(Test.class.getDeclaredField("intfield"));
u.putInt(t, b1, 2);
System.out.println("intfield:"+t.intfield);
這里使用allocateInstance方法獲取了一個Test類的實例,并且沒有打印“constructor called”,說明構(gòu)造方法沒有調(diào)用。
修改實例變量與修改數(shù)組的值類似,同樣要獲取地址偏移值,然后調(diào)用putInt方法。
- objectFieldOffset: 獲取對象某個屬性的地址偏移值。
我們通過Unsafe類修改了Java堆中的數(shù)據(jù)。
修改Test類的靜態(tài)變量
Field staticIntField = Test.class.getDeclaredField("staticIntField");
Object o = u.staticFieldBase(staticIntField);
System.out.prinln(o==Test.class);
Long b4 = u.staticFieldOffset(staticIntField);
//因為是靜態(tài)變量,傳入的Object參數(shù)應(yīng)為class對象
u.putInt(o, b4, 10);
System.out.println("staticIntField:"+u.getInt(Test.class, b4));
打印結(jié)果:
true
staticIntField:10
靜態(tài)變量與實例變量不同之處在于,靜態(tài)變量位于于方法區(qū)中,它的地址偏移值與Test類在方法區(qū)的地址相關(guān),與Test類的實例無關(guān)。
staticFieldBase: 獲取靜態(tài)變量所屬的類在方法區(qū)的首地址。可以看到,返回的對象就是Test.class。
staticFieldOffset: 獲取靜態(tài)變量地址偏移值。
我們通過Unsafe類修改了方法區(qū)中的信息。
調(diào)戲String.intern
在jdk7中,String.intern不再拷貝string對象實例,而是保存第一次出現(xiàn)的對象的引用。在下面的代碼中,通過Unsafe修改被引用對象s的私有屬性value達到間接修改s1的效果!
String s = "abc";
//保存s的引用
s.intern();
//此時s1==s,地址相同
String s1 = "abc";
Unsafe u = getUnsafeInstance();
//獲取s的實例變量value
Field valueInString = String.class.getDeclaredField("value");
//獲取value的變量偏移值
long offset = u.objectFieldOffset(valueInString);
//value本身是一個char[],要修改它元素的值,仍要獲取baseOffset和indexScale
long base = u.arrayBaseOffset(char[].class);
long scale = u.arrayIndexScale(char[].class);
//獲取value
char[] values = (char[]) u.getObject(s, offset);
//為value賦值
u.putChar(values, base + scale, 'c');
System.out.println("s:"+s+" s1:"+s1);
//將s的值改為 abc
s = "abc";
String s2 = "abc";
String s3 = "abc";
System.out.println("s:"+s+" s1:"+s1);
打印結(jié)果:
s:acc s1:acc
s:acc s1:acc s2:acc s3:acc
我們發(fā)現(xiàn)了什么?所有值為“abc”的字符串都變成了“acc”?。?!
Unsafe類果然不安全?。?!