Unsafe類強大功能的原理詳解
點擊上方 Java學習之道,選擇 設(shè)為星標
來源: blog.csdn.net/weixin_43767015/article/details/104643890
作者: 劉_Java
Part1概述
本文基于JDK1.8。
Unsafe類位于rt.jar包,Unsafe類提供了硬件級別的原子操作,類中的方法都是native方法,它們使用JNI的方式訪問本地C++實現(xiàn)庫。由此提供了一些繞開JVM的更底層功能,可以提高程序效率。
JNI:Java Native Interface。使得Java 與 本地其他類型語言(如C、C++)直接交互。
Unsafe 是用于擴展 Java 語言表達能力、便于在更高層(Java 層)代碼里實現(xiàn)原本要在更低層(C 層)實現(xiàn)的核心庫功能用的。這些功能包括直接內(nèi)存的申請/釋放/訪問,低層硬件的 atomic/volatile 支持,創(chuàng)建未初始化對象,通過偏移量操作對象字段、方法、實現(xiàn)線程無鎖掛起和恢復等功能。
所謂Java對象的“布局”就是在內(nèi)存里Java對象的各個部分放在哪里,包括對象的實例字段和一些元數(shù)據(jù)之類。Unsafe里關(guān)于對象字段訪問的方法把對象布局抽象出來,它提供了objectFieldOffset()方法用于獲取某個字段相對Java對象的“起始地址”的偏移量,也提供了getInt、getLong、getObject之類的方法可以使用前面獲取的偏移量來訪問某個Java對象的某個字段。
Unsafe作用可以大致歸納為:
內(nèi)存管理,包括分配內(nèi)存、釋放內(nèi)存等。 非常規(guī)的對象實例化。 操作類、對象、變量。 自定義超大數(shù)組操作。 多線程同步。包括鎖機制、CAS操作等。 線程掛起與恢復。 內(nèi)存屏障。
Part2API詳解
Unsafe中一共有82個public native修飾的方法,還有幾十個基于這82個public native方法的其他方法,一共有114個方法。
2.1 初始化方法
我們可以直接在源碼里面看到,Unsafe是單例模式的類:
private static final Unsafe theUnsafe;
//構(gòu)造器私有
private Unsafe() {
}
//靜態(tài)塊初始化
static {
Reflection.registerMethodsToFilter(Unsafe.class, new String[]{"getUnsafe"});
theUnsafe = new Unsafe();
}
//靜態(tài)方法獲取實例
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
從上面的代碼知道,好像是可以通過getUnsafe()方法獲取實例,但是如果我們調(diào)用該方法會得到一個異常:
java.lang.SecurityException: Unsafe
at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
//……………………
實際上我們可以看到getUnsafe()方法上有個 @CallerSensitive 注解,就是因為這個注解,在執(zhí)行時候需要做權(quán)限判斷:只有由主類加載器(BootStrap classLoader)加載的類才能調(diào)用這個類中的方法(比如rt.jar中的類,就可以調(diào)用該方法,原因從類名可以看出來,它是“不安全的”,怎能隨意調(diào)用,至于有哪些隱患后面會講)。顯然我們的類是由AppClassLoader加載的,所以這里直接拋出了異常。
因此最簡單的使用方式是基于反射獲取Unsafe實例,代碼如下:
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
2.2 類、對象和變量相關(guān)方法
主要包括基于偏移地址獲取或者設(shè)置變量的值、基于偏移地址獲取或者設(shè)置數(shù)組元素的值、class初始化以及對象非常規(guī)的創(chuàng)建等。
2.2.1 對象操作
/*對象操作*/
/*獲取對象字段的值*/
//通過給定的Java變量獲取引用值。這里實際上是獲取一個Java對象o中,獲取偏移地址為offset的屬性的值,此方法可以突破修飾符的抑制,也就是無視private、protected和default修飾符。
// 類似的方法有g(shù)etInt、getDouble等等。
public native Object getObject(Object o, long offset);
//此方法和上面的getObject功能類似,不過附加了'volatile'加載語義,也就是強制從主存中獲取屬性值。類似的方法有g(shù)etIntVolatile、getDoubleVolatile等等。
// 這個方法要求被使用的屬性被volatile修飾,否則功能和getObject方法相同。
public native Object getObjectVolatile(Object o, long offset);
/*修改對象字段的值*/
//設(shè)置Java對象o中偏移地址為offset的屬性的值為x,此方法可以突破修飾符的抑制,也就是無視private、protected和default修飾符。用于修改修改非基本數(shù)據(jù)類型的值。
//類似的方法有putInt、putDouble等等,用于修改基本數(shù)據(jù)類型的值,再次不再贅述。
public native void putObject(Object o, long offset, Object x);
//此方法和上面的putObject功能類似,不過附加了'volatile'加載語義,也就是設(shè)置值的時候強制(JMM會保證獲得鎖到釋放鎖之間所有對象的狀態(tài)更新都會在鎖被釋放之后)更新到主存,從而保證這些變更對其他線程是可見的。
// 類似的方法有putIntVolatile、putDoubleVolatile等等。這個方法要求被使用的屬性被volatile修飾,否則功能和putObject方法相同。
public native void putObjectVolatile(Object o, long offset, Object x);
//設(shè)置o對象中offset偏移地址offset對應(yīng)的Object型field的值為指定值x。這是一個有序或者有延遲的putObjectVolatile方法,并且不保證值的改變被其他線程立即看到。
// 只有在field被volatile修飾并且期望被修改的時候使用才會生效。類似的方法有putOrderedInt和putOrderedLong。
// 最終會設(shè)置成x,但是可能導致其他線程在之后的一小段時間內(nèi)還是可以讀到舊的值。關(guān)于該方法的更多信息可以參考并發(fā)編程網(wǎng)翻譯的一篇文章《AtomicLong.lazySet是如何工作的?》,文章地址是“http://ifeve.com/how-does-atomiclong-lazyset-work/”。
public native void putOrderedObject(Object o, long offset, Object x);
/*獲取對象的字段相對該對象地址的偏移量*/
//返回給定的靜態(tài)屬性在它的類的存儲分配中的位置(偏移地址)。即相對于 className.class 的偏移量,通過這個偏移量可以快速定位字段.
// 注意:這個方法僅僅針對靜態(tài)屬性,使用在非靜態(tài)屬性上會拋異常。
public native long staticFieldOffset(Field f);
//返回給定的非靜態(tài)屬性在它的類的存儲分配中的位置(偏移地址)。即字段到對象頭的偏移量,通過這個偏移量可以快速定位字段.
// 注意:這個方法僅僅針對非靜態(tài)屬性,使用在靜態(tài)屬性上會拋異常。
public native long objectFieldOffset(Field f);
//返回給定的靜態(tài)屬性的位置,配合staticFieldOffset方法使用。實際上,這個方法返回值就是靜態(tài)屬性所在的Class對象的一個內(nèi)存快照
// 注釋中說到,此方法返回的Object有可能為null,它只是一個'cookie'而不是真實的對象,不要直接使用的它的實例中的獲取屬性和設(shè)置屬性的方法,它的作用只是方便調(diào)用上面提到的像getInt(Object,long)等等的任意方法。
public native Object staticFieldBase(Field f);
/*創(chuàng)建對象*/
//繞過構(gòu)造方法、初始化代碼來非常規(guī)的創(chuàng)建對象
public native Object allocateInstance(Class<?> cls) throws InstantiationException;
2.2.2 class 相關(guān)
//檢測給定的類是否需要初始化。通常需要使用在獲取一個類的靜態(tài)屬性的時候(因為一個類如果沒初始化,它的靜態(tài)屬性也不會初始化)。
//此方法當且僅當ensureClassInitialized方法不生效的時候才返回false。
public native boolean shouldBeInitialized(Class<?> c);
//檢測給定的類是否已經(jīng)初始化。通常需要使用在獲取一個類的靜態(tài)屬性的時候(因為一個類如果沒初始化,它的靜態(tài)屬性也不會初始化)。
public native void ensureClassInitialized(Class<?> c);
//定義一個類,返回類實例,此方法會跳過JVM的所有安全檢查。默認情況下,ClassLoader(類加載器)和ProtectionDomain(保護域)實例應(yīng)該來源于調(diào)用者。
public native Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);
///定義一個匿名類,與Java8的lambda表達式相關(guān),會用到該方法實現(xiàn)相應(yīng)的函數(shù)式接口的匿名類,可以看結(jié)尾文章鏈接。
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);
2.2.3 數(shù)組元素相關(guān)
//返回數(shù)組類型的第一個元素的偏移地址(基礎(chǔ)偏移地址)。如果arrayIndexScale方法返回的比例因子不為0,你可以通過結(jié)合基礎(chǔ)偏移地址和比例因子訪問數(shù)組的所有元素。
// Unsafe中已經(jīng)初始化了很多類似的常量如ARRAY_BOOLEAN_BASE_OFFSET等。
public native int arrayBaseOffset(Class<?> arrayClass);
//返回數(shù)組單個元素的大小,數(shù)組中的元素的地址是連續(xù)的。
// Unsafe中已經(jīng)初始化了很多類似的常量如ARRAY_BOOLEAN_INDEX_SCALE等。
public native int arrayIndexScale(Class<?> arrayClass);
2.3 內(nèi)存管理
該部分包括了allocateMemory(分配內(nèi)存)、reallocateMemory(重新分配內(nèi)存)、copyMemory(拷貝內(nèi)存)、freeMemory(釋放內(nèi)存 )、getAddress(獲取內(nèi)存地址)、addressSize、pageSize、getInt(獲取內(nèi)存地址指向的整數(shù))、getIntVolatile(獲取內(nèi)存地址指向的整數(shù),并支持volatile語義)、putInt(將整數(shù)寫入指定內(nèi)存地址)、putIntVolatile(將整數(shù)寫入指定內(nèi)存地址,并支持volatile語義)、putOrderedInt(將整數(shù)寫入指定內(nèi)存地址、有序或者有延遲的方法)等方法。getXXX和putXXX包含了各種基本類型的操作。
利用copyMemory方法,我們可以實現(xiàn)一個通用的對象拷貝方法,無需再對每一個對象都實現(xiàn)clone方法,當然這通用的方法只能做到對象淺拷貝。
Unsafe分配的內(nèi)存,不受Integer.MAX_VALUE的限制,并且分配在非堆內(nèi)存,使用它時,需要非常謹慎:忘記手動回收時,會產(chǎn)生內(nèi)存泄露,可以通過Unsafe#freeMemory方法手動回收;非法的地址訪問時,會導致JVM崩潰。在需要分配大的連續(xù)區(qū)域、實時編程(不能容忍JVM延遲)時,可以使用它,因為直接內(nèi)存的效率會更好,詳細介紹可以去看看Java的NIO源碼,NIO中使用了這一技術(shù)。
JDK nio包中通過ByteBuffer#allocateDirect方法分配直接內(nèi)存時,DirectByteBuffer的構(gòu)造函數(shù)中就使用到了Unsafe的allocateMemory和setMemory方法:通過Unsafe.allocateMemory分配內(nèi)存、Unsafe.setMemory進行內(nèi)存初始化,而后構(gòu)建一個虛引用Cleaner對象用于跟蹤DirectByteBuffer對象的垃圾回收,以實現(xiàn)當DirectByteBuffer被垃圾回收時,分配的堆外內(nèi)存一起被釋放(通過在Cleaner中調(diào)用Unsafe#freeMemory方法)。
//獲取本地指針的大小(單位是byte),通常值為4(32位系統(tǒng))或者8(64位系統(tǒng))。常量ADDRESS_SIZE就是調(diào)用此方法。
public native int addressSize();
//獲取本地內(nèi)存的頁數(shù),此值為2的冪次方。
//java.nio下的工具類Bits中計算待申請內(nèi)存所需內(nèi)存頁數(shù)量的靜態(tài)方法,其依賴于Unsafe中pageSize方法獲取系統(tǒng)內(nèi)存頁大小實現(xiàn)后續(xù)計算邏輯
public native int pageSize();
/*分配一塊新的本地內(nèi)存,
通過bytes指定內(nèi)存塊的大小(單位是byte),返回新開辟的內(nèi)存的地址。
可以通過freeMemory方法釋放內(nèi)存塊,
或者通過reallocateMemory方法調(diào)整內(nèi)存塊大小。*/
/*bytes值為負數(shù)或者過大會拋出IllegalArgumentException異常,
如果系統(tǒng)拒絕分配內(nèi)存會拋出OutOfMemoryError異常。*/
public native long allocateMemory(long bytes);
//通過指定的內(nèi)存地址address重新調(diào)整本地內(nèi)存塊的大小,調(diào)整后的內(nèi)存塊大小通過bytes指定(單位為byte)。可以通過freeMemory方法釋放內(nèi)存塊,或者通過reallocateMemory方法調(diào)整內(nèi)存塊大小。
//bytes值為負數(shù)或者過大會拋出IllegalArgumentException異常,如果系統(tǒng)拒絕分配內(nèi)存會拋出OutOfMemoryError異常。
public native long reallocateMemory(long address, long bytes);
//在給定的內(nèi)存塊中設(shè)置值。內(nèi)存塊的地址由對象引用o和偏移地址共同決定,如果對象引用o為null,offset就是絕對地址。第三個參數(shù)就是內(nèi)存塊的大小,如果使用allocateMemory進行內(nèi)存開辟的話,這里的值應(yīng)該和allocateMemory的參數(shù)一致。value就是設(shè)置的固定值,一般為0(這里可以參考netty的DirectByteBuffer)。
//一般而言,o為null,所以有個重載方法是public native void setMemory(long offset, long bytes, byte value);,等效于setMemory(null, long offset, long bytes, byte value);。
public native void setMemory(Object o, long offset, long bytes, byte value);
//釋放內(nèi)存
public native void freeMemory(long address);
2.4 多線程同步
主要包括監(jiān)視器鎖定、解鎖以及CAS相關(guān)的方法。這部分包括了monitorEnter、tryMonitorEnter、monitorExit、compareAndSwapInt、compareAndSwap等方法。其中monitorEnter、tryMonitorEnter、monitorExit已經(jīng)被標記為deprecated,不建議使用。
Unsafe類的CAS操作可能是用的最多的,它為Java的鎖機制提供了一種新的解決辦法,比如AtomicInteger等類都是通過該方法來實現(xiàn)的。這是一種樂觀鎖,通常認為在大部分情況下不出現(xiàn)競態(tài)條件,如果操作失敗,會不斷重試直到成功。
//鎖定對象,必須通過monitorExit方法才能解鎖。此方法經(jīng)過實驗是可以重入的,也就是可以多次調(diào)用,然后通過多次調(diào)用monitorExit進行解鎖。
@Deprecated
public native void monitorEnter(Object o);
//解鎖對象,前提是對象必須已經(jīng)調(diào)用monitorEnter進行加鎖,否則拋出IllegalMonitorStateException異常。
@Deprecated
public native void monitorExit(Object o);
//嘗試鎖定對象,如果加鎖成功返回true,否則返回false。必須通過monitorExit方法才能解鎖。
@Deprecated
public native boolean tryMonitorEnter(Object o);
//針對Object對象進行CAS操作。即是對應(yīng)Java變量引用o,原子性地更新o中偏移地址為offset的屬性的值為x,當且僅的偏移地址為offset的屬性的當前值為expected才會更新成功返回true,否則返回false。
//o:目標Java變量引用。offset:目標Java變量中的目標屬性的偏移地址。expected:目標Java變量中的目標屬性的期望的當前值。x:目標Java變量中的目標屬性的目標更新值。
//類似的方法有compareAndSwapInt和compareAndSwapLong,在Jdk8中基于CAS擴展出來的方法有g(shù)etAndAddInt、getAndAddLong、getAndSetInt、getAndSetLong、getAndSetObject,它們的作用都是:通過CAS設(shè)置新的值,返回舊的值。
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
//獲取對象obj 中偏移量為offset 的變量volatile語義的當前值,并設(shè)置變量volatile 語義的值為update
long getAndSetLong(Object obj, long offset, long update)
//獲取對象obj同中偏移量為offset 的變量volatile語義的當前值,并設(shè)置變量值為原始值+addValue
long getAndAddLong(Object obj, long offset, long addValue)
2.5 線程的掛起和恢復
這部分包括了park、unpark等方法。
將一個線程進行掛起是通過park方法實現(xiàn)的,調(diào)用 park后,線程將一直阻塞直到超時或者中斷等條件出現(xiàn)。unpark可以終止一個掛起的線程,使其恢復正常。整個并發(fā)框架中對線程的掛起操作被封裝在 LockSupport類中,LockSupport類中有各種版本pack方法,但最終都調(diào)用了Unsafe.park()方法。
Java8的新鎖StampedLock使用該系列方法。
//釋放被park阻塞的線程,也可以被使用來終止一個先前調(diào)用park導致的阻塞,即這兩個方法的調(diào)用順序可以是先unpark再park。
public native void unpark(Object thread);
//阻塞當前線程直到一個unpark方法出現(xiàn)(被調(diào)用)、一個用于unpark方法已經(jīng)出現(xiàn)過(在此park方法調(diào)用之前已經(jīng)調(diào)用過)、線程被中斷或者time時間到期(也就是阻塞超時)。
// 在time非零的情況下,如果isAbsolute為true,time是相對于新紀元之后的毫秒,否則time表示納秒。
public native void park(boolean isAbsolute, long time);
2.6 內(nèi)存屏障
這部分包括了loadFence、storeFence、fullFence等方法。這是在Java 8新引入的,用于定義內(nèi)存屏障,避免代碼重排序。如果你了解JVM的volatile、鎖的內(nèi)存寓意,那么理解“內(nèi)存屏障”這幾個字應(yīng)該不會太難,這里只是把它包裝成了Java代碼。
loadFence() 表示該方法之前的所有l(wèi)oad操作在內(nèi)存屏障之前完成。同理storeFence()表示該方法之前的所有store操作在內(nèi)存屏障之前完成。fullFence()表示該方法之前的所有l(wèi)oad、store操作在內(nèi)存屏障之前完成。
//在該方法之前的所有讀操作,一定在load屏障之前執(zhí)行完成。
public native void loadFence();
//在該方法之前的所有寫操作,一定在store屏障之前執(zhí)行完成
public native void storeFence();
//在該方法之前的所有讀寫操作,一定在full屏障之前執(zhí)行完成,這個內(nèi)存屏障相當于上面兩個(load屏障和store屏障)的合體功能。
public native void fullFence();
2.7 其他
//獲取系統(tǒng)的平均負載值,loadavg這個double數(shù)組將會存放負載值的結(jié)果,nelems決定樣本數(shù)量,nelems只能取值為1到3,分別代表最近1、5、15分鐘內(nèi)系統(tǒng)的平均負載。
//如果無法獲取系統(tǒng)的負載,此方法返回-1,否則返回獲取到的樣本數(shù)量(loadavg中有效的元素個數(shù))。實驗中這個方法一直返回-1,其實完全可以使用JMX中的相關(guān)方法替代此方法。
public native int getLoadAverage(double[] loadavg, int nelems);
//繞過檢測機制直接拋出異常。這讓我們可以做些特別的事。
public native void throwException(Throwable ee);
Part3應(yīng)用
3.0 根據(jù)偏移量(指針)修改屬性值
public class TestUnSafe {
static final Unsafe UNSAFE;
//要更新的字段
private volatile long state;
//記錄字段的偏移量
private static final long stateOffset;
/**
* 靜態(tài)塊初始化unsafe,并且獲取state字段的偏移量
*/
static {
try {
//反射獲取unsafe
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
UNSAFE = (Unsafe) f.get(null);
//獲取偏移量
stateOffset = UNSAFE.objectFieldOffset(TestUnSafe.class.getDeclaredField("state"));
} catch (Exception ex) {
throw new Error(ex);
}
}
public TestUnSafe(long state) {
this.state = state;
}
public static void main(String[] args) {
TestUnSafe testUnSafe = new TestUnSafe(0);
//嘗試更改變量值
boolean b = UNSAFE.compareAndSwapLong(testUnSafe, stateOffset, testUnSafe.state, 2);
System.out.println(b);
System.out.println(testUnSafe.state);
}
}
3.1 對象的非常規(guī)實例化
我們通常所用到的創(chuàng)建對象的方式,有直接new創(chuàng)建、也有反射創(chuàng)建,其本質(zhì)都是調(diào)用相應(yīng)的構(gòu)造器,而使用有參構(gòu)造函數(shù)時,必須傳遞相應(yīng)個數(shù)的參數(shù)才能完成對象實例化。
而Unsafe中提供allocateInstance方法,僅通過Class對象就可以創(chuàng)建此類的實例對象,而且不需要調(diào)用其構(gòu)造函數(shù)、初始化代碼、JVM安全檢查等。并且它抑制修飾符檢測,也就是即使構(gòu)造器是private修飾的也能通過此方法實例化,只需提類對象即可創(chuàng)建相應(yīng)的對象。
由于這種特性,allocateInstance在java.lang.invoke、Objenesis(提供繞過類構(gòu)造器的對象生成方式)、Gson(反序列化時用到)中都有相應(yīng)的應(yīng)用。在Gson反序列化時,如果類有默認構(gòu)造函數(shù),則通過反射調(diào)用默認構(gòu)造函數(shù)創(chuàng)建實例,否則通過UnsafeAllocator來實現(xiàn)對象實例的構(gòu)造,UnsafeAllocator通過調(diào)用Unsafe的allocateInstance實現(xiàn)對象的實例化,保證在目標類無默認構(gòu)造函數(shù)時,反序列化不夠影響。
案例:
public class UnsafeTest {
private static Unsafe UNSAFE;
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
UNSAFE = (Unsafe) field.get(null);
} catch (Exception ignored) {
}
}
public static void main(String[] args) {
//reflect();
unsafe();
}
/**
* 反射測試,注釋掉無參構(gòu)造器,方法報錯;開放注釋,方法執(zhí)行成功,type字段有值。
*/
public static void reflect() {
/*如果沒有無參構(gòu)造器,該反射會拋出異常,其內(nèi)部還是使用的new關(guān)鍵字*/
try {
Class<?> aClass = Class.forName("com.thread.test.juc.unsafe.User");
Constructor<?> constructor = aClass.getDeclaredConstructor();
constructor.setAccessible(true);
User o = (User) constructor.newInstance(null);
System.out.println(o);
/*值為vip,正常*/
System.out.println(o.type);
System.out.println(o.age);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* UNSAFE測試,注釋掉無參構(gòu)造器,照樣成功構(gòu)造對象,但是type字段為null。這就是沒有走構(gòu)造器的后果之一:沒有對字段進行初始化
*/
public static void unsafe() {
try {
/*不需要相應(yīng)的構(gòu)造器即可創(chuàng)建對象*/
User user = (User) UNSAFE.allocateInstance(User.class);
user.setName("user1");
System.out.println("instance: " + user);
user.test();
/*通過unsafe設(shè)置屬性值*/
Field name = user.getClass().getDeclaredField("name");
UNSAFE.putObject(user, UNSAFE.objectFieldOffset(name), "user2");
user.test();
/*值為null,說明unsafe并沒有初始化字段。*/
System.out.println(user.type);
System.out.println(user.age);
} catch (Exception e) {
e.printStackTrace();
}
}
}
class User {
public String type = "VIP";
public int age = 20;
private String name;
public void setName(String name) {
this.name = name;
}
public void test() {
System.err.println("hello,world " + name);
}
/*private User() {
System.out.println("constructor");
}*/
private User(String name) {
this.name = name;
}
}
注意:UNSAFE測試時,其vip字段并沒有獲取到值。實際上一個new操作,編譯成指令后(javap -v xx.class)是3條:
* 第一條指令的意思是根據(jù)類型分配一塊內(nèi)存區(qū)域
第二條指令是把第一條指令返回的內(nèi)存地址壓入操作數(shù)棧頂 第三條指令是調(diào)用類的構(gòu)造函數(shù),對字段進行顯示初始化操作。
Unsafe.allocateInstance()方法只做了第一步和第二步,即分配內(nèi)存空間,返回內(nèi)存地址,沒有做第三步調(diào)用構(gòu)造函數(shù)。所以Unsafe.allocateInstance()方法創(chuàng)建的對象都是只有初始值,沒有默認值也沒有構(gòu)造函數(shù)設(shè)置的值,因為它完全沒有使用new機制,直接操作內(nèi)存創(chuàng)建了對象。
3.2 超長數(shù)組操作
前面講的arrayBaseOffset與arrayIndexScale配合起來使用,就可以定位數(shù)組中每個元素在內(nèi)存中的位置。putByte和getByte則可以獲取指定位置的byte數(shù)據(jù)。
常規(guī)Java的數(shù)組最大值為Integer.MAX_VALUE,但是使用Unsafe類的內(nèi)存分配方法可以實現(xiàn)超大數(shù)組。實際上這樣的數(shù)據(jù)就可以認為是C數(shù)組,因此需要注意在合適的時間釋放內(nèi)存。
下例創(chuàng)建分配一段連續(xù)的內(nèi)存(數(shù)組),它的容量是Java允許最大容量的兩倍(有可能造成JVM崩潰):
class SuperArray {
private final static int BYTE = 1;
private long size;
private long address;
private static Unsafe unsafe;
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
} catch (Exception e) {
}
}
public SuperArray(long size) {
this.size = size;
//得到分配內(nèi)的起始地址
address = unsafe.allocateMemory(size * BYTE);
}
public void set(long i, byte value) {
//設(shè)置值
unsafe.putByte(address + i * BYTE, value);
}
public int get(long idx) {
//獲取值
return unsafe.getByte(address + idx * BYTE);
}
public long size() {
return size;
}
public static void main(String[] args) {
//兩倍Integer.MAX_VALUE長度
long SUPER_SIZE = (long) Integer.MAX_VALUE * 2;
SuperArray array = new SuperArray(SUPER_SIZE);
System.out.println("Array size:" + array.size()); // 4294967294
int sum = 0;
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
}
}
3.3 包裝受檢異常為運行時異常
unsafe.throwException(new IOException());
3.4 運行時動態(tài)創(chuàng)建類
標準的動態(tài)加載類的方法是Class.forName()(在編寫jdbc程序時,記憶深刻),使用Unsafe也可以動態(tài)加載java 的class文件。操作方式就是將.class文件讀取到字節(jié)數(shù)據(jù)組中,并將其傳到defineClass方法中。
public class CreateClass {
private static Unsafe unsafe;
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
} catch (Exception e) {
e.printStackTrace();
}
}
//Method to read .class file
private static byte[] getClassContent() throws Exception {
File f = new File("target/classes/com/thread/test/juc/unsafe/A.class");
FileInputStream input = new FileInputStream(f);
byte[] content = new byte[(int) f.length()];
input.read(content);
input.close();
return content;
}
public static void main(String[] args) throws Exception {
//Sample code to creat classes
byte[] classContents = getClassContent();
Class c = unsafe.defineClass(null, classContents, 0, classContents.length, CreateClass.class.getClassLoader(), null);
c.getMethod("a").invoke(c.newInstance()); //aaaa
}
}
class A {
public void a() {
System.out.println("aaaa");
}
}
3.5 實現(xiàn)淺克隆
使用直接獲取內(nèi)存的方式實現(xiàn)淺克隆。把一個對象的字節(jié)碼拷貝到內(nèi)存的另外一個地方,然后再將這個對象轉(zhuǎn)換為被克隆的對象類型。為了表述方便,用S代表要克隆的對象,D表示克隆后的對象,SD表示S的內(nèi)存地址,DD表示D的內(nèi)存地址,SIZE表示該對象在內(nèi)存中的大小。
獲取原對象的所在的內(nèi)存地址SD。 計算原對象在內(nèi)存中的大小SIZE。 新分配一塊內(nèi)存,大小為原對象大小SIZE,記錄新分配內(nèi)存的地址DD。 從原對象內(nèi)存地址SD處復制大小為SIZE的內(nèi)存,復制到DD處。 DD處的SIZE大小的內(nèi)存就是原對象的淺克隆對象,強制轉(zhuǎn)換為源對象類型就可以了。
Part4總結(jié)和注意
從上面的介紹中,我們可以看到Unsafe非常強大和有趣的功能,但是實際上官方是不推薦我們在代碼中直接使用Unsafe類的。甚至從命名就能看出來"Unsafe"——那肯定就是不安全的意思啦。那么什么不安全呢?我們知道C或C++是可以直接操作指針的,指針操作是非常不安全的,這也是Java“去除”指針的原因。
回到Unsafe類,類中包含大量操作指針偏移量的方法,偏移量要自己計算,如若使用不當,會對程序帶來許多不可控的災難,JVM直接崩潰虧。因此對它的使用我們需要慎之又慎,生產(chǎn)級別的代碼就更不應(yīng)該使用Unsafe類了。
另外Unsafe類還有很多自主操作內(nèi)存的方法,這些都是直接內(nèi)存,而使用的這些內(nèi)存不受JVM管理(無法被GC),需要手動管理,一旦出現(xiàn)疏忽很有可能成為內(nèi)存泄漏的源頭。
盡管Unsafe是“不安全的”,但是它的“應(yīng)用”卻很廣泛。Unsafe在JUC(java.util.concurrent)包中大量使用(主要是CAS),在netty中方便使用直接內(nèi)存,還有一些高并發(fā)的交易系統(tǒng)為了提高CAS的效率也有可能直接使用到Unsafe,比如Hadoop、Kafka、akka。
總而言之,Unsafe類是一把雙刃劍。或許這里的“不安全”只是針對像我們這些“菜鳥”而提出的警告吧!::>_<::
-
| 更多精彩文章 -
▽加我微信,交個朋友 長按/掃碼添加↑↑↑



