掃描下方二維碼或者微信搜索公衆號菜鳥飛呀飛,便可關注微信公衆號,閱讀更多
Spring源碼分析
和Java併發編程
文章。java
在上一篇文章《初始CAS的實現原理》中,提到了Unsafe類相關方法,今天這篇文章將詳細介紹Unsafe類的源碼。 爲何要單獨用一篇文章介紹Unsafe類呢?這是由於在看源碼過程當中,常常會碰到它,例如JUC包下的原子類、AQS、Netty等源碼中,最終都會看見Unsafe類的使用。搞清楚Unsafe類的使用,對之後看源碼會有很大的幫助。程序員
rt.jar
中sun.misc
包下的類,從類名就能看出來,這個類是不安全的,可是它的功能十分強大。相比C和C++的開發人員,做爲一名Java開發人員是十分幸福的,由於在Java中程序員在開發時不須要關注內存的管理,對象的回收,由於JVM所有都幫助咱們完成了。若是Java開發人員須要本身手動去操做內存,那麼能夠經過Unsafe類去進行申請,這也是Unsafe類被定義爲不安全
的類的緣由,由於一不當心就容易出現忘記釋放內存
等問題。筆者畫了一張腦圖,由於圖片佔用空間較大,爲了避免影響閱讀,我把這張圖放在了文章末尾,以供參考。
// 類被final修飾,表示不能被繼承
public final class Unsafe {
// 構造器被私有化
private Unsafe() {}
private static final Unsafe theUnsafe = new Unsafe();
public static Unsafe getUnsafe() {
Class<?> caller = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(caller.getClassLoader()))
throw new SecurityException("Unsafe");
return theUnsafe;
}
}
複製代碼
SecurityException
異常。例如以下示例:public class Demo {
public static void main(String[] args) {
Unsafe unsafe = Unsafe.getUnsafe();
}
}
複製代碼
Exception in thread "main" java.lang.SecurityException: Unsafe
at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
at com.tiantang.study.Demo.main(Demo.java:14)
複製代碼
SecurityException
異常呢?這是由於在Unsafe類的getUnsafe()
方法中,它作了一層校驗,判斷當前類(Demo)的類加載器(ClassLoader)是否是啓動類加載器(Bootstrap ClassLoader)
,若是不是,則會拋出SecurityException
異常。在JVM的類加載機制中,自定義的類使用的類加載器是應用程序類加載器(Application ClassLoader)
,因此這個時候校驗失敗,會拋出異常。反射反射,程序員的快樂
)。反射的代碼能夠參考以下示例:public static void main(String[] args) {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
// 將字段的訪問權限設置爲true
field.setAccessible(true);
// 由於theUnsafe字段在Unsafe類中是一個靜態字段,因此經過Field.get()獲取字段值時,能夠傳null獲取
Unsafe unsafe = (Unsafe) field.get(null);
// 控制檯能打印出對象哈希碼
System.out.println(unsafe);
} catch (Exception e) {
e.printStackTrace();
}
}
複製代碼
compareAndSwapInt()、compareAndSwapLong()、compareAndSwapObject()
這三個CAS方法都是native方法,具體實現是在JVM中實現,它們的做用是比較並交換,這個操做是原子操做。關於CAS更詳細的講解能夠參考這篇文章:初識CAS的實現原理。protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
複製代碼
DirectByteBuffer(int cap) {
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 {
// 調用unsafe申請內存
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
// 初始化內存
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;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
複製代碼
io.netty.buffer.UnpooledUnsafeDirectByteBuf
類申請內存時的源碼以下:public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf {
protected ByteBuffer allocateDirect(int initialCapacity) {
// 調用ButeBuffer來申請堆外內存,ButeBuffer是java.nio包下的內
return ByteBuffer.allocateDirect(initialCapacity);
}
}
複製代碼
java.nio.ByteBuffer
類是經過DirectByteBuffer類來操做內存,DirectByteBuffer又是經過Unsafe類來操做內存,因此最終實際上Netty對堆外的內存的操做是經過Unsafe類中的API來實現的。LockSupport.park()、LockSupport.unpark()
方法來進行線程間的通訊。LockSupport中的這些方法最終調用的是Unsafe類的park()和unPark()。下面是LockSupport類的部分源代碼。public class LockSupport {
// UNSAFE是Unsafe類的實例
public static void park() {
// 阻塞線程
UNSAFE.park(false, 0L);
}
public static void unpark(Thread thread) {
if (thread != null)
// 喚醒線程
UNSAFE.unpark(thread);
}
}
複製代碼
arrayBaseOffset()、arrayIndexScale()
。// 返回數組中第一個元素在內存中的偏移量
public native int arrayBaseOffset(Class<?> arrayClass);
// 返回數組中每一個元素佔用的內存大小,單位是字節
public native int arrayIndexScale(Class<?> arrayClass);
複製代碼
public class AtomicIntegerArray implements java.io.Serializable {
private static final long serialVersionUID = 2862133569453604235L;
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 獲取數組中第一元素在內存中的偏移量
private static final int base = unsafe.arrayBaseOffset(int[].class);
private static final int shift;
private final int[] array;
static {
// 獲取數組中每一個元素佔用的內存大小
// 對於int類型的元素,佔用的是4個字節大小,因此此時返回的是4
int scale = unsafe.arrayIndexScale(int[].class);
if ((scale & (scale - 1)) != 0)
throw new Error("data type scale not a power of two");
shift = 31 - Integer.numberOfLeadingZeros(scale);
}
private static long byteOffset(int i) {
// 根據數組中第一個元素在內存中的偏移量和每一個元素佔用的大小,
// 計算出數組中第i個元素在內存中的偏移量
return ((long) i << shift) + base;
}
}
複製代碼
getObject()和putObject()
,一個是從內存中獲取給定對象的指定偏移量的Object類型對象,一個是向內存中寫。與此相似的還有getInt()、getLong()...等方法。還有一組加了volatile語義的方法,例如:getObjectValotile()、putObjectVolatile()
,它們的做用就是使用volatile語義獲取值和存儲值。什麼是volatile語義呢?就是讀數據時每次都從內存中取最新的值,而不是使用CPU緩存中的值;存數據時將值立馬刷新到內存,而不是先寫到CPU緩存,等之後再刷新回內存。部分方法註釋以下://從對象o的指定地址偏移量offset處獲取變量的引用,與此相似方法有:getInt,getLong等等
public native Object getObject(Object o, long offset);
//對對象o的指定地址偏移量offset處設值,與此相似方法有:putInt,putLong等等
public native void putObject(Object o, long offset, Object x);
//從對象o的指定地址偏移量offset處獲取變量的引用,使用volatile語義讀取,與此相似方法有:getIntVolatile,getLongVolatile等等
public native Object getObjectVolatile(Object o, long offset);
//對對象o的指定地址偏移量offset處設值,使用volatile語義存儲,與此相似方法有:putIntVolatile,putLongVolatile等等
public native void putObjectVolatile(Object o, long offset, Object x);
複製代碼
objectFieldOffset()
。它的做用是獲取對象的某個非靜態字段相對於該對象
的偏移地址,它與staticFieldOffset()
的做用相似,可是存在一點區別。staticFieldOffset()獲取的是靜態字段相對於類對象(即類所對應的Class對象)的偏移地址。靜態字段存在於方法區中,靜態字段每次獲取的偏移量的值都是相同的。// 獲取對象的某個非靜態字段相對於該對象的偏移地址
public native long objectFieldOffset(Field f);
複製代碼
objectFieldOffset()
的應用場景十分普遍,由於在Unsafe類中,大部分API方法都須要傳入一個offset參數,這個參數表示的是偏移量,要想直接操做內存中某個地址的數據,就必須先找到這個數據在哪兒,而經過offset就能知道這個數據在哪兒。所以這個方法應用得十分普遍,下面以AtomicInteger類爲例:在靜態代碼塊中,經過objectFieldOffset()
獲取了value屬性在內存中的偏移量,這樣後面將value寫入到內存時,就能根據offset來寫入了。public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
// 在static靜態塊中調用objectFieldOffset()方法,獲取value字段在內存中的偏移量
// 由於後面AtomicInteger在進行原子操做時,須要調用Unsafe類的CAS方法,而這些方法均須要傳入offset這個參數
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
}
複製代碼
staticFieldOffset()
,獲取靜態字段的對象指針:staticFieldBase()
。// 獲取給定靜態字段的偏移量
public native long staticFieldOffset(Field f);
// 獲取給定靜態字段的對象指針
public native Object staticFieldBase(Field f);
複製代碼
invokedynimic
和VM Anonymous Class
模板機制來實現的,VM Anonymous Class
模板機制最終會使用到Unsafe類的defineAnonymousClass()方法來建立匿名類。對這一塊感興趣的朋友能夠去查閱一下相關的資料,歡迎分享。// 定義一個匿名內部類
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);
複製代碼
loadFence()、 storeFence() 、fullFence()
。// 禁止load操做重排序
public native void loadFence();
// 禁止store操做重排序
public native void storeFence();
// 禁止load和store操做重排序
public native void fullFence();
複製代碼
// 獲取指針的大小,單位是字節。
// 對於64位系統,返回8,表示指針大小是8字節
// 對於32位系統,返回4,表示指針大小是4字節
public native int addressSize();
// 返回內存頁的大小,單位是字節。返回值必定是2的多少次冪
public native int pageSize();
複製代碼
public static void main(String[] args) {
Unsafe unsafe = null;
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
} catch (Exception e) {
e.printStackTrace();
}
// 指針大小
System.out.println(unsafe.addressSize());
// 內存頁大小
System.out.println(unsafe.pageSize());
}
複製代碼
8
4096
複製代碼
java.nio.Bits
類中有實際應用。Bits做爲工具類,提供了計算所申請內存須要佔用多少內存頁的方法,這個時候須要知道硬件的內存頁大小,才能計算出佔用內存頁的數量。所以在這裏藉助了Unsafe.pageSize()方法來實現。Bits
類的部分源碼以下。class Bits {
static int pageSize() {
if (pageSize == -1)
// 獲取內存頁大小
pageSize = unsafe().pageSize();
return pageSize;
}
// 根據內存大小,計算須要的內存頁數量
static int pageCount(long size) {
return (int)(size + (long)pageSize() - 1L) / pageSize();
}
}
複製代碼
objectFieldOffset(Field f)
這個方法很經常使用,它是獲取字段在內存中的偏移量,一般和Unsafe類中的其餘方法結合使用。經過這個方法能知道要修改的數據在內存中的位置,而後再經過Unsafe類中其餘方法來根據數據在內存中的位置從而來修改數據。