JAVA中神奇的双刃剑--Unsafe
?
參考資料:
?
前些天發(fā)現(xiàn)了一個(gè)巨牛的人工智能學(xué)習(xí)網(wǎng)站,通俗易懂,風(fēng)趣幽默,忍不住分享一下給大家。點(diǎn)擊跳轉(zhuǎn)到教程。
?
- Java魔法類:sun.misc.Unsafe
- 在openjdk8下看Unsafe源碼
Unsafe介紹
在Oracle的Jdk8無法獲取到sun.misc包的源碼,想看此包的源碼可以直接下載openjdk,包的路徑是:
- openjdk-8u40-src-b25-10_feb_2015\openjdk\jdk\src\share\classes\sun\misc。
當(dāng)然,不同的openjdk版本的根目錄(這里是openjdk-8u40-src-b25-10_feb_2015)不一定相同。sun.misc包含了低級(native硬件級別的原子操作)、不安全的操作集合。
Java無法直接訪問到操作系統(tǒng)底層(如系統(tǒng)硬件等),為此Java使用native方法來擴(kuò)展Java程序的功能。Unsafe類提供了硬件級別的原子操作,提供了一些繞開JVM的更底層功能,由此提高效率。本文的Unsafe類來源于openjdk-8u40-src-b25-10_feb_2015。
Unsafe的使用建議#
建議先看這個(gè)知乎帖子第一樓R大的回答:為什么JUC中大量使用了sun.misc.Unsafe 這個(gè)類,但官方卻不建議開發(fā)者使用。
使用Unsafe要注意以下幾個(gè)問題:
- 1、Unsafe有可能在未來的Jdk版本移除或者不允許Java應(yīng)用代碼使用,這一點(diǎn)可能導(dǎo)致使用了Unsafe的應(yīng)用無法運(yùn)行在高版本的Jdk。
- 2、Unsafe的不少方法中必須提供原始地址(內(nèi)存地址)和被替換對象的地址,偏移量要自己計(jì)算,一旦出現(xiàn)問題就是JVM崩潰級別的異常,會導(dǎo)致整個(gè)JVM實(shí)例崩潰,表現(xiàn)為應(yīng)用程序直接crash掉。
- 3、Unsafe提供的直接內(nèi)存訪問的方法中使用的內(nèi)存不受JVM管理(無法被GC),需要手動(dòng)管理,一旦出現(xiàn)疏忽很有可能成為內(nèi)存泄漏的源頭。
暫時(shí)總結(jié)出以上三點(diǎn)問題。Unsafe在JUC(java.util.concurrent)包中大量使用(主要是CAS),在netty中方便使用直接內(nèi)存,還有一些高并發(fā)的交易系統(tǒng)為了提高CAS的效率也有可能直接使用到Unsafe。總而言之,Unsafe類是一把雙刃劍。
Unsafe詳解
Unsafe中一共有82個(gè)public native修飾的方法,還有幾十個(gè)基于這82個(gè)public native方法的其他方法。
1 //擴(kuò)充內(nèi)存 2 public native long reallocateMemory(long address, long bytes); 3 4 //分配內(nèi)存 5 public native long allocateMemory(long bytes); 6 7 //釋放內(nèi)存 8 public native void freeMemory(long address); 9 10 //在給定的內(nèi)存塊中設(shè)置值 11 public native void setMemory(Object o, long offset, long bytes, byte value); 12 13 //從一個(gè)內(nèi)存塊拷貝到另一個(gè)內(nèi)存塊 14 public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes); 15 16 //獲取值,不管java的訪問限制,其他有類似的getInt,getDouble,getLong,getChar等等 17 public native Object getObject(Object o, long offset); 18 19 //設(shè)置值,不管java的訪問限制,其他有類似的putInt,putDouble,putLong,putChar等等 20 public native void putObject(Object o, long offset); 21 22 //從一個(gè)給定的內(nèi)存地址獲取本地指針,如果不是allocateMemory方法的,結(jié)果將不確定 23 public native long getAddress(long address); 24 25 //存儲一個(gè)本地指針到一個(gè)給定的內(nèi)存地址,如果地址不是allocateMemory方法的,結(jié)果將不確定 26 public native void putAddress(long address, long x); 27 28 //該方法返回給定field的內(nèi)存地址偏移量,這個(gè)值對于給定的filed是唯一的且是固定不變的 29 public native long staticFieldOffset(Field f); 30 31 //報(bào)告一個(gè)給定的字段的位置,不管這個(gè)字段是private,public還是保護(hù)類型,和staticFieldBase結(jié)合使用 32 public native long objectFieldOffset(Field f); 33 34 //獲取一個(gè)給定字段的位置 35 public native Object staticFieldBase(Field f); 36 37 //確保給定class被初始化,這往往需要結(jié)合基類的靜態(tài)域(field) 38 public native void ensureClassInitialized(Class c); 39 40 //可以獲取數(shù)組第一個(gè)元素的偏移地址 41 public native int arrayBaseOffset(Class arrayClass); 42 43 //可以獲取數(shù)組的轉(zhuǎn)換因子,也就是數(shù)組中元素的增量地址。將arrayBaseOffset與arrayIndexScale配合使用, 可以定位數(shù)組中每個(gè)元素在內(nèi)存中的位置 44 public native int arrayIndexScale(Class arrayClass); 45 46 //獲取本機(jī)內(nèi)存的頁數(shù),這個(gè)值永遠(yuǎn)都是2的冪次方 47 public native int pageSize(); 48 49 //告訴虛擬機(jī)定義了一個(gè)沒有安全檢查的類,默認(rèn)情況下這個(gè)類加載器和保護(hù)域來著調(diào)用者類 50 public native Class defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain); 51 52 //定義一個(gè)類,但是不讓它知道類加載器和系統(tǒng)字典 53 public native Class defineAnonymousClass(Class hostClass, byte[] data, Object[] cpPatches); 54 55 //鎖定對象,必須是沒有被鎖的 56 public native void monitorEnter(Object o); 57 58 //解鎖對象 59 public native void monitorExit(Object o); 60 61 //試圖鎖定對象,返回true或false是否鎖定成功,如果鎖定,必須用monitorExit解鎖 62 public native boolean tryMonitorEnter(Object o); 63 64 //引發(fā)異常,沒有通知 65 public native void throwException(Throwable ee); 66 67 //CAS,如果對象偏移量上的值=期待值,更新為x,返回true.否則false.類似的有compareAndSwapInt,compareAndSwapLong,compareAndSwapBoolean,compareAndSwapChar等等。 68 public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x); 69 70 // 該方法獲取對象中offset偏移地址對應(yīng)的整型field的值,支持volatile load語義。類似的方法有g(shù)etIntVolatile,getBooleanVolatile等等 71 public native Object getObjectVolatile(Object o, long offset); 72 73 //線程調(diào)用該方法,線程將一直阻塞直到超時(shí),或者是中斷條件出現(xiàn)。 74 public native void park(boolean isAbsolute, long time); 75 76 //終止掛起的線程,恢復(fù)正常.java.util.concurrent包中掛起操作都是在LockSupport類實(shí)現(xiàn)的,也正是使用這兩個(gè)方法 77 public native void unpark(Object thread); 78 79 //獲取系統(tǒng)在不同時(shí)間系統(tǒng)的負(fù)載情況 80 public native int getLoadAverage(double[] loadavg, int nelems); 81 82 //創(chuàng)建一個(gè)類的實(shí)例,不需要調(diào)用它的構(gòu)造函數(shù)、初使化代碼、各種JVM安全檢查以及其它的一些底層的東西。即使構(gòu)造函數(shù)是私有,我們也可以通過這個(gè)方法創(chuàng)建它的實(shí)例,對于單例模式,簡直是噩夢,哈哈 83 public native Object allocateInstance(Class cls) throws InstantiationException;類、對象和變量相關(guān)方法#
主要包括類的非常規(guī)實(shí)例化、基于偏移地址獲取或者設(shè)置變量的值、基于偏移地址獲取或者設(shè)置數(shù)組元素的值等。
getObject#
- public native Object getObject(Object o, long offset);
通過給定的Java變量獲取引用值。這里實(shí)際上是獲取一個(gè)Java對象o中,獲取偏移地址為offset的屬性的值,此方法可以突破修飾符的抑制,也就是無視private、protected和default修飾符。類似的方法有g(shù)etInt、getDouble等等。
putObject#
- public native void putObject(Object o, long offset, Object x);
將引用值存儲到給定的Java變量中。這里實(shí)際上是設(shè)置一個(gè)Java對象o中偏移地址為offset的屬性的值為x,此方法可以突破修飾符的抑制,也就是無視private、protected和default修飾符。類似的方法有putInt、putDouble等等。
getObjectVolatile#
- public native Object getObjectVolatile(Object o, long offset);
此方法和上面的getObject功能類似,不過附加了'volatile'加載語義,也就是強(qiáng)制從主存中獲取屬性值。類似的方法有g(shù)etIntVolatile、getDoubleVolatile等等。這個(gè)方法要求被使用的屬性被volatile修飾,否則功能和getObject方法相同。
putObjectVolatile#
- public native void putObjectVolatile(Object o, long offset, Object x);
此方法和上面的putObject功能類似,不過附加了'volatile'加載語義,也就是設(shè)置值的時(shí)候強(qiáng)制(JMM會保證獲得鎖到釋放鎖之間所有對象的狀態(tài)更新都會在鎖被釋放之后)更新到主存,從而保證這些變更對其他線程是可見的。類似的方法有putIntVolatile、putDoubleVolatile等等。這個(gè)方法要求被使用的屬性被volatile修飾,否則功能和putObject方法相同。
putOrderedObject#
- public native void putOrderedObject(Object o, long offset, Object x);
設(shè)置o對象中offset偏移地址offset對應(yīng)的Object型field的值為指定值x。這是一個(gè)有序或者有延遲的putObjectVolatile方法,并且不保證值的改變被其他線程立即看到。只有在field被volatile修飾并且期望被修改的時(shí)候使用才會生效。類似的方法有putOrderedInt和putOrderedLong。
staticFieldOffset#
- public native long staticFieldOffset(Field f);
返回給定的靜態(tài)屬性在它的類的存儲分配中的位置(偏移地址)。不要在這個(gè)偏移量上執(zhí)行任何類型的算術(shù)運(yùn)算,它只是一個(gè)被傳遞給不安全的堆內(nèi)存訪問器的cookie。注意:這個(gè)方法僅僅針對靜態(tài)屬性,使用在非靜態(tài)屬性上會拋異常。下面源碼中的方法注釋估計(jì)有誤,staticFieldOffset和objectFieldOffset的注釋估計(jì)是對調(diào)了,為什么會出現(xiàn)這個(gè)問題無法考究。
objectFieldOffset#
- public native long objectFieldOffset(Field f);
返回給定的非靜態(tài)屬性在它的類的存儲分配中的位置(偏移地址)。不要在這個(gè)偏移量上執(zhí)行任何類型的算術(shù)運(yùn)算,它只是一個(gè)被傳遞給不安全的堆內(nèi)存訪問器的cookie。注意:這個(gè)方法僅僅針對非靜態(tài)屬性,使用在靜態(tài)屬性上會拋異常。
staticFieldBase#
- public native Object staticFieldBase(Field f);
返回給定的靜態(tài)屬性的位置,配合staticFieldOffset方法使用。實(shí)際上,這個(gè)方法返回值就是靜態(tài)屬性所在的Class對象的一個(gè)內(nèi)存快照。注釋中說到,此方法返回的Object有可能為null,它只是一個(gè)'cookie'而不是真實(shí)的對象,不要直接使用的它的實(shí)例中的獲取屬性和設(shè)置屬性的方法,它的作用只是方便調(diào)用上面提到的像getInt(Object,long)等等的任意方法。
shouldBeInitialized#
- public native boolean shouldBeInitialized(Class<?> c);
檢測給定的類是否需要初始化。通常需要使用在獲取一個(gè)類的靜態(tài)屬性的時(shí)候(因?yàn)橐粋€(gè)類如果沒初始化,它的靜態(tài)屬性也不會初始化)。 此方法當(dāng)且僅當(dāng)ensureClassInitialized方法不生效的時(shí)候才返回false。
ensureClassInitialized#
- public native void ensureClassInitialized(Class<?> c);
檢測給定的類是否已經(jīng)初始化。通常需要使用在獲取一個(gè)類的靜態(tài)屬性的時(shí)候(因?yàn)橐粋€(gè)類如果沒初始化,它的靜態(tài)屬性也不會初始化)。
arrayBaseOffset#
- public native int arrayBaseOffset(Class<?> arrayClass);
返回?cái)?shù)組類型的第一個(gè)元素的偏移地址(基礎(chǔ)偏移地址)。如果arrayIndexScale方法返回的比例因子不為0,你可以通過結(jié)合基礎(chǔ)偏移地址和比例因子訪問數(shù)組的所有元素。Unsafe中已經(jīng)初始化了很多類似的常量如ARRAY_BOOLEAN_BASE_OFFSET等。
arrayIndexScale#
- public native int arrayIndexScale(Class<?> arrayClass);
返回?cái)?shù)組類型的比例因子(其實(shí)就是數(shù)據(jù)中元素偏移地址的增量,因?yàn)閿?shù)組中的元素的地址是連續(xù)的)。此方法不適用于數(shù)組類型為"narrow"類型的數(shù)組,"narrow"類型的數(shù)組類型使用此方法會返回0(這里narrow應(yīng)該是狹義的意思,但是具體指哪些類型暫時(shí)不明確,筆者查了很多資料也沒找到結(jié)果)。Unsafe中已經(jīng)初始化了很多類似的常量如ARRAY_BOOLEAN_INDEX_SCALE等。
defineClass#
- public native Class<?> defineClass(String name, byte[] b, int off, int len,ClassLoader loader,ProtectionDomain protectionDomain);
告訴JVM定義一個(gè)類,返回類實(shí)例,此方法會跳過JVM的所有安全檢查。默認(rèn)情況下,ClassLoader(類加載器)和ProtectionDomain(保護(hù)域)實(shí)例應(yīng)該來源于調(diào)用者。
defineAnonymousClass#
- public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);
這個(gè)方法的使用可以看R大的知乎回答:JVM crashes at libjvm.so,下面截取一點(diǎn)內(nèi)容解釋此方法。
- 1、VM Anonymous Class可以看作一種模板機(jī)制,如果程序要?jiǎng)討B(tài)生成很多結(jié)構(gòu)相同、只是若干變量不同的類的話,可以先創(chuàng)建出一個(gè)包含占位符常量的正常類作為模板,然后利用sun.misc.Unsafe#defineAnonymousClass()方法,傳入該類(host class,宿主類或者模板類)以及一個(gè)作為"constant pool path"的數(shù)組來替換指定的常量為任意值,結(jié)果得到的就是一個(gè)替換了常量的VM Anonymous Class。
- 2、VM Anonymous Class從VM的角度看是真正的"沒有名字"的,在構(gòu)造出來之后只能通過Unsafe#defineAnonymousClass()返回出來一個(gè)Class實(shí)例來進(jìn)行反射操作。
還有其他幾點(diǎn)看以自行閱讀。這個(gè)方法雖然翻譯為"定義匿名類",但是它所定義的類和實(shí)際的匿名類有點(diǎn)不相同,因此一般情況下我們不會用到此方法。在Jdk中l(wèi)ambda表達(dá)式相關(guān)的東西用到它,可以看InnerClassLambdaMetafactory這個(gè)類。
allocateInstance#
- public native Object allocateInstance(Class<?> cls) throws InstantiationException;
通過Class對象創(chuàng)建一個(gè)類的實(shí)例,不需要調(diào)用其構(gòu)造函數(shù)、初始化代碼、JVM安全檢查等等。同時(shí),它抑制修飾符檢測,也就是即使構(gòu)造器是private修飾的也能通過此方法實(shí)例化。
內(nèi)存管理#
addressSize#
- public native int addressSize();
獲取本地指針的大小(單位是byte),通常值為4或者8。常量ADDRESS_SIZE就是調(diào)用此方法。
pageSize#
- public native int pageSize();
獲取本地內(nèi)存的頁數(shù),此值為2的冪次方。
allocateMemory#
- public native long allocateMemory(long bytes);
分配一塊新的本地內(nèi)存,通過bytes指定內(nèi)存塊的大小(單位是byte),返回新開辟的內(nèi)存的地址。如果內(nèi)存塊的內(nèi)容不被初始化,那么它們一般會變成內(nèi)存垃圾。生成的本機(jī)指針永遠(yuǎn)不會為零,并將對所有值類型進(jìn)行對齊。可以通過freeMemory方法釋放內(nèi)存塊,或者通過reallocateMemory方法調(diào)整內(nèi)存塊大小。bytes值為負(fù)數(shù)或者過大會拋出IllegalArgumentException異常,如果系統(tǒng)拒絕分配內(nèi)存會拋出OutOfMemoryError異常。
reallocateMemory#
- public native long reallocateMemory(long address, long bytes);
通過指定的內(nèi)存地址address重新調(diào)整本地內(nèi)存塊的大小,調(diào)整后的內(nèi)存塊大小通過bytes指定(單位為byte)。可以通過freeMemory方法釋放內(nèi)存塊,或者通過reallocateMemory方法調(diào)整內(nèi)存塊大小。bytes值為負(fù)數(shù)或者過大會拋出IllegalArgumentException異常,如果系統(tǒng)拒絕分配內(nèi)存會拋出OutOfMemoryError異常。
setMemory#
- public native void setMemory(Object o, long offset, long bytes, byte value);
將給定內(nèi)存塊中的所有字節(jié)設(shè)置為固定值(通常是0)。內(nèi)存塊的地址由對象引用o和偏移地址共同決定,如果對象引用o為null,offset就是絕對地址。第三個(gè)參數(shù)就是內(nèi)存塊的大小,如果使用allocateMemory進(jìn)行內(nèi)存開辟的話,這里的值應(yīng)該和allocateMemory的參數(shù)一致。value就是設(shè)置的固定值,一般為0(這里可以參考netty的DirectByteBuffer)。一般而言,o為null,所有有個(gè)重載方法是public native void setMemory(long offset, long bytes, byte value);,等效于setMemory(null, long offset, long bytes, byte value);。
多線程同步#
主要包括監(jiān)視器鎖定、解鎖以及CAS相關(guān)的方法。
monitorEnter#
- public native void monitorEnter(Object o);
鎖定對象,必須通過monitorExit方法才能解鎖。此方法經(jīng)過實(shí)驗(yàn)是可以重入的,也就是可以多次調(diào)用,然后通過多次調(diào)用monitorExit進(jìn)行解鎖。
monitorExit#
- public native void monitorExit(Object o);
解鎖對象,前提是對象必須已經(jīng)調(diào)用monitorEnter進(jìn)行加鎖,否則拋出IllegalMonitorStateException異常。
tryMonitorEnter#
- public native boolean tryMonitorEnter(Object o);
嘗試鎖定對象,如果加鎖成功返回true,否則返回false。必須通過monitorExit方法才能解鎖。
compareAndSwapObject#
- public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
針對Object對象進(jìn)行CAS操作。即是對應(yīng)Java變量引用o,原子性地更新o中偏移地址為offset的屬性的值為x,當(dāng)且僅的偏移地址為offset的屬性的當(dāng)前值為expected才會更新成功返回true,否則返回false。
- o:目標(biāo)Java變量引用。
- offset:目標(biāo)Java變量中的目標(biāo)屬性的偏移地址。
- expected:目標(biāo)Java變量中的目標(biāo)屬性的期望的當(dāng)前值。
- x:目標(biāo)Java變量中的目標(biāo)屬性的目標(biāo)更新值。
類似的方法有compareAndSwapInt和compareAndSwapLong,在Jdk8中基于CAS擴(kuò)展出來的方法有g(shù)etAndAddInt、getAndAddLong、getAndSetInt、getAndSetLong、getAndSetObject,它們的作用都是:通過CAS設(shè)置新的值,返回舊的值。
線程的掛起和恢復(fù)#
unpark#
- public native void unpark(Object thread);
釋放被park創(chuàng)建的在一個(gè)線程上的阻塞。這個(gè)方法也可以被使用來終止一個(gè)先前調(diào)用park導(dǎo)致的阻塞。這個(gè)操作是不安全的,因此必須保證線程是存活的(thread has not been destroyed)。從Java代碼中判斷一個(gè)線程是否存活的是顯而易見的,但是從native代碼中這機(jī)會是不可能自動(dòng)完成的。
park#
- public native void park(boolean isAbsolute, long time);
阻塞當(dāng)前線程直到一個(gè)unpark方法出現(xiàn)(被調(diào)用)、一個(gè)用于unpark方法已經(jīng)出現(xiàn)過(在此park方法調(diào)用之前已經(jīng)調(diào)用過)、線程被中斷或者time時(shí)間到期(也就是阻塞超時(shí))。在time非零的情況下,如果isAbsolute為true,time是相對于新紀(jì)元之后的毫秒,否則time表示納秒。這個(gè)方法執(zhí)行時(shí)也可能不合理地返回(沒有具體原因)。并發(fā)包java.util.concurrent中的框架對線程的掛起操作被封裝在LockSupport類中,LockSupport類中有各種版本pack方法,但最終都調(diào)用了Unsafe#park()方法。
內(nèi)存屏障#
內(nèi)存屏障相關(guān)的方法是在Jdk8添加的。內(nèi)存屏障相關(guān)的知識可以先自行查閱。
loadFence#
- public native void loadFence();
在該方法之前的所有讀操作,一定在load屏障之前執(zhí)行完成。
storeFence#
- public native void storeFence();
在該方法之前的所有寫操作,一定在store屏障之前執(zhí)行完成
fullFence#
- public native void fullFence();
在該方法之前的所有讀寫操作,一定在full屏障之前執(zhí)行完成,這個(gè)內(nèi)存屏障相當(dāng)于上面兩個(gè)(load屏障和store屏障)的合體功能。
其他#
getLoadAverage#
- public native int getLoadAverage(double[] loadavg, int nelems);
獲取系統(tǒng)的平均負(fù)載值,loadavg這個(gè)double數(shù)組將會存放負(fù)載值的結(jié)果,nelems決定樣本數(shù)量,nelems只能取值為1到3,分別代表最近1、5、15分鐘內(nèi)系統(tǒng)的平均負(fù)載。如果無法獲取系統(tǒng)的負(fù)載,此方法返回-1,否則返回獲取到的樣本數(shù)量(loadavg中有效的元素個(gè)數(shù))。實(shí)驗(yàn)中這個(gè)方法一直返回-1,其實(shí)完全可以使用JMX中的相關(guān)方法替代此方法。
throwException#
- public native void throwException(Throwable ee);
繞過檢測機(jī)制直接拋出異常。
作者:?throwable?、只會一點(diǎn)java?
出處:https://www.cnblogs.com/throwable/p/9139947.html、?https://www.cnblogs.com/dennyzhangdd/p/7230012.html
總結(jié)
以上是生活随笔為你收集整理的JAVA中神奇的双刃剑--Unsafe的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C#类的属性遍历及属性值获取
- 下一篇: 项目移植,项目环境问题