Java 中的 Unsafe 魔法類的作用大全
Unsafe是位于sun.misc包下的一個類,主要提供一些用于執(zhí)行低級別、不安全操作的方法,如直接訪問系統(tǒng)內(nèi)存資源、自主管理內(nèi)存資源等,這些方法在提升Java運行效率、增強Java語言底層資源操作能力方面起到了很大的作用。
但是,這個類的作者不希望我們使用它,因為我們雖然我們獲取到了對底層的控制權,但是也增大了風險,安全性正是Java相對于C++/C的優(yōu)勢。因為該類在sun.misc
包下,默認是被BootstrapClassLoader加載的。如果我們在程序中去調(diào)用這個類的話,我們使用的類加載器肯定是 AppClassLoader,問題是在Unsafe中是這樣寫的:
private static final Unsafe theUnsafe; private Unsafe() { } @CallerSensitive public static Unsafe getUnsafe() { Class var0 = Reflection.getCallerClass(); if (!VM.isSystemDomainLoader(var0.getClassLoader())) { throw new SecurityException("Unsafe"); } else { return theUnsafe; } }
將構造函數(shù)私有,然后提供了一個靜態(tài)方法去獲取當前類實例。在getUnsafe()
方法中首先判斷當前類加載器是否為空,因為使用 BootstrapClassLoader 本身就是空,它是用c++實現(xiàn)的,這樣就限制了我們在自己的代碼中使用這個類。
但是同時作者也算是給我們提供了一個后門,因為Java有反射機制。調(diào)用的思路就是將theUnsafe
對象設置為可見。
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) theUnsafeField.get(null); System.out.println(unsafe);
unsafe類功能介紹:
內(nèi)存操作
這部分主要包含堆外內(nèi)存的分配、拷貝、釋放、給定地址值操作等方法。
//分配內(nèi)存, 相當于C++的malloc函數(shù) public native long allocateMemory(long bytes); //擴充內(nèi)存 public native long reallocateMemory(long address, long bytes); //釋放內(nèi)存 public native void freeMemory(long address); //在給定的內(nèi)存塊中設置值 public native void setMemory(Object o, long offset, long bytes, byte value); //內(nèi)存拷貝 public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes); //獲取給定地址值,忽略修飾限定符的訪問限制。與此類似操作還有: getInt,getDouble,getLong,getChar等 public native Object getObject(Object o, long offset); //為給定地址設置值,忽略修飾限定符的訪問限制,與此類似操作還有: putInt,putDouble,putLong,putChar等 public native void putObject(Object o, long offset, Object x); //獲取給定地址的byte類型的值(當且僅當該內(nèi)存地址為allocateMemory分配時,此方法結果為確定的) public native byte getByte(long address); //為給定地址設置byte類型的值(當且僅當該內(nèi)存地址為allocateMemory分配時,此方法結果才是確定的) public native void putByte(long address, byte x);
通常,我們在Java中創(chuàng)建的對象都處于堆內(nèi)內(nèi)存(heap)中,堆內(nèi)內(nèi)存是由JVM所管控的Java進程內(nèi)存,并且它們遵循JVM的內(nèi)存管理機制,JVM會采用垃圾回收機制統(tǒng)一管理堆內(nèi)存。與之相對的是堆外內(nèi)存,存在于JVM管控之外的內(nèi)存區(qū)域,Java中對堆外內(nèi)存的操作,依賴于Unsafe提供的操作堆外內(nèi)存的native方法。
使用堆外內(nèi)存的原因
- 對垃圾回收停頓的改善。由于堆外內(nèi)存是直接受操作系統(tǒng)管理而不是JVM,所以當我們使用堆外內(nèi)存時,即可保持較小的堆內(nèi)內(nèi)存規(guī)模。從而在GC時減少回收停頓對于應用的影響。
- 提升程序I/O操作的性能。通常在I/O通信過程中,會存在堆內(nèi)內(nèi)存到堆外內(nèi)存的數(shù)據(jù)拷貝操作,對于需要頻繁進行內(nèi)存間數(shù)據(jù)拷貝且生命周期較短的暫存數(shù)據(jù),都建議存儲到堆外內(nèi)存。
典型應用
DirectByteBuffer是Java用于實現(xiàn)堆外內(nèi)存的一個重要類,通常用在通信過程中做緩沖池,如在Netty、MINA等NIO框架中應用廣泛。DirectByteBuffer對于堆外內(nèi)存的創(chuàng)建、使用、銷毀等邏輯均由Unsafe提供的堆外內(nèi)存API來實現(xiàn)。
下面的代碼為DirectByteBuffer構造函數(shù),創(chuàng)建DirectByteBuffer的時候,通過Unsafe.allocateMemory分配內(nèi)存、Unsafe.setMemory進行內(nèi)存初始化,而后構建Cleaner對象用于跟蹤DirectByteBuffer對象的垃圾回收,以實現(xiàn)當DirectByteBuffer被垃圾回收時,分配的堆外內(nèi)存一起被釋放。
DirectByteBuffer(int cap) { // package-private super(-1, 0, cap, cap); boolean pa = VM.isDirectMemoryPageAligned(); int ps = Bits.pageSize(); long size = Math.max(1L, (long)cap + (pa ? ps : 0)); Bits.reserveMemory(size, cap); long base = 0; try { //分配內(nèi)存,返回基地址 base = unsafe.allocateMemory(size); } catch (OutOfMemoryError x) { Bits.unreserveMemory(size, cap); throw x; } //內(nèi)存初始化 unsafe.setMemory(base, size, (byte) 0); if (pa && (base % ps != 0)) { // Round up to page boundary address = base + ps - (base & (ps - 1)); } else { address = base; } //跟蹤directbytebuffer 對象的垃圾回收,實現(xiàn)堆外內(nèi)存的釋放 cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); att = null; }
上面最后一句代碼通過Cleaner.create()
來進行對象監(jiān)控,釋放堆外內(nèi)存。這里是如何做到的呢?跟蹤一下Cleaner類:
public class Cleaner extends PhantomReference<Object> { public static Cleaner create(Object var0, Runnable var1) { return var1 == null ? null : add(new Cleaner(var0, var1)); } }
可以看到繼承了PhantomReference
,Java中的4大引用類型我們都知道。PhantomReference的作用于其他的Refenrence作用大有不同。像 SoftReference、WeakReference都是為了保證引用的類對象能在不用的時候及時的被回收,但是 PhantomReference 并不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,對象不可達時就會被垃圾回收器回收,但是任何時候都無法通過虛引用獲得對象。虛引用主要用來跟蹤對象被垃圾回收器回收的活動。
那他的作用到底是啥呢?準確來說 PhantomReference 給使用者提供了一種機制-來監(jiān)控對象的垃圾回收的活動。
可能這樣說不是太明白,我來舉個例子:
package com.rickiyang.learn.javaagent; import java.lang.ref.PhantomReference; import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; import java.lang.reflect.Field; /** * @author rickiyang * @date 2019-08-08 * @Desc */ public class TestPhantomReference { public static boolean isRun = true; public static void main(String[] args) throws Exception { String str = new String("123"); System.out.println(str.getClass() + "@" + str.hashCode()); final ReferenceQueue<String> referenceQueue = new ReferenceQueue<>(); new Thread(() -> { while (isRun) { Object obj = referenceQueue.poll(); if (obj != null) { try { Field rereferent = Reference.class.getDeclaredField("referent"); rereferent.setAccessible(true); Object result = rereferent.get(obj); System.out.println("gc will collect:" + result.getClass() + "@" + result.hashCode() + "\t" + result); } catch (Exception e) { e.printStackTrace(); } } } }).start(); PhantomReference<String> weakRef = new PhantomReference<>(str, referenceQueue); str = null; Thread.currentThread().sleep(2000); System.gc(); Thread.currentThread().sleep(2000); isRun = false; } }
上面這段代碼的含義是new PhantomReference(),因為PhantomReference必須的維護一個ReferenceQueue用來保存當前被虛引用的對象。上例中手動去調(diào)用referenceQueue.poll()
方法,這里你需要注意的是并不是我們主動去釋放queue中的對象,你跟蹤進去 poll() 方法可以看到有一個全局鎖對象,只有當當前對象失去了引用之后才會釋放鎖,poll()方法才能執(zhí)行。在執(zhí)行poll()方法釋放對象的時候我們可以針對這個對象做一些監(jiān)控。這就是 PhantomReference 的意義所在。
說回到 Cleaner, 通過看源碼,create()
方法調(diào)用了add()
方法,在Cleaner類里面維護了一個雙向鏈表,將每一個add進來的Cleaner對象都添加到這個鏈表中維護。那么在Cleaner 鏈表中的對象實在何時被釋放掉呢?
注意到 Cleaner中有一個clean()方法:
public void clean() { if (remove(this)) { try { this.thunk.run(); } catch (final Throwable var2) { AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { if (System.err != null) { (new Error("Cleaner terminated abnormally", var2)).printStackTrace(); } System.exit(1); return null; } }); } } }
remove()方法是將該對象從內(nèi)部維護的雙向鏈表中清除。下面緊跟著是thunk.run()
,thunk = 我們通過create()
方法傳進來的參數(shù),在``DirectByteBuffer中那就是:
Cleaner.create(this, new Deallocator(base, size, cap))`,Deallocator類也是一個線程:
private static class Deallocator implements Runnable { private static Unsafe unsafe = Unsafe.getUnsafe(); //省略無關 代碼 public void run() { if (address == 0) { // Paranoia return; } unsafe.freeMemory(address); address = 0; Bits.unreserveMemory(size, capacity); } }
看到在run方法中調(diào)用了freeMemory()
去釋放掉對象。
在 Reference
類中調(diào)用了該方法,Reference 類中的靜態(tài)代碼塊 有個一內(nèi)部類:ReferenceHandler
,它繼承了 Thread,在run方法中調(diào)用了 tryHandlePending()
,并且被設置為守護線程,意味著會循環(huán)不斷的處理pending鏈表中的對象引用。
這里要注意的點是:
Cleaner本身不帶有清理邏輯,所有的邏輯都封裝在thunk中,因此thunk是怎么實現(xiàn)的才是最關鍵的。
另外,Java 最新核心技術系列教程和示例源碼看這里:https://github.com/javastacks/javastack
static { ThreadGroup tg = Thread.currentThread().getThreadGroup(); for (ThreadGroup tgn = tg; tgn != null; tg = tgn, tgn = tg.getParent()); Thread handler = new ReferenceHandler(tg, "Reference Handler"); /* If there were a special system-only priority greater than * MAX_PRIORITY, it would be used here */ handler.setPriority(Thread.MAX_PRIORITY); handler.setDaemon(true); handler.start(); // provide access in SharedSecrets SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() { @Override public boolean tryHandlePendingReference() { return tryHandlePending(false); } }); } static boolean tryHandlePending(boolean waitForNotify) { Reference<Object> r; Cleaner c; try { synchronized (lock) { if (pending != null) { r = pending; //如果當前Reference對象是Cleaner類型的就進行特殊處理 c = r instanceof Cleaner ? (Cleaner) r : null; // unlink 'r' from 'pending' chain pending = r.discovered; r.discovered = null; } else { // The waiting on the lock may cause an OutOfMemoryError // because it may try to allocate exception objects. if (waitForNotify) { lock.wait(); } // retry if waited return waitForNotify; } } } catch (OutOfMemoryError x) { Thread.yield(); // retry return true; } catch (InterruptedException x) { // retry return true; } // clean 不為空的時候,走清理的邏輯 if (c != null) { c.clean(); return true; } ReferenceQueue<? super Object> q = r.queue; if (q != ReferenceQueue.NULL) q.enqueue(r); return true; }
tryHandlePending
這段代碼的意思是:
如果一個對象經(jīng)過JVM檢測他已經(jīng)沒有強引用了,但是還有 弱引用 或者 軟引用 或者 虛引用的情況下,那么就會把此對象放到一個名為pending的鏈表里,這個鏈表是通過Reference.discovered域連接在一起的。
ReferenceHandler
這個線程會一直從鏈表中取出被pending的對象,它可能是WeakReference,也可能是SoftReference,當然也可能是PhantomReference和Cleaner。如果是Cleaner,那就直接調(diào)用Cleaner的clean方法,然后就結束了。其他的情況下,要交給這個對象所關聯(lián)的queue,以便于后續(xù)的處理。
關于堆外內(nèi)存分配和回收的代碼我們就先分析到這里。需要注意的是對外內(nèi)存回收的時機也是不確定的,所以不要持續(xù)分配一些大對象到堆外,如果沒有被回收掉,這是一件很可怕的事情。畢竟它無法被JVM檢測到。
內(nèi)存屏障
硬件層的內(nèi)存屏障分為兩種:Load Barrier
和 Store Barrier
即讀屏障和寫屏障。內(nèi)存屏障有兩個作用:阻止屏障兩側的指令重排序;強制把寫緩沖區(qū)/高速緩存中的臟數(shù)據(jù)等寫回主內(nèi)存,讓緩存中相應的數(shù)據(jù)失效。在Unsafe中提供了三個方法來操作內(nèi)存屏障:
//讀屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前 public native void loadFence(); //寫屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前 public native void storeFence(); //全能屏障,禁止load、store操作重排序 public native void fullFence();
先簡單了解兩個指令:
- Store:將處理器緩存的數(shù)據(jù)刷新到內(nèi)存中。
- Load:將內(nèi)存存儲的數(shù)據(jù)拷貝到處理器的緩存中。
JVM平臺提供了一下幾種內(nèi)存屏障:
屏障類型 | 指令示例 | 說明 |
---|---|---|
LoadLoad Barriers | Load1;LoadLoad;Load2 | 該屏障確保Load1數(shù)據(jù)的裝載先于Load2及其后所有裝載指令的的操作 |
StoreStore Barriers | Store1;StoreStore;Store2 | 該屏障確保Store1立刻刷新數(shù)據(jù)到內(nèi)存(使其對其他處理器可見)該操作先于Store2及其后所有存儲指令的操作 |
LoadStore Barriers | Load1;LoadStore;Store2 | 確保Load1的數(shù)據(jù)裝載先于Store2及其后所有的存儲指令刷新數(shù)據(jù)到內(nèi)存的操作 |
StoreLoad Barriers | Store1;StoreLoad;Load2 | 該屏障確保Store1立刻刷新數(shù)據(jù)到內(nèi)存的操作先于Load2及其后所有裝載裝載指令的操作。它會使該屏障之前的所有內(nèi)存訪問指令(存儲指令和訪問指令)完成之后,才執(zhí)行該屏障之后的內(nèi)存訪問指令 |
StoreLoad Barriers同時具備其他三個屏障的效果,因此也稱之為全能屏障
(mfence),是目前大多數(shù)處理器所支持的;但是相對其他屏障,該屏障的開銷相對昂貴。
loadFence
實現(xiàn)了LoadLoad Barriers,該操作禁止了指令的重排序。
storeFence
實現(xiàn)了 StoreStore Barriers,確保屏障前的寫操作能夠立刻刷入到主內(nèi)存,并且確保屏障前的寫操作一定先于屏障后的寫操作。即保證了內(nèi)存可見性和禁止指令重排序。
fullFence
實現(xiàn)了 StoreLoad Barriers,強制所有在mfence指令之前的store/load指令,都在該mfence指令執(zhí)行之前被執(zhí)行;所有在mfence指令之后的store/load指令,都在該mfence指令執(zhí)行之后被執(zhí)行。
在 JDK 中調(diào)用了 內(nèi)存屏障這幾個方法的實現(xiàn)類有 StampedLock
。關于StampedLock
的實現(xiàn)我們后面會專門抽出一篇去講解。它并沒有去實現(xiàn)AQS隊列。而是采用了 其他方式實現(xiàn)。
系統(tǒng)相關
這部分包含兩個獲取系統(tǒng)相關信息的方法。
//返回系統(tǒng)指針的大小。返回值為4(32位系統(tǒng))或 8(64位系統(tǒng))。 public native int addressSize(); //內(nèi)存頁的大小,此值為2的冪次方。 public native int pageSize();
在 java.nio
下的Bits類中調(diào)用了pagesize()方法計算系統(tǒng)中頁大?。?/code>
private static int pageSize = -1; static int pageSize() { if (pageSize == -1) pageSize = unsafe().pageSize(); return pageSize; }
線程調(diào)度
線程調(diào)度中提供的方法包括:線程的掛起,恢復 和 對象鎖機制等,其中獲取對象的監(jiān)視器鎖方法已經(jīng)被標記為棄用。
// 終止掛起的線程,恢復正常.java.util.concurrent包中掛起操作都是在LockSupport類實現(xiàn)的,其底層正是使用這兩個方法 public native void unpark(Object thread); // 線程調(diào)用該方法,線程將一直阻塞直到超時,或者是中斷條件出現(xiàn)。 public native void park(boolean isAbsolute, long time); //獲得對象鎖(可重入鎖) @Deprecated public native void monitorEnter(Object o); //釋放對象鎖 @Deprecated public native void monitorExit(Object o); //嘗試獲取對象鎖 @Deprecated public native boolean tryMonitorEnter(Object o);
將一個線程進行掛起是通過 park 方法實現(xiàn)的,調(diào)用park()
后,線程將一直 阻塞 直到 超時 或者 中斷 等條件出現(xiàn)。unpark
可以釋放一個被掛起的線程,使其恢復正常。整個并發(fā)框架中對線程的掛起操作被封裝在LockSupport
類中,LockSupport 類中有各種版本 pack 方法,但最終都調(diào)用了Unsafe.park()
方法。 我們來看一個例子:
package leetcode; import sun.misc.Unsafe; import java.lang.reflect.Field; import java.util.concurrent.TimeUnit; /** * @author: rickiyang * @date: 2019/8/10 * @description: */ public class TestUsafe { private static Thread mainThread; public Unsafe getUnsafe() throws Exception { Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafeField.setAccessible(true); return (Unsafe) theUnsafeField.get(null); } public void testPark() throws Exception { Unsafe unsafe = getUnsafe(); mainThread = Thread.currentThread(); System.out.println(String.format("park %s", mainThread.getName())); unsafe.park(false, TimeUnit.SECONDS.toNanos(3)); new Thread(() -> { System.out.println(String.format("%s unpark %s", Thread.currentThread().getName(), mainThread.getName())); unsafe.unpark(mainThread); }).start(); System.out.println("main thread is done"); } public static void main(String[] args) throws Exception { TestUsafe testUsafe = new TestUsafe(); testUsafe.testPark(); } }
運行上面的例子,那你會發(fā)現(xiàn)在第29行 park
方法設置了超時時間為3秒后,會阻塞當前主線程,直到超時時間到達,下面的代碼才會繼續(xù)執(zhí)行。
對象操作
Unsafe類中提供了多個方法來進行 對象實例化 和 獲取對象的偏移地址 的操作:
// 傳入一個Class對象并創(chuàng)建該實例對象,但不會調(diào)用構造方法 public native Object allocateInstance(Class<?> cls) throws InstantiationException; // 獲取字段f在實例對象中的偏移量 public native long objectFieldOffset(Field f); // 返回值就是f.getDeclaringClass() public native Object staticFieldBase(Field f); // 靜態(tài)屬性的偏移量,用于在對應的Class對象中讀寫靜態(tài)屬性 public native long staticFieldOffset(Field f); // 獲得給定對象偏移量上的int值,所謂的偏移量可以簡單理解為指針指向該變量;的內(nèi)存地址, // 通過偏移量便可得到該對象的變量,進行各種操作 public native int getInt(Object o, long offset); // 設置給定對象上偏移量的int值 public native void putInt(Object o, long offset, int x); // 獲得給定對象偏移量上的引用類型的值 public native Object getObject(Object o, long offset); // 設置給定對象偏移量上的引用類型的值 public native void putObject(Object o, long offset, Object x);); // 設置給定對象的int值,使用volatile語義,即設置后立馬更新到內(nèi)存對其他線程可見 public native void putIntVolatile(Object o, long offset, int x); // 獲得給定對象的指定偏移量offset的int值,使用volatile語義,總能獲取到最新的int值。 public native int getIntVolatile(Object o, long offset); // 與putIntVolatile一樣,但要求被操作字段必須有volatile修飾 public native void putOrderedInt(Object o, long offset, int x);
allocateInstance
方法在這幾個場景下很有用:跳過對象的實例化階段(通過構造函數(shù))、忽略構造函數(shù)的安全檢查(反射newInstance()時)、你需要某類的實例但該類沒有public的構造函數(shù)。
另外,Java 最新核心技術系列教程和示例源碼看這里:https://github.com/javastacks/javastack
舉個例子:
public class User { private String name; private int age; private static String address = "beijing"; public User(){ name = "xiaoming"; } public String getname(){ return name; } } /** * 實例化對象 * @throws Exception */ public void newInstance() throws Exception{ TestUsafe testUsafe = new TestUsafe(); Unsafe unsafe = testUsafe.getUnsafe(); User user = new User(); System.out.println(user.getname()); User user1 = User.class.newInstance(); System.out.println(user1.getname()); User o = (User)unsafe.allocateInstance(User.class); System.out.println(o.getname()); }
打印的結果可以看到最后輸出的是null,說明構造函數(shù)未被加載??梢赃M一步實驗,將User類中的構造函數(shù)設置為 private,你會發(fā)現(xiàn)在前面兩種實例化方式檢查期就報錯。但是第三種是可以用的。這是因為allocateInstance
只是給對象分配了內(nèi)存,它并不會初始化對象中的屬性。
下面是對象操作的使用示例:
public void testObject() throws Exception{ TestUsafe testUsafe = new TestUsafe(); Unsafe unsafe = testUsafe.getUnsafe(); //通過allocateInstance創(chuàng)建對象,為其分配內(nèi)存地址,不會加載構造函數(shù) User user = (User) unsafe.allocateInstance(User.class); System.out.println(user); // Class && Field Class<? extends User> userClass = user.getClass(); Field name = userClass.getDeclaredField("name"); Field age = userClass.getDeclaredField("age"); Field location = userClass.getDeclaredField("address"); // 獲取實例域name和age在對象內(nèi)存中的偏移量并設置值 System.out.println(unsafe.objectFieldOffset(name)); unsafe.putObject(user, unsafe.objectFieldOffset(name), "xiaoming"); System.out.println(unsafe.objectFieldOffset(age)); unsafe.putInt(user, unsafe.objectFieldOffset(age), 18); System.out.println(user); // 獲取定義location字段的類 Object staticFieldBase = unsafe.staticFieldBase(location); System.out.println(staticFieldBase); // 獲取static變量address的偏移量 long staticFieldOffset = unsafe.staticFieldOffset(location); // 獲取static變量address的值 System.out.println(unsafe.getObject(staticFieldBase, staticFieldOffset)); // 設置static變量address的值 unsafe.putObject(staticFieldBase, staticFieldOffset, "tianjin"); System.out.println(user + " " + user.getAddress()); }
對象實例布局與內(nèi)存大小
一個Java對象占用多大的內(nèi)存空間呢?這個問題很值得讀者朋友去查一下。 因為這個輸出本篇的重點所以簡單說一下。一個 Java 對象在內(nèi)存中由對象頭、示例數(shù)據(jù)和對齊填充構成。對象頭存儲了對象運行時的基本數(shù)據(jù),如 hashCode、鎖狀態(tài)、GC 分代年齡、類型指針等等。實例數(shù)據(jù)是對象中的非靜態(tài)字段值,可能是一個原始類型的值,也可能是一個指向其他對象的指針。對齊填充就是 padding,保證對象都采用 8 字節(jié)對齊。除此以外,在 64 位虛擬機中還可能會開啟指針壓縮,將 8 字節(jié)的指針壓縮為 4 字節(jié),這里就不再過多介紹了。
也就是說一個 Java 對象在內(nèi)存中,首先是對象頭,然后是各個類中字段的排列,這之間可能會有 padding 填充。這樣我們大概就能理解字段偏移量的含義了,它實際就是每個字段在內(nèi)存中所處的位置。
public class User { private String name; private int age; } TestUsafe testUsafe = new TestUsafe(); Unsafe unsafe = testUsafe.getUnsafe(); for (Field field : User.class.getDeclaredFields()) { System.out.println(field.getName() + "-" + field.getType() + ": " + unsafe.objectFieldOffset(field)); } 結果: name-class java.lang.String: 16 age-int: 12
從上面的運行結果中可以:
age:偏移值為12,即前面 12 個字節(jié)的對象頭;
name:name從16字節(jié)開始,因為int 類型的age占了4個字節(jié)。
繼續(xù)算下去整個對象占用的空間,對象頭12,age 4,name 是指針類型,開啟指針壓縮占用4個字節(jié),那么User對象整個占用20字節(jié),因為上面說的padding填充,必須8字節(jié)對齊,那么實際上會補上4個字節(jié)的填充,即一共占用了24個字節(jié)。
按照這種計算方式,我們可以字節(jié)寫一個計算size的工具類:
public static long sizeOf(Object o) throws Exception{ TestUsafe testUsafe = new TestUsafe(); Unsafe unsafe = testUsafe.getUnsafe(); HashSet<Field> fields = new HashSet<Field>(); 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(); } //計算每個字段的偏移量,因為第一個字段的偏移量即在對象頭的基礎上偏移的 //所以只需要比較當前偏移量最大的字段即表示這是該對象最后一個字段的位置 long maxSize = 0; for (Field f : fields) { long offset = unsafe.objectFieldOffset(f); if (offset > maxSize) { maxSize = offset; } } //上面計算的是對象最后一個字段的偏移量起始位置,java中對象最大長度是8個字節(jié)(long) //這里的計算方式是 將 當前偏移量 / 8 + 8字節(jié) 的padding return ((maxSize/8) + 1) * 8; }
上面的工具類計算的結果也是24。
class相關操作
//靜態(tài)屬性的偏移量,用于在對應的Class對象中讀寫靜態(tài)屬性 public native long staticFieldOffset(Field f); //獲取一個靜態(tài)字段的對象指針 public native Object staticFieldBase(Field f); //判斷是否需要初始化一個類,通常在獲取一個類的靜態(tài)屬性的時候(因為一個類如果沒初始化,它的靜態(tài)屬性也不會初始化)使用。 當且僅當ensureClassInitialized方法不生效時返回false public native boolean shouldBeInitialized(Class<?> c); //確保類被初始化 public native void ensureClassInitialized(Class<?> c); //定義一個類,可用于動態(tài)創(chuàng)建類,此方法會跳過JVM的所有安全檢查,默認情況下,ClassLoader(類加載器)和ProtectionDomain(保護域)實例來源于調(diào)用者 public native Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain); //定義一個匿名類,可用于動態(tài)創(chuàng)建類 public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);
數(shù)組操作
數(shù)組操作主要有兩個方法:
//返回數(shù)組中第一個元素的偏移地址 public native int arrayBaseOffset(Class<?> arrayClass); //返回數(shù)組中一個元素占用的大小 public native int arrayIndexScale(Class<?> arrayClass);
CAS操作
相信所有的開發(fā)者對這個詞都不陌生,在AQS類中使用了無鎖的方式來進行并發(fā)控制,主要就是CAS的功勞。
CAS的全稱是Compare And Swap 即比較交換,其算法核心思想如下
執(zhí)行函數(shù):CAS(V,E,N)
包含3個參數(shù)
- V表示要更新的變量
- E表示預期值
- N表示新值
如果V值等于E值,則將V的值設為N。若V值和E值不同,則說明已經(jīng)有其他線程做了更新,則當前線程什么都不做。通俗的理解就是CAS操作需要我們提供一個期望值,當期望值與當前線程的變量值相同時,說明沒有別的線程修改該值,當前線程可以進行修改,也就是執(zhí)行CAS操作,但如果期望值與當前線程不符,則說明該值已被其他線程修改,此時不執(zhí)行更新操作,但可以選擇重新讀取該變量再嘗試再次修改該變量,也可以放棄操作。
Unsafe類中提供了三個方法來進行CAS操作:
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update); public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update); public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);
另外,在 JDK1.8中新增了幾個 CAS 的方法,他們的實現(xiàn)是基于上面三個方法做的一層封裝:
//1.8新增,給定對象o,根據(jù)獲取內(nèi)存偏移量指向的字段,將其增加delta, //這是一個CAS操作過程,直到設置成功方能退出循環(huán),返回舊值 public final int getAndAddInt(Object o, long offset, int delta) { int v; do { //獲取內(nèi)存中最新值 v = getIntVolatile(o, offset); //通過CAS操作 } while (!compareAndSwapInt(o, offset, v, v + delta)); return v; } //1.8新增,方法作用同上,只不過這里操作的long類型數(shù)據(jù) public final long getAndAddLong(Object o, long offset, long delta) { long v; do { v = getLongVolatile(o, offset); } while (!compareAndSwapLong(o, offset, v, v + delta)); return v; } //1.8新增,給定對象o,根據(jù)獲取內(nèi)存偏移量對于字段,將其 設置為新值newValue, //這是一個CAS操作過程,直到設置成功方能退出循環(huán),返回舊值 public final int getAndSetInt(Object o, long offset, int newValue) { int v; do { v = getIntVolatile(o, offset); } while (!compareAndSwapInt(o, offset, v, newValue)); return v; } // 1.8新增,同上,操作的是long類型 public final long getAndSetLong(Object o, long offset, long newValue) { long v; do { v = getLongVolatile(o, offset); } while (!compareAndSwapLong(o, offset, v, newValue)); return v; } //1.8新增,同上,操作的是引用類型數(shù)據(jù) public final Object getAndSetObject(Object o, long offset, Object newValue) { Object v; do { v = getObjectVolatile(o, offset); } while (!compareAndSwapObject(o, offset, v, newValue)); return v; }
CAS在java.util.concurrent.atomic相關類、Java AQS、CurrentHashMap等實現(xiàn)上有非常廣泛的應用。
到此這篇關于Java 中的 Unsafe 魔法類的作用大全的文章就介紹到這了,更多相關Java nsafe 魔法類內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
詳解Eclipse Validating緩慢的優(yōu)化
這篇文章主要介紹了詳解Eclipse Validating緩慢的優(yōu)化,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-03-03測試springboot項目出現(xiàn)Test Ignored的解決
這篇文章主要介紹了測試springboot項目出現(xiàn)Test Ignored的解決,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11JDK1.8下載、安裝和環(huán)境配置超詳細教程(最新最完整)
jdk1.8是一款功能強大的Java語音軟件開發(fā)工具包,JDK是學好Java的第一步,本文重點給大家介紹JDK1.8下載、安裝和環(huán)境配置教程,需要的朋友可以參考下2022-11-11SpringBoot 集成 Jasypt 對數(shù)據(jù)庫加密以及踩坑的記錄分享
這篇文章主要介紹了SpringBoot 集成 Jasypt 對數(shù)據(jù)庫加密以及踩坑,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-08-08