java 中random类使用_Java中的天使和魔鬼:Unsafe类
我們在看ConcurrentHashMap源碼時經常看到Unsafe類的使用,今天我們來了解下Unsafe類。
Java是一個安全的編程語言,它能最大程度的防止程序員犯一些低級的錯誤(大部分是和內存管理有關的)。但凡事不是絕對的,使用Unsafe程序員就可以操作內存,因此可能帶來一個安全隱患。
這篇文章是就快速學習下sun.misc.Unsafe的公共API和一些有趣的使用例子。
1、Unsafe 實例化
在使用Unsafe之前我們需要先實例化它。但我們不能通過像Unsafe unsafe = new Unsafe()這種簡單的方式來實現Unsafe的實例化,這是由于Unsafe的構造方法是私有的。Unsafe有一個靜態的getUnsafe()方法,但是如果天真的以為調用該方法就可以的話,那你將遇到一個SecurityException異常,這是由于該方法只能在被信任的代碼中調用。
public static Unsafe getUnsafe() { Class cc = sun.reflect.Reflection.getCallerClass(2); if (cc.getClassLoader() != null) throw new SecurityException("Unsafe"); return theUnsafe;}那Java是如何判斷我們的代碼是否是受信的呢?它就是通過判斷加載我們代碼的類加載器是否是根類加載器。
我們可是通過這種方法將我們自己的代碼變為受信的,使用jvm參數bootclasspath。如下所示:
java?-Xbootclasspath:/usr/jdk1.7.0/jre/lib/rt.jar:.?com.mishadoff.magic.UnsafeClient但這種方式太難了,Unsafe類內部有一個名為theUnsafe的私有實例變量,我們可以通過反射來獲取該實例變量。
Field f = Unsafe.class.getDeclaredField("theUnsafe");f.setAccessible(true);Unsafe?unsafe?=?(Unsafe)?f.get(null);注意: 忽略你的IDE提示. 例如, eclipse可能會報這樣的錯誤”Access restriction…” 單如果你運行你的代碼,會發現一切正常。如果還是還是提示錯誤,你可以通過如下的方式關閉該錯誤提示:
Preferences -> Java -> Compiler -> Errors/Warnings ->Deprecated?and?restricted?API?->?Forbidden?reference?->?Warning?????2、Unsafe API
類 sun.misc.Unsafe 由150個方法組成。事實上這些方法只有幾組是非常重要的用來操作不同的對象。下面我們就來看下這些方法中的一部分。
1、Info 僅僅是返回一個低級別的內存相關的信息
addressSize
pageSize
2、Objects. 提供操作對象和對象字段的方法
allocateInstance
objectFieldOffset
3、Classes. 提供針對類和類的靜態字段操作的方法
staticFieldOffset
defineClass
defineAnonymousClass
ensureClassInitialized
4、Arrays. 數組操作
arrayBaseOffset
arrayIndexScale
5、Synchronization. 低級別的同步原語
monitorEnter
tryMonitorEnter
monitorExit
compareAndSwapInt
putOrderedInt
6、Memory. 直接訪問內存的方法
allocateMemory
copyMemory
freeMemory
getAddress
getInt
putInt
接下來是一些有趣的使用case
3、跳過構造初始化
allocateInstance方法可能是有用的,當你需要在構造函數中跳過對象初始化階段或繞過安全檢查又或者你想要實例化哪些沒有提供公共構造函數的類時就可以使用該方法。考慮下面的類:
class A { private long a; // not initialized value public A() { this.a = 1; // initialization } public long a() { return this.a; }}通過構造函數,反射,Unsafe分別來實例化該類結果是不同的:
A o1 = new A(); // constructoro1.a(); // prints 1A o2 = A.class.newInstance(); // reflectiono2.a(); // prints 1A o3 = (A) unsafe.allocateInstance(A.class); // unsafeo3.a();?//?prints?0allocateInstance根本沒有進入構造方法,對于單例模式,簡直是噩夢。
4、內存修改,繞過安全檢查器
對C程序員來說這中情況是很常見的。
思考一下一些簡單的類是如何堅持訪問規則的:
class Guard { private int ACCESS_ALLOWED = 1; public boolean giveAccess() { return 42 == ACCESS_ALLOWED; }}客戶端代碼是非常安全的,調用giveAccess()檢查訪問規則。不幸的是對所有的客戶端代碼,它總是返回false。只有特權用戶在某種程度上可以改變ACCESS_ALLOWED常量并且獲得訪問權限。
事實上,這不是真的。這是證明它的代碼:
Guard guard = new Guard();guard.giveAccess(); // false, no access// bypassUnsafe unsafe = getUnsafe();Field f = guard.getClass().getDeclaredField("ACCESS_ALLOWED");unsafe.putInt(guard, unsafe.objectFieldOffset(f), 42); // memory corruptionguard.giveAccess();?//?true,?access?granted現在所有的客戶端都沒有訪問限制了。
事實上同樣的功能也可以通過反射來實現。但有趣的是, 通過上面的方式我們修改任何對象,即使我們沒有持有對象的引用。
舉個例子, 在內存中有另外的一個Guard對象,并且地址緊挨著當前對象的地址,我們就可以通過下面的代碼來修改該對象的ACCESS_ALLOWED字段的值。
unsafe.putInt(guard,?16?+?unsafe.objectFieldOffset(f),?42);?//?memory?corruption注意,我們沒有使用任何指向該對象的引用,16是Guard對象在32位架構上的大小。我們也可以通過sizeOf方法來計算Guard對象的大小。
5、sizeOf 計算內存大小
使用objectFieldOffset方法我們可以實現C風格的sizeof方法。下面的方法實現返回對象的表面上的大小
public static long sizeOf(Object o) { Unsafe u = getUnsafe(); HashSet fields = new HashSet(); 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(); } // get offset long maxSize = 0; for (Field f : fields) { long offset = u.objectFieldOffset(f); if (offset > maxSize) { maxSize = offset; } } return ((maxSize/8) + 1) * 8; // padding}算法邏輯如下:收集所有包括父類在內的非靜態字段,獲得每個字段的偏移量,發現最大并添加填充。也許,我錯過了一些東西,但是概念是明確的。
更簡單的sizeof方法實現邏輯是:我們只讀取該對象對應的class對象中關于大小的字段值。在JVM 1.7 32 位版本上該表示大小的字段偏移量是12。
public static long sizeOf(Object object){ return getUnsafe().getAddress( normalize(getUnsafe().getInt(object, 4L)) + 12L);}normalize是一個將有符號的int類型轉為無符號的long類型的方法。
private static long normalize(int value) { if(value >= 0) return value; return (~0L >>> 32) & value;}太棒了,這個方法返回的結果和我們之前的sizeof函數是相同的。
事實上,對于合適的,安全的,準確的sizeof函數最好使用java.lang.instrument包,但它需要特殊的JVM參數。
6、淺拷貝
在實現了計算對象淺層大小的基礎上,我們可以非常容易的添加對象的拷貝方法。標準的辦法需要修改我們的代碼和Cloneable。或者你可以實現自定義的對象拷貝函數,但它不會變為通用的函數。
淺拷貝:
static Object shallowCopy(Object obj) { long size = sizeOf(obj); long start = toAddress(obj); long address = getUnsafe().allocateMemory(size); getUnsafe().copyMemory(start, address, size); return fromAddress(address);}toAddress 和 fromAddress 將對象轉為它在內存中的地址或者從指定的地址內容轉為對象。
static long toAddress(Object obj) { Object[] array = new Object[] {obj}; long baseOffset = getUnsafe().arrayBaseOffset(Object[].class); return normalize(getUnsafe().getInt(array, baseOffset));}static Object fromAddress(long address) { Object[] array = new Object[] {null}; long baseOffset = getUnsafe().arrayBaseOffset(Object[].class); getUnsafe().putLong(array, baseOffset, address); return array[0];}該拷貝函數可以用來拷貝任何類型的對象,因為對象的大小是動態計算的。
注意 在完成拷貝動作后你需要將拷貝對象的類型強轉為目標類型。
7、隱藏密碼
在Unsafe的直接內存訪問方法使用case中有一個非常有趣的用法就是刪除內存中不想要的對象。
大多數獲取用戶密碼的API方法的返回值不是byte[]就是char[],這是為什么呢?
這完全是出于安全原因, 因為我們可以在不需要它們的時候將數組元素置為失效。如果我們獲取的密碼是字符串類型,則密碼字符串是作為一個對象保存在內存中的。要將該密碼字符串置為無效,我們只能講字符串引用職位null,但是該字符串的內容任然存在內存直到GC回收該對象后。
這個技巧在內存創建一個假的大小相同字符串對象來替換原來的:
String password = new String("l00k@myHor$e");String fake = new String(password.replaceAll(".", "?"));System.out.println(password); // l00k@myHor$eSystem.out.println(fake); // ????????????getUnsafe().copyMemory(fake, 0L, null, toAddress(password), sizeOf(password));System.out.println(password); // ????????????System.out.println(fake);?//?????????????感覺安全了嗎?
其實該方法不是真的安全。想要真的安全我們可以通過反射API將字符串對象中的字符數組value字段的值修改為null。
Field stringValue = String.class.getDeclaredField("value");stringValue.setAccessible(true);char[] mem = (char[]) stringValue.get(password);for (int i=0; i < mem.length; i++) { mem[i] = '?';}8、多重繼承
在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))有個問題是我們需要先將要轉的對象轉為Object,然后再轉為我們想要的類型。這是為了欺騙編譯器。
9、動態類
我們可以在運行時創建類, 例如通過一個編譯好的class文件。將class文件的內容讀入到字節數組中然后將該數組傳遞到合適的defineClass方法中。
byte[] classContents = getClassContent();Class c = getUnsafe().defineClass(null, classContents, 0, classContents.length);c.getMethod("a").invoke(c.newInstance(),?null);?//?1讀取class文件內如的代碼:
private static byte[] getClassContent() throws Exception { File f = new File("/home/mishadoff/tmp/A.class"); FileInputStream input = new FileInputStream(f); byte[] content = new byte[(int)f.length()]; input.read(content); input.close(); return content;}該方式是非常有用的,如果你確實需要在運行時動態的創建類。比如生產代理類或切面類。
10、拋出一個異常
不喜歡受檢異常?這不是問題。
getUnsafe().throwException(new?IOException());該方法拋出一個受檢異常,但是你的代碼不需要強制捕獲該異常就像運行時異常一樣。
11、快速序列化
每個人都知道java標準的序列化的功能速度很慢而且它還需要類擁有公有的構造函數。
外部序列化是更好的方式,但是需要定義針對待序列化類的schema。
非常流行的高性能序列化庫,像kryo是有使用限制的,比如在內存缺乏的環境就不合適。
但通過使用Unsafe類我們可以非常簡單的實現完整的序列化功能。
序列化:
通過反射定義類的序列化。這個可以只做一次。
通過Unsafe的getLong, getInt, getObject等方法獲取字段真實的值。
添加可以恢復該對象的標識符。
將這些數據寫入到輸出
當然也可以使用壓縮來節省空間。
反序列化:
創建一個序列化類的實例,可以通過方法allocateInstance。因為該方法不需要任何構造方法。
創建schema, 和序列化類似
從文件或輸入讀取或有的字段
使用 Unsafe 的 putLong, putInt, putObject等方法來填充對象。
事實上要正確實現序列化和反序列化需要注意很多細節,但是思路是清晰的。
這種序列化方式是非常快的。
12、大數組
如你所知Java數組長度的最大值是Integer.MAX_VALUE。使用直接內存分配我們可以創建非常大的數組,該數組的大小只受限于堆的大小。
這里有一個SuperArray的實現:
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()); // 4294967294for (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限制,就不會因為GC導致非常大的停頓。
13、并發
關于并發編程使用Unsafe的只言片語。compareAndSwap 方法是原子的,可以用來實現高性能的無鎖化數據結構。
舉個例子,多個線程并發的更新共享的對象這種場景:
首先我們定義一個簡單的接口 Counter:
interface Counter { void increment(); long getCounter();}我們定義工作線程 CounterClient, 它會使用 Counter:
class CounterClient implements Runnable { private Counter c; private int num; public CounterClient(Counter c, int num) { this.c = c; this.num = num; } @Override public void run() { for (int i = 0; i < num; i++) { c.increment(); } }}這是測試代碼:
int NUM_OF_THREADS = 1000;int NUM_OF_INCREMENTS = 100000;ExecutorService service = Executors.newFixedThreadPool(NUM_OF_THREADS);Counter counter = ... // creating instance of specific counterlong before = System.currentTimeMillis();for (int i = 0; i < NUM_OF_THREADS; i++) { service.submit(new CounterClient(counter, NUM_OF_INCREMENTS));}service.shutdown();service.awaitTermination(1, TimeUnit.MINUTES);long after = System.currentTimeMillis();System.out.println("Counter result: " + c.getCounter());System.out.println("Time?passed?in?ms:"?+?(after?-?before));第一個實現-沒有同步的計數器:
class StupidCounter implements Counter { private long counter = 0; @Override public void increment() { counter++; } @Override public long getCounter() { return counter; }}輸出:
Counter result: 99542945Time?passed?in?ms:?679速度很多,但是沒有對所有的線程進行協調所以結果是錯誤的。第二個版本,使用Java常見的同步方式來實現
class SyncCounter implements Counter { private long counter = 0; @Override public synchronized void increment() { counter++; } @Override public long getCounter() { return counter; }}輸出:
Counter result: 100000000Time?passed?in?ms:?10136徹底的同步當然會導致正確的結果。但是花費的時間令人沮喪。讓我們試試 ReentrantReadWriteLock:
class LockCounter implements Counter { private long counter = 0; private WriteLock lock = new ReentrantReadWriteLock().writeLock(); @Override public void increment() { lock.lock(); counter++; lock.unlock(); } @Override public long getCounter() { return counter; }}輸出:
Counter result: 100000000Time?passed?in?ms:?8065結果依然是正確的,時間也短。那使用原子的類呢?
class AtomicCounter implements Counter { AtomicLong counter = new AtomicLong(0); @Override public void increment() { counter.incrementAndGet(); } @Override public long getCounter() { return counter.get(); }}輸出:
Counter result: 100000000Time?passed?in?ms:?6552使用AtomicCounter的效果更好一點。最后我們試試Unsafe的原子方法compareAndSwapLong看看是不是更進一步。
class CASCounter implements Counter { private volatile long counter = 0; private Unsafe unsafe; private long offset; public CASCounter() throws Exception { unsafe = getUnsafe(); offset = unsafe.objectFieldOffset(CASCounter.class.getDeclaredField("counter")); } @Override public void increment() { long before = counter; while (!unsafe.compareAndSwapLong(this, offset, before, before + 1)) { before = counter; } } @Override public long getCounter() { return counter; }}輸出:
Counter result: 100000000Time?passed?in?ms:?6454看起來和使用原子類是一樣的效果,難道原子類使用了Unsafe?答案是YES。
事實上該例子非常簡單但表現出了Unsafe的強大功能。
就像前面提到的 CAS原語可以用來實現高效的無鎖數據結構。實現的原理很簡單:
1、擁有一個狀態;
2、創建一個它的副本;
3、修改該副本;
4、執行 CAS 操作;
5、如果失敗就重復執行;
事實上,在真實的環境它的實現難度超過你的想象,這其中有需要類似ABA,指令重排序這樣的問題。
14、結論
盡管Unsafe有這么多有用的應用,但是盡量不要使用。當然了使用JDK中利用了Unsafe實現的類是可以的。或者你對你代碼功力非常自信,可以自己挖坑再填坑哈~
歡迎小伙伴們留言交流~~
總結
以上是生活随笔為你收集整理的java 中random类使用_Java中的天使和魔鬼:Unsafe类的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java api接口报500_应用程序编
- 下一篇: webmagic 获取文本_学习使用Ja