Java后端|Unsafe類及其應(yīng)用
本文概述Java Unsafe類,并舉例說明其應(yīng)用場景,快速瀏覽下即可
閱讀了美團2019技術(shù)年貨,有一篇文章是對Java魔法類——Unsafe的講解。文章不錯,在此結(jié)合源碼作一個總結(jié),并添加個人的一些理解和學(xué)習(xí)文章資源。
原文鏈接:美團點評 2019 技術(shù)年貨 P2
目錄
- Unsafe類簡介
- Unsafe類使用
- Unsafe類應(yīng)用
Unsafe類簡介
Java作為一種面向?qū)ο缶幊陶Z言,相對于C++,其具有的自動垃圾回收機制大大降低了編程的復(fù)雜度,但同時導(dǎo)致性能較低、空間占用大等問題。
為解決上述問題,Java提供了Unsafe類(位于sun.misc包)。該類提供了一些底層原生方法,可直接訪問系統(tǒng)內(nèi)存資源、自主管理內(nèi)存資源等。通過該類的方法,可以彌補Java這一上層語言的不足,提升程序運行效率以及對系統(tǒng)資源的管控能力。有很多Java工具包和框架都使用了Unsafe類,如java.nio包、java.util.concurrent包、Netty、Kafka、Hadoop等。
但Unsafe類也是一把雙刃劍。比如內(nèi)存分配及回收操作(類似C語言指針),對于適應(yīng)了JVM自動管理內(nèi)存的Java程序員來說,很容易出現(xiàn)內(nèi)存泄漏等問題。因此要對Unsafe抱有敬畏之心。
查看Unsafe類源碼中定義的方法,可知其主要功能,如下圖:
下面首先介紹如何使用Unsafe類。
Unsafe類使用
Unsafe類的方法基本都是實例方法,因此需獲取unsafe實例。
查看Unsafe類的源碼可知,Unsafe類是餓漢式單例模式的設(shè)計,通過靜態(tài)代碼塊對單例對象theUnsafe進行實例化,通過getUnsafe靜態(tài)方法獲取實例。
public final class Unsafe { // 單例對象 private static final Unsafe theUnsafe; static { registerNatives(); Reflection.registerMethodsToFilter(Unsafe.class, new String[]{"getUnsafe"}); // 創(chuàng)建實例 theUnsafe = new Unsafe(); } private Unsafe() { } @CallerSensitive public static Unsafe getUnsafe() { ... } ... }
因此想要獲取Unsafe對象,有如下兩種方式:
1. 調(diào)用Unsafe方法
閱讀源碼,發(fā)現(xiàn)調(diào)用getUnsafe方法的類必須是被BootstrapClassLoader加載的,否則會拋出異常!
@CallerSensitive public static Unsafe getUnsafe() { Class var0 = Reflection.getCallerClass(); if (!VM.isSystemDomainLoader(var0.getClassLoader())) { throw new SecurityException("Unsafe"); } else { return theUnsafe; } }
可通過Java cmd命令的-Xbootclasspath/a參數(shù)把調(diào)用Unsafe方法的類所在jar包路徑追加到默認的Bootstrap路徑中,使得該類可被BootstrapClassLoader加載,從而獲取Unsafe實例。
java -Xbootclasspath/a: ${path} // path為調(diào)用Unsafe方法的類所在jar包路徑
2. 反射
Java反射機制能夠動態(tài)生成對象和獲取、調(diào)用任意類的靜態(tài)屬性及方法。
可以使用反射獲取unsafe實例:
try { Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); return (Unsafe) field.get(null); } catch (Exception e) { return null; }
獲取到unsafe實例后,便可以愉快地使用其方法了。
Unsafe類應(yīng)用
堆外內(nèi)存操作
通常Java中新建的對象存儲在JVM堆內(nèi)存中,受配置限制,由GC自動回收。
而Unsafe類提供JVM管轄外的堆外內(nèi)存(由操作系統(tǒng)管理)的操作,包括內(nèi)存分配、拷貝、釋放、給定地址值操作等。
為什么要使用堆外內(nèi)存?
通過使用堆外內(nèi)存,減少JVM堆內(nèi)內(nèi)存占用,從而減少垃圾回收停頓對于應(yīng)用性能的影響。
提升程序 I/O 操作的性能。通常在 I/O 通信過程中,存在堆內(nèi)內(nèi)存到堆外內(nèi)存的數(shù)據(jù)拷貝。因此,對于需要頻繁進行內(nèi)存間數(shù)據(jù)拷貝且生命周期較短的暫存數(shù)據(jù),都建議存儲到堆外內(nèi)存,節(jié)約數(shù)據(jù)拷貝的時間開銷。
源碼
// 分配內(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); // 設(shè)置指定內(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);
應(yīng)用
1. DirectByteBuffer類
DirectByteBuffer類是 Java 用于實現(xiàn)堆外內(nèi)存的一個重要類,通常用在通信過程中做緩沖池,在 Netty、MINA 等 NIO 框架中應(yīng)用廣泛。
其對于堆外內(nèi)存的創(chuàng)建、使用、銷毀等邏輯均由Unsafe提供的堆外內(nèi)存 API 來實現(xiàn),如下:
創(chuàng)建DirectByteBuffer時,通過unsafe.allocateMemory分配內(nèi)存,并通過unsafe.setMemory進行內(nèi)存初始化
構(gòu)建Cleaner對象(繼承了PhantomReference,為虛引用)用于跟蹤 DirectByteBuffer 對象的垃圾回收,以實現(xiàn)當DirectByteBuffer被垃圾回收時,分配的堆外內(nèi)存一起被釋放。
DirectByteBuffer(int cap) { ... cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); }
當DirectByteBuffer僅被Cleaner引用(即為虛引用)時,其可以在任意GC時段被回收。當DirectByteBuffer實例對象被回收時,在ReferenceHandler線程操作中,會調(diào)用Cleaner的clean方法根據(jù)創(chuàng)建Cleaner時傳入的Deallocator來進行堆外內(nèi)存的釋放。
Deallocator實現(xiàn)了Runnable接口,在run方法中調(diào)用了unsafe.freeMemory(address)方法釋放了堆外內(nèi)存,防止內(nèi)存泄漏。
深入了解可以看這篇文章:深入理解DirectByteBuffer
CAS
CAS即比較并替換(compare and swap),是實現(xiàn)并發(fā)、鎖機制時常用的技術(shù)。
CAS操作包含三個操作數(shù):內(nèi)存位置、預(yù)期原值及新值。執(zhí)行 CAS 操作時,先定位到指定位置的內(nèi)存,將該內(nèi)存的值與預(yù)期原值比較,若匹配,CPU會將該內(nèi)存位置的值更新為新值,否則,CPU不做任何操作。
CAS底層是基于一條CPU的原子指令(cmpxchg 指令)實現(xiàn),是原子操作,再并發(fā)時能夠保證數(shù)據(jù)一致性。
源碼
Unsafe類提供了三種類型的CAS,包括對象、整型和長整形,使用簡單:
/** * CAS * @param o 包含要修改field的對象 * @param offset 對象中某field的偏移量 * @param expected 期望值 * @param update 更新值 * @return true | false */ 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);
應(yīng)用
CAS在并發(fā)編程中應(yīng)用廣泛,比如Concurrent并發(fā)包中的原子類和同步器。
1. Atomic原子類
以AtomicInteger為例,其內(nèi)部更新值的方法均基于unsafe的CAS實現(xiàn),代碼如下:
public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
在該Atomic類初始化時,會通過靜態(tài)代碼塊調(diào)用unsafe.objectFieldOffset來獲取該字段相對于Atomic類的地址偏移值,并賦值給valueOffset靜態(tài)屬性,用作上述CAS的參數(shù),代碼如下:
private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } }
2. AQS
AQS即AbstractQueuedSynchronizer(等待隊列同步器),是ReentrantLock(可重入鎖)等實現(xiàn)的關(guān)鍵技術(shù),其內(nèi)部維護了一個volatile關(guān)鍵字修飾的state字段表示同步狀態(tài),通過CAS實現(xiàn)了對該字段的原子更新。部分源碼如下:
/** * The synchronization state. */ private volatile int state; protected final int getState() { return state; } protected final void setState(int newState) { state = newState; } /** * Atomically sets synchronization state to the given updated * value if the current state value equals the expected value. * This operation has memory semantics of a {@code volatile} read * and write. * * @param expect the expected value * @param update the new value * @return {@code true} if successful. False return indicates that the actual * value was not equal to the expected value. */ protected final boolean compareAndSetState(int expect, int update) { // See below for intrinsics setup to support this return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }
深入了解可以看這篇文章:Java AQS詳解
線程調(diào)度
Unsafe提供了線程的掛起/恢復(fù)、鎖的獲取/釋放操作。
源碼
// 阻塞線程 public native void park(boolean isAbsolute, long time); // 取消阻塞線程 public native void unpark(Object thread); // 獲得對象鎖(可重入鎖) @Deprecated public native void monitorEnter(Object o); // 釋放對象鎖 @Deprecated public native void monitorExit(Object o); // 嘗試獲取對象鎖 @Deprecated public native boolean tryMonitorEnter(Object o);
應(yīng)用
1. AQS
AQS中,對于鎖的操作是調(diào)用了LockSupport的相關(guān)方法實現(xiàn),比如park、unpark等,而這些方法底層是調(diào)用了unsafe類的線程調(diào)度方法實現(xiàn),源碼如下:
// 線程掛起 public static void park(Object blocker) { Thread t = Thread.currentThread(); setBlocker(t, blocker); UNSAFE.park(false, 0L); setBlocker(t, null); } // 線程恢復(fù) public static void unpark(Thread thread) { if (thread != null) UNSAFE.unpark(thread); }
Class相關(guān)
此部分主要提供 Class 和它的靜態(tài)字段的操作相關(guān)方法,包含靜態(tài)字段內(nèi)存定位、定義類、定義匿名類、檢驗 & 確保初始化等。
// 獲取給定靜態(tài)字段的內(nèi)存地址偏移量,這個值對于給定的字段是唯一且固定不變的 public native long static FieldOffset(Field f); // 獲取一個靜態(tài)類中給定字段的對象 public native Object static FieldBase(Field f); // 判斷是否需要初始化一個類,通常在獲取一個類的靜態(tài)屬性的時候(因為一個類如果沒初始化,它的靜態(tài)屬性也不會初始化)使用。當且僅當ensureClassInitialized方法不生效時返回false。 public native booleanshouldBeInitialized(Class<?> c); // 檢測給定類是否已初始化。通常在獲取一個類的靜態(tài)屬性的時候(因為一個類如果沒初始化,它的靜態(tài)屬性也不會初始化)使用。 public native void ensureClassInitialized(Class<?> c); // 定義一個類,此方***跳過JVM的所有安全檢查,默認情況下,ClassLoader(類加載器)和ProtectionDomain(保護域)實例來源于調(diào)用者 public native Class<?> defineClass(String name, byte[] b, int off, intlen, ClassLoader loader, ProtectionDomain protectionDomain); // 定義一個匿名類 public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches)
應(yīng)用
1. Java8 Lambda表達式
Java8的Lambda表達式基于虛擬機指令invokedynamic及VM Anonymous Class機制實現(xiàn)。
invokedynamic
invokedynamic是Java7為了實現(xiàn)在 JVM 上運行動態(tài)語言而引入的一條新的虛擬機指令,它可以實現(xiàn)在運行期動態(tài)解析出調(diào)用點限定符所引用的方法,然后再執(zhí)行該方法,invokedynamic指令的分派邏輯是由用戶設(shè)定的引導(dǎo)方法決定。
VM Anonymous Class
可看做一種模板機制,針對于程序動態(tài)生成很多結(jié)構(gòu)相同、僅若干常量不同的類時,可以先創(chuàng)建包含常量占位符的模板類。而后通過Unsafe.defineAnonymousClass方法定義具體類時填充模板的占位符生成具體的匿名類。
由于生成的匿名類不被任何ClassLoader加載,因此只要當該類沒有存在的實例對象、且沒有強引用來引用該類的 Class對象時,該類就會被 GC 回收。相比于Java語言層面的匿名內(nèi)部類,節(jié)約了通過ClassLoader進行類加載的開銷且更易回收。
Lamda表達式實現(xiàn):
首先,通過invokedynamic指令調(diào)用引導(dǎo)方法生成調(diào)用點,在此過程中,會通過ASM動態(tài)生成字節(jié)碼,而后利用 Unsafe.defineAnonymousClass方法定義實現(xiàn)函數(shù)式接口的匿名類,并實例化此匿名類,并返回與此匿名類中函數(shù)式方法的方法句柄關(guān)聯(lián)的調(diào)用點;而后可以通過此調(diào)用點實現(xiàn)調(diào)用相應(yīng)Lambda表達式定義邏輯的功能。
詳細可閱讀這篇文章:由淺入深學(xué)習(xí)java8的Lambda原理
對象操作
Unsafe類提供了操作對象成員屬性及非常規(guī)的對象實例化方法。
先了解下對象實例化的兩種方式:
常規(guī)對象實例化方式
本質(zhì)是通過new機制來實現(xiàn)對象的創(chuàng)建。new機制的特點是必須提供構(gòu)造函數(shù)且傳入指定數(shù)量的參數(shù),存在一定局限性。
非常規(guī)的實例化方式
使用Unsafe的allocateInstance 方法,僅通過Class對象就可以創(chuàng)建此類的實例對象(類似反射,但反射無法繞過構(gòu)造方法),而且不需要調(diào)用其構(gòu)造方法、初始化代碼、JVM安全檢查等。它抑制修飾符檢測,即使構(gòu)造器是private修飾的也能通過此方法實例化,只需提Class對象即可創(chuàng)建相應(yīng)的對象。靈活性高,得以廣泛應(yīng)用。
源碼
// 返回對象成員屬性在內(nèi)存地址相對于此對象的內(nèi)存地址的偏移量 public native long objectFieldOffset(Field f); // 獲取指定對象偏移地址的值,忽略修飾限定符的訪問限制,與此類似操作還有: getInt,getDouble,getLong,getChar等 public native Object getObject(Object o, long offset); // 為指定對象的偏移地址設(shè)置值,忽略修飾限定符的訪問限制,與此類似操作還有: putInt,putDouble,putLong,putChar等 public native void putObject(Object o, long offset, Object x); // 從對象的指定偏移量處獲取變量的引用,使用volatile的加載語義 public native Object getObjectVolatile(Object o, long offset); // 存儲變量的引用到對象的指定的偏移量處,使用volatile的存儲語義 public native void putObjectVolatile(Object o, long offset, Object x); // 有序、延遲版本的putObjectVolatile方法,不保證值的改變被其他線程立即看到。只有在field被volatile修飾符修飾時有效 public native void putOrderedObject(Object o, long offset, Object x); // 繞過構(gòu)造方法、初始化代碼來創(chuàng)建對象(非常規(guī)實例化) public native Object allocateInstance(Class<?> cls) throwsInstantiationException;
應(yīng)用
GSON
GSON是json對象序列化框架,實現(xiàn)了對json字符串與java對象的互相轉(zhuǎn)換。
將json反序列化為java對象時,如果類有默認構(gòu)造函數(shù)或是接口,則通過反射生成實例,否則通過UnsafeAllocator以非常規(guī)方式實例化對象。流程源碼如下:
// 有默認構(gòu)造函數(shù) ObjectConstructor<T> defaultConstructor = newDefaultConstructor(rawType); if (defaultConstructor != null) { return defaultConstructor; } // 是接口,獲取默認接口實現(xiàn)類 ObjectConstructor<T> defaultImplementation = newDefaultImplementationConstructor(type, rawType); if (defaultImplementation != null) { return defaultImplementation; } // 否則使用unsafe生成實例 return newUnsafeAllocator(type, rawType);
newUnsafeAllocator中,先調(diào)用UnsafeAllocator.create創(chuàng)建了實現(xiàn)unsafeAllocator.newInstance抽象方法的UnsafeAllocator,該方法通過unsafe.allocateInstance非常規(guī)地生成對象實例。而后調(diào)用unsafeAllocator.newInstance方法即可生成實例,源碼如下:
public static UnsafeAllocator create() { // try JVM 獲取Unsafe的allocateInstance方法 // public class Unsafe { // public Object allocateInstance(Class<?> type); // } try { Class<?> unsafeClass = Class.forName("sun.misc.Unsafe"); Field f = unsafeClass.getDeclaredField("theUnsafe"); f.setAccessible(true); final Object unsafe = f.get(null); final Method allocateInstance = unsafeClass.getMethod("allocateInstance", Class.class); return new UnsafeAllocator() { @Override @SuppressWarnings("unchecked") public <T> T newInstance(Class<T> c) throws Exception { assertInstantiable(c); return (T) allocateInstance.invoke(unsafe, c); } }; }
深入了解請看這里:Gson源碼分析
數(shù)組相關(guān)
Unsafe類不提供對數(shù)組的修改操作,只有arrayBaseOffset與arrayIndexScale兩個方法。通常兩者配合使用,可定位數(shù)組中每個元素在內(nèi)存中的位置。
源碼
// 返回數(shù)組的基地址(第一個元素的偏移地址) public native int arrayBaseOffset(Class<?> arrayClass); // 返回數(shù)組中每個元素占用的大小 public native int arrayIndexScale(Class<?> arrayClass);
應(yīng)用
數(shù)組中第N個元素的位置公式為:
valueOffset = baseOffset + (scale * N);
可以使用valueOffset及Unsafe的其他方法來獲取數(shù)組元素或更新數(shù)組的值
// CAS更新數(shù)組指定下標元素值 long[] longArray = new long[15]; unsafe.compareAndSwapLong(longArray, valueOffset, expectedValue, newValue); // 獲取數(shù)組指定元素值 String[] stringArray = new String[]{"aaa", "bbb", "ccc"}; String str = (String) unsafe.getObject(stringArray, valueOffset);
系統(tǒng)相關(guān)
Unsafe類提供獲取系統(tǒng)信息的方法,包括獲取系統(tǒng)指針大小、內(nèi)存頁大小、負載情況。
源碼
// 返回系統(tǒng)指針的大小。返回值為4(32位系統(tǒng))或 8(64位系統(tǒng)) public native int addressSize(); // 內(nèi)存頁的大小,此值為2的冪次方。 public native int pageSize(); // 獲取系統(tǒng)的平均負載值 public native int getLoadAverage(double[] loadAvg, int nelems);
第三個方法中,loadAvg這個double數(shù)組參數(shù)將存放負載值的結(jié)果。nelems參數(shù)決定樣本數(shù)量,nelems只能取值為1到3,分別代表最近1、5、15分鐘內(nèi)系統(tǒng)的平均負載。如果無法獲取系統(tǒng)的負載,此方法返回-1,否則返回獲取到的樣本數(shù)量(即loadAvg中有效的元素個數(shù))。該方法并不常用,可使用JMX中的相關(guān)方法來替代此方法。
應(yīng)用
1. java.nio.Bits類
Bits是java.nio包中的工具類,具有默認的包訪問權(quán)限,不對外暴露。
其中,pageCount是計算待申請內(nèi)存所需內(nèi)存頁數(shù)量的靜態(tài)方法,其依賴Unsafe類的pageSize方法獲取系統(tǒng)內(nèi)存頁大小,以計算總頁數(shù),源碼如下:
private static int pageSize = -1; // 獲取單內(nèi)存頁面大小 static int pageSize() { if (pageSize == -1) pageSize = unsafe().pageSize(); return pageSize; } // 獲取內(nèi)存頁總頁數(shù) static int pageCount(long size) { return (int)(size + (long)pageSize() - 1L) / pageSize(); }
copySwapMemory方法用于將所有元素從一塊內(nèi)存復(fù)制到另一塊內(nèi)存,其中調(diào)用了unsafe.addressSize()方法獲取系統(tǒng)位數(shù),并對32位系統(tǒng)進行特殊處理,源碼如下:
private static void copySwapMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes, long elemSize) { // Sanity check size and offsets on 32-bit platforms. Most // significant 32 bits must be zero. if (unsafe.addressSize() == 4 && (bytes >>> 32 != 0 || srcOffset >>> 32 != 0 || destOffset >>> 32 != 0)) { throw new IllegalArgumentException(); } }
Bits類還大量調(diào)用了Unsafe類的其他方法,如arrayBaseOffset、copyMemory等,有興趣的讀者可閱讀源碼自行研究。
內(nèi)存屏障
Unsafe類在Java 8中引入了一套用于定義內(nèi)存屏障的方法,能夠避免指令重排序。
源碼
// 內(nèi)存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前 public native void loadFence(); // 內(nèi)存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前 public native void storeFence(); // 內(nèi)存屏障,禁止load、store操作重排序 public native void fullFence();
應(yīng)用
1. StampedLock
Java8的StampedLock類對讀寫鎖進行了改進。它的思想是讀寫鎖中讀不僅不阻塞讀,同時也不應(yīng)該阻塞寫。在讀的時候如果發(fā)生了寫,則應(yīng)當重讀而不是在讀的時候直接阻塞寫。因為在讀線程非常多而寫線程比較少的情況下,如果讀線程阻塞寫線程,寫線程可能發(fā)生饑餓現(xiàn)象。當讀執(zhí)行的時候另一個線程執(zhí)行了寫,則讀線程發(fā)現(xiàn)數(shù)據(jù)不一致則執(zhí)行重讀即可。
因此讀寫都存在時,使用StampedLock可保證讀寫線程之間不會互相阻塞,但寫線程間仍存在阻塞。
由于 StampedLock 提供的樂觀讀鎖不阻塞寫線程獲取鎖,因此當線程共享變量從主內(nèi)存load到線程工作內(nèi)存時,會存在數(shù)據(jù)不一致問題。所以當使用StampedLock的樂觀讀鎖時,遵循下述流程保障數(shù)據(jù)一致性。
第③步校驗鎖狀態(tài)操作至關(guān)重要,需要判斷鎖狀態(tài)是否發(fā)生改變,從而判斷之前 copy 到線程工作內(nèi)存中的值是否與主內(nèi)存的值存在不一致。
StampedLock.validate方法中,通過鎖標記與相關(guān)常量進行位運算、比較來校驗鎖狀態(tài),在校驗邏輯之前,會通過Unsafe的loadFence方法加入一個load內(nèi)存屏障,目的是避免上圖步驟②和StampedLock.validate中鎖狀態(tài)校驗運算發(fā)生重排序?qū)е骆i狀態(tài)校驗不準確的問題。源碼如下:
public boolean validate(long stamp) { U.loadFence(); return (stamp & SBITS) == (state & SBITS); }
最后
Unsafe類以多種方式應(yīng)用于Java底層庫中,涉及大量底層知識,要想正確使用并掌握它還是任重而道遠啊。
#Java工程師#