臭名昭著的sun.misc.Unsafe解释
Java虛擬機的最大競爭對手可能是托管C#等語言的Microsoft CLR 。 CLR允許編寫不安全的代碼作為低級編程的入口,這在JVM上很難實現。 如果您需要Java中的此類高級功能,則可能會被迫使用JNI ,這需要您了解一些C并Swift導致代碼緊密耦合到特定平臺。 使用sun.misc.Unsafe ,盡管不鼓勵使用Java API在Java plarform上進行低級編程,但還有另一種選擇。 盡管如此,仍有一些應用程序依賴于sun.misc.Unsafe (例如objenesis)以及所有基于后者的庫(例如kryo) ,例如kryo ,該庫再次用于Twitter的Storm中 。 因此,現在該看看一下了,特別是因為sun.misc.Unsafe的功能被認為已成為Java 9中Java公共API的一部分 。
取得sun.misc.Unsafe實例
sun.misc.Unsafe類旨在僅由核心Java類使用,因此其作者將其唯一的構造函數設為私有,并且僅添加了一個同樣私有的singleton實例。 此實例的公共獲取程序將執行安全檢查,以避免其公共使用:
public static Unsafe getUnsafe() {Class cc = sun.reflect.Reflection.getCallerClass(2);if (cc.getClassLoader() != null)throw new SecurityException("Unsafe");return theUnsafe; }此方法首先從當前線程的方法堆棧中查找調用的Class 。 該查找由另一個名為sun.reflection.Reflection內部類實現,該內部類基本上是瀏覽給定數量的調用堆棧幀,然后返回此方法的定義類。 但是,此安全檢查可能會在將來的版本中更改 。 瀏覽堆棧時,第一個找到的類(索引0 )顯然是Reflection類本身,第二個(索引1 )類將是Unsafe類,這樣索引2將保存正在調用Unsafe#getUnsafe() 。
然后檢查此查找的類的ClassLoader ,其中使用null引用表示HotSpot虛擬機上的引導程序類加載器。 (這在Class#getClassLoader()中有說明,其中說“ 某些實現可能使用null來表示引導類加載器 ”。)由于通常不會在該類加載器中加載非核心Java類,因此您永遠不會能夠直接調用此方法,但收到拋出的SecurityException作為答案。 (從技術上講,您可以通過將引導程序類加載器添加到–Xbootclasspath來強制VM使用引導程序類加載器來加載應用程序類,但這需要在應用程序代碼之外進行一些設置,您可能希望避免這種設置。)因此,進行以下測試將會成功:
@Test(expected = SecurityException.class) public void testSingletonGetter() throws Exception {Unsafe.getUnsafe(); }但是,安全檢查的設計不當,應視為對單例反模式的警告。 只要不禁止使用反射 (這很困難,因為在許多框架中廣泛使用反射 ),您總是可以通過檢查類的私有成員來獲得實例。 從Unsafe類的源代碼中,您可以了解到單例實例存儲在名為theUnsafe的私有靜態字段中。 對于HotSpot虛擬機至少是這樣。 對于我們來說不幸的是,其他虛擬機實現有時對此字段使用其他名稱。 例如,Android的Unsafe類將其單例實例存儲在名為THE_ONE的字段中。 這使得很難提供一種“兼容”的方式來接收實例。 但是,由于我們已經通過使用Unsafe類離開了兼容性的保存范圍,因此我們不必為此擔心,而應該比完全使用該類還要擔心。 為了掌握單例實例,您只需讀取單例字段的值即可:
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); Unsafe unsafe = (Unsafe) theUnsafe.get(null);或者,您可以調用私人教練。 我個人更喜歡這種方式,因為它可以與Android一起使用,而提取字段卻不能:
Constructor<Unsafe> unsafeConstructor = Unsafe.class.getDeclaredConstructor(); unsafeConstructor.setAccessible(true); Unsafe unsafe = unsafeConstructor.newInstance();您為這種次要的兼容性優勢所付出的代價是最小的堆空間。 但是,在字段或構造函數上使用反射時執行的安全性檢查類似。
創建類的實例而不調用構造函數
我第一次使用Unsafe類是為了創建類的實例而不調用任何類的構造函數。 我需要代理整個類,該類只具有一個嘈雜的構造函數,但我只想將所有方法調用委托給一個實際實例,但是在構造時我還不知道。 創建子類很容易,如果該類已由接口表示,則創建代理將是一件簡單的事情。 但是,對于昂貴的構造函數,我陷入了困境。 通過使用Unsafe類,我得以解決該問題。 考慮一個帶有虛假的構造函數的類:
class ClassWithExpensiveConstructor {private final int value;private ClassWithExpensiveConstructor() {value = doExpensiveLookup();}private int doExpensiveLookup() {try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}return 1;}public int getValue() {return value;} }使用Unsafe ,我們可以創建ClassWithExpensiveConstructor (或其任何子類)的實例,而不必調用上述構造函數,只需直接在堆上分配實例即可:
@Test public void testObjectCreation() throws Exception {ClassWithExpensiveConstructor instance = (ClassWithExpensiveConstructor)unsafe.allocateInstance(ClassWithExpensiveConstructor.class);assertEquals(0, instance.getValue()); }請注意,final字段尚未由構造方法初始化,但使用其類型的默認值進行設置 。 除此之外,構造的實例的行為類似于普通的Java對象。 例如,當它變得不可訪問時,將被垃圾回收。
Java運行時本身在創建對象(例如反序列化)時無需調用構造函數即可創建對象。 因此, ReflectionFactory提供了更多訪問單個對象的權限:
@Test public void testReflectionFactory() throws Exception {@SuppressWarnings("unchecked")Constructor<ClassWithExpensiveConstructor> silentConstructor = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(ClassWithExpensiveConstructor.class, Object.class.getConstructor());silentConstructor.setAccessible(true);assertEquals(10, silentConstructor.newInstance().getValue()); }請注意, ReflectionFactory類只需要一個RuntimePermission呼吁reflectionFactoryAccess接收的單一實例,因此沒有反映這里需要。 收到的ReflectionFactory實例允許您定義任何構造函數以成為給定類型的構造函數。 在上面的示例中,我為此使用了默認的java.lang.Object構造函數。 但是,您可以使用任何構造函數:
class OtherClass {private final int value;private final int unknownValue;private OtherClass() {System.out.println("test");this.value = 10;this.unknownValue = 20;} }@Test public void testStrangeReflectionFactory() throws Exception {@SuppressWarnings("unchecked")Constructor<ClassWithExpensiveConstructor> silentConstructor = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(ClassWithExpensiveConstructor.class,OtherClass.class.getDeclaredConstructor());silentConstructor.setAccessible(true);ClassWithExpensiveConstructor instance = silentConstructor.newInstance();assertEquals(10, instance.getValue());assertEquals(ClassWithExpensiveConstructor.class, instance.getClass());assertEquals(Object.class, instance.getClass().getSuperclass()); }請注意,即使調用了完全不同的類的構造函數,也已在此構造函數中設置了value 。 然而,目標類中不存在的字段也會被忽略,從上面的示例中也可以明顯看出。 請注意, OtherClass不會成為構造的實例類型層次結構的一部分,只需為“序列化”類型借用OtherClass的構造函數。
此博客條目中未提及的是其他方法,例如Unsafe#defineClass , Unsafe#defineAnonymousClass或Unsafe#ensureClassInitialized 。 但是,公共API的ClassLoader也定義了類似的功能。
本機內存分配
您是否曾經想過用Java分配一個數組,該數組應該具有多個Integer.MAX_VALUE條目? 可能不是因為這不是一項常見任務,但是如果您曾經需要此功能,則可以實現。 您可以通過分配本機內存來創建這樣的數組。 本機內存分配例如由Java的NIO包中提供的直接字節緩沖區使用。 除了堆內存之外,本機內存不是堆區域的一部分,可以非排他性地用于例如與其他進程進行通信。 結果,Java的堆空間與本機空間競爭:分配給JVM的內存越多,剩余的本機內存就越少。
讓我們看一個示例,該示例在Java中使用本地(堆外)內存創建上述超大數組:
class DirectIntArray {private final static long INT_SIZE_IN_BYTES = 4;private final long startIndex;public DirectIntArray(long size) {startIndex = unsafe.allocateMemory(size * INT_SIZE_IN_BYTES);unsafe.setMemory(startIndex, size * INT_SIZE_IN_BYTES, (byte) 0);}}public void setValue(long index, int value) {unsafe.putInt(index(index), value);}public int getValue(long index) {return unsafe.getInt(index(index));}private long index(long offset) {return startIndex + offset * INT_SIZE_IN_BYTES;}public void destroy() {unsafe.freeMemory(startIndex);} }@Test public void testDirectIntArray() throws Exception {long maximum = Integer.MAX_VALUE + 1L;DirectIntArray directIntArray = new DirectIntArray(maximum);directIntArray.setValue(0L, 10);directIntArray.setValue(maximum, 20);assertEquals(10, directIntArray.getValue(0L));assertEquals(20, directIntArray.getValue(maximum));directIntArray.destroy(); }首先,請確保您的計算機有足夠的內存來運行此示例! 您至少需要(2147483647 + 1) * 4 byte = 8192 MB本機內存才能運行代碼。 如果您使用過其他編程語言(例如C),則每天都要進行直接內存分配。 通過調用Unsafe#allocateMemory(long) ,虛擬機將為您分配請求的本機內存量。 之后,您有責任正確處理此內存。
存儲特定值所需的內存量取決于類型的大小。 在上面的示例中,我使用了一個int類型,該類型表示32位整數。 因此,單個int值消耗4個字節。 對于基本類型, 大小有據可查 。 但是,計算對象類型的大小更為復雜,因為它們取決于類型層次結構中任何地方聲明的非靜態字段的數量。 計算對象大小的最典型方法是使用Java的Attach API中的Instrumented類,該類為此目的提供了一種專用方法,稱為getObjectSize 。 但是,在本節的最后,我將評估處理對象的另一種(hacky)方式。
請注意,直接分配的內存始終是本機內存 ,因此不會進行垃圾回收。 因此,如上例所示,您必須通過調用Unsafe#freeMemory(long)顯式釋放內存。 否則,您將保留一些內存,只要JVM實例正在運行,就無法將其用于其他用途,這是內存泄漏和非垃圾收集語言中的常見問題。 或者,您也可以通過調用Unsafe#reallocateMemory(long, long)直接在某個地址重新分配內存,其中第二個參數描述了JVM在給定地址保留的新字節數。
另外,請注意,直接分配的內存未使用特定值初始化。 通常,您會從該內存區域的舊用法中發現垃圾,因此如果需要默認值,則必須顯式初始化分配的內存。 當您讓Java運行時為您分配內存時,通常會為您完成此操作。 在上面的示例中,借助于Unsafe#setMemory方法,整個區域被零覆蓋。
使用直接分配的內存時,JVM都不會為您執行范圍檢查。 因此,如以下示例所示,可能會破壞您的內存:
@Test public void testMallaciousAllocation() throws Exception {long address = unsafe.allocateMemory(2L * 4);unsafe.setMemory(address, 8L, (byte) 0);assertEquals(0, unsafe.getInt(address));assertEquals(0, unsafe.getInt(address + 4));unsafe.putInt(address + 1, 0xffffffff);assertEquals(0xffffff00, unsafe.getInt(address));assertEquals(0x000000ff, unsafe.getInt(address + 4)); }請注意,我們在空間中寫入了一個值,該值分別部分保留給第一個和第二個數字。 這張照片可能會清除一切。 請注意,內存中的值是從“右向左”運行的(但這可能取決于計算機)。
第一行顯示將零寫入整個分配的本機內存區域后的初始狀態。 然后,我們使用32個字節覆蓋4個字節,并以單個字節的偏移量覆蓋。 最后一行顯示此寫入操作后的結果。
最后,我們想將整個對象寫入本地內存。 如上所述,這是一項艱巨的任務,因為我們首先需要計算對象的大小才能知道我們需要保留的大小。 但是,Unsafe類不提供此類功能。 至少不是直接地,因為我們至少可以使用Unsafe類來查找實例字段的偏移量,JVM自身在堆上分配對象時會使用該實例字段的偏移量。 這使我們能夠找到對象的近似大小:
public long sizeOf(Class<?> clazz)long maximumOffset = 0;do {for (Field f : clazz.getDeclaredFields()) {if (!Modifier.isStatic(f.getModifiers())) {maximumOffset = Math.max(maximumOffset, unsafe.objectFieldOffset(f));}}} while ((clazz = clazz.getSuperclass()) != null);return maximumOffset + 8; }乍一看,這似乎很神秘,但是此代碼背后沒有什么大秘密。 我們只是簡單地遍歷在類本身或其任何超類中聲明的所有非靜態字段。 我們不必擔心接口,因為它們無法定義字段,因此永遠不會更改對象的內存布局。 這些字段中的任何一個都有一個偏移量,該偏移量表示當JVM將此類實例存儲在內存中時,該字段的值相對于用于該對象的第一個字節占用該字段的第一個字節。 我們只需找到最大偏移量即可找到除最后一個字段以外的所有字段所需的空間。 由于在64位計算機上運行時,字段對于long值或double值或對象引用的占用空間永遠不會超過64位(8字節),因此我們至少找到了用于存儲空間的上限。賓語。 因此,我們只需將這8個字節添加到最大索引中,就不會遇到保留很小空間的危險。 這個想法當然會浪費一些字節,并且應該在生產代碼中使用更好的算法。
在這種情況下,最好將類定義視為異構數組的一種形式。 請注意,最小字段偏移量不是0而是正值。 前幾個字節包含元信息。 下圖通過一個int和一個long字段(其中兩個字段都有偏移量)的示例對象形象化了該原理。 請注意,在將對象的副本寫入本機內存時,我們通常不會寫入元信息,因此我們可以進一步減少使用的本機便箋的數量。 還要注意,此內存布局可能高度依賴于Java虛擬機的實現。
通過這種過度仔細的估計,我們現在可以實現一些存根方法,將對象的淺表副本直接寫入本機內存。 請注意,本機內存并不真正了解對象的概念。 我們基本上只是將給定的字節數設置為反映對象當前值的值。 只要我們記住此類型的內存布局,這些字節就包含了足以重構此對象的信息。
public void place(Object o, long address) throws Exception {Class clazz = o.getClass();do {for (Field f : clazz.getDeclaredFields()) {if (!Modifier.isStatic(f.getModifiers())) {long offset = unsafe.objectFieldOffset(f);if (f.getType() == long.class) {unsafe.putLong(address + offset, unsafe.getLong(o, offset));} else if (f.getType() == int.class) {unsafe.putInt(address + offset, unsafe.getInt(o, offset));} else {throw new UnsupportedOperationException();}}}} while ((clazz = clazz.getSuperclass()) != null); }public Object read(Class clazz, long address) throws Exception {Object instance = unsafe.allocateInstance(clazz);do {for (Field f : clazz.getDeclaredFields()) {if (!Modifier.isStatic(f.getModifiers())) {long offset = unsafe.objectFieldOffset(f);if (f.getType() == long.class) {unsafe.putLong(instance, offset, unsafe.getLong(address + offset));} else if (f.getType() == int.class) {unsafe.putLong(instance, offset, unsafe.getInt(address + offset));} else {throw new UnsupportedOperationException();}}}} while ((clazz = clazz.getSuperclass()) != null);return instance; }@Test public void testObjectAllocation() throws Exception {long containerSize = sizeOf(Container.class);long address = unsafe.allocateMemory(containerSize);Container c1 = new Container(10, 1000L);Container c2 = new Container(5, -10L);place(c1, address);place(c2, address + containerSize);Container newC1 = (Container) read(Container.class, address);Container newC2 = (Container) read(Container.class, address + containerSize);assertEquals(c1, newC1);assertEquals(c2, newC2); }請注意,這些用于在本機內存中寫入和讀取對象的存根方法僅支持int和long字段值。 當然, Unsafe支持所有原始值,甚至可以通過使用方法的易失性形式編寫值,而無需訪問線程本地緩存。 存根僅用于使示例簡潔。 請注意,由于這些“實例”是直接分配其內存的,因此永遠也不會垃圾回收。 (但是,也許這就是您想要的。)此外,在計算大小時要小心,因為對象的內存布局可能取決于VM,并且與32位計算機相比,如果64位計算機運行您的代碼,則該對象也會更改。 偏移甚至可能在JVM重新啟動之間發生變化。
為了讀取和編寫原語或對象引用, Unsafe提供了以下類型相關的方法:
- getXXX(Object target, long offset) :將在指定的偏移量處從目標地址讀取XXX類型的值。
- putXXX(Object target, long offset, XXX value) :將值放置在目標地址的指定偏移量處。
- getXXXVolatile(Object target, long offset) :將在指定的偏移量處從目標地址讀取XXX類型的值,并且不會命中任何線程本地緩存。
- putXXXVolatile(Object target, long offset, XXX value) :將值放置在目標地址處的指定偏移量處,并且不會命中任何線程本地緩存。
- putOrderedXXX(Object target, long offset, XXX value) :將值放置在指定offet的目標地址上,并且可能不會訪問所有線程本地緩存。
- putXXX(long address, XXX value) :將XXX類型的指定值直接放在指定地址。
- getXXX(long address) :將從指定地址讀取XXX類型的值。
- compareAndSwapXXX(Object target, long offset, long expectedValue, long value) :將從目標地址的指定偏移量原子讀取一個XXX類型的值,如果此偏移量的當前值等于預期值,則設置給定值。
請注意,使用getObject(Object, long)方法族在本地內存中寫入或讀取對象副本時,您正在復制引用。 因此,在應用上述方法時,您僅創建實例的淺表副本。 但是,您始終可以遞歸讀取對象大小和偏移量并創建深層副本。 但是,請注意循環對象引用,當不小心應用此原理時,循環引用會導致無限循環。
這里沒有提到Unsafe類中的現有實用程序,這些實用程序允許處理諸如staticFieldOffset類的靜態字段值并用于處理數組類型。 最后,兩種名為Unsafe#copyMemory方法Unsafe#copyMemory可以指示相對于特定對象偏移量或絕對地址的直接內存復制,如以下示例所示:
@Test public void testCopy() throws Exception {long address = unsafe.allocateMemory(4L);unsafe.putInt(address, 100);long otherAddress = unsafe.allocateMemory(4L);unsafe.copyMemory(address, otherAddress, 4L);assertEquals(100, unsafe.getInt(otherAddress)); }拋出未經檢查的檢查異常
在Unsafe中還有其他有趣的方法可以找到。 您是否曾經想過要拋出特定的異常以在較低層中進行處理,但是您的高層接口類型并未聲明此已檢查異常? Unsafe#throwException允許這樣做:
@Test(expected = Exception.class) public void testThrowChecked() throws Exception {throwChecked(); }public void throwChecked() {unsafe.throwException(new Exception()); }本機并發
使用park和unpark方法,您可以將線程暫停一定時間并恢復它:
@Test public void testPark() throws Exception {final boolean[] run = new boolean[1];Thread thread = new Thread() {@Overridepublic void run() {unsafe.park(true, 100000L);run[0] = true;}};thread.start();unsafe.unpark(thread);thread.join(100L);assertTrue(run[0]); }此外,可以通過使用monitorEnter(Object) , monitorExit(Object)和tryMonitorEnter(Object)使用Unsafe直接獲取監視器。
- 包含此博客條目所有示例的文件的主旨是可用 。
翻譯自: https://www.javacodegeeks.com/2013/12/the-infamous-sun-misc-unsafe-explained.html
總結
以上是生活随笔為你收集整理的臭名昭著的sun.misc.Unsafe解释的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: DDOS攻击示意图(ddos攻击图示)
- 下一篇: 安卓手机刷机怎么刷机教程(安卓手机刷机怎