Java 中的 Unsafe 魔法類(lèi)的作用大全
Unsafe是位于sun.misc包下的一個(gè)類(lèi),主要提供一些用于執(zhí)行低級(jí)別、不安全操作的方法,如直接訪(fǎng)問(wèn)系統(tǒng)內(nèi)存資源、自主管理內(nèi)存資源等,這些方法在提升Java運(yùn)行效率、增強(qiáng)Java語(yǔ)言底層資源操作能力方面起到了很大的作用。
但是,這個(gè)類(lèi)的作者不希望我們使用它,因?yàn)槲覀冸m然我們獲取到了對(duì)底層的控制權(quán),但是也增大了風(fēng)險(xiǎn),安全性正是Java相對(duì)于C++/C的優(yōu)勢(shì)。因?yàn)樵擃?lèi)在sun.misc包下,默認(rèn)是被BootstrapClassLoader加載的。如果我們?cè)诔绦蛑腥フ{(diào)用這個(gè)類(lèi)的話(huà),我們使用的類(lèi)加載器肯定是 AppClassLoader,問(wèn)題是在Unsafe中是這樣寫(xiě)的:
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;
}
}
將構(gòu)造函數(shù)私有,然后提供了一個(gè)靜態(tài)方法去獲取當(dāng)前類(lèi)實(shí)例。在getUnsafe()方法中首先判斷當(dāng)前類(lèi)加載器是否為空,因?yàn)槭褂?BootstrapClassLoader 本身就是空,它是用c++實(shí)現(xiàn)的,這樣就限制了我們?cè)谧约旱拇a中使用這個(gè)類(lèi)。
但是同時(shí)作者也算是給我們提供了一個(gè)后門(mén),因?yàn)镴ava有反射機(jī)制。調(diào)用的思路就是將theUnsafe對(duì)象設(shè)置為可見(jiàn)。
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafeField.get(null);
System.out.println(unsafe);
unsafe類(lèi)功能介紹:

內(nèi)存操作
這部分主要包含堆外內(nèi)存的分配、拷貝、釋放、給定地址值操作等方法。
//分配內(nèi)存, 相當(dāng)于C++的malloc函數(shù) public native long allocateMemory(long bytes); //擴(kuò)充內(nèi)存 public native long reallocateMemory(long address, long bytes); //釋放內(nèi)存 public native void freeMemory(long address); //在給定的內(nèi)存塊中設(shè)置值 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); //獲取給定地址值,忽略修飾限定符的訪(fǎng)問(wèn)限制。與此類(lèi)似操作還有: getInt,getDouble,getLong,getChar等 public native Object getObject(Object o, long offset); //為給定地址設(shè)置值,忽略修飾限定符的訪(fǎng)問(wèn)限制,與此類(lèi)似操作還有: putInt,putDouble,putLong,putChar等 public native void putObject(Object o, long offset, Object x); //獲取給定地址的byte類(lèi)型的值(當(dāng)且僅當(dāng)該內(nèi)存地址為allocateMemory分配時(shí),此方法結(jié)果為確定的) public native byte getByte(long address); //為給定地址設(shè)置byte類(lèi)型的值(當(dāng)且僅當(dāng)該內(nèi)存地址為allocateMemory分配時(shí),此方法結(jié)果才是確定的) public native void putByte(long address, byte x);
通常,我們?cè)贘ava中創(chuàng)建的對(duì)象都處于堆內(nèi)內(nèi)存(heap)中,堆內(nèi)內(nèi)存是由JVM所管控的Java進(jìn)程內(nèi)存,并且它們遵循JVM的內(nèi)存管理機(jī)制,JVM會(huì)采用垃圾回收機(jī)制統(tǒng)一管理堆內(nèi)存。與之相對(duì)的是堆外內(nèi)存,存在于JVM管控之外的內(nèi)存區(qū)域,Java中對(duì)堆外內(nèi)存的操作,依賴(lài)于Unsafe提供的操作堆外內(nèi)存的native方法。
使用堆外內(nèi)存的原因
- 對(duì)垃圾回收停頓的改善。由于堆外內(nèi)存是直接受操作系統(tǒng)管理而不是JVM,所以當(dāng)我們使用堆外內(nèi)存時(shí),即可保持較小的堆內(nèi)內(nèi)存規(guī)模。從而在GC時(shí)減少回收停頓對(duì)于應(yīng)用的影響。
- 提升程序I/O操作的性能。通常在I/O通信過(guò)程中,會(huì)存在堆內(nèi)內(nèi)存到堆外內(nèi)存的數(shù)據(jù)拷貝操作,對(duì)于需要頻繁進(jìn)行內(nèi)存間數(shù)據(jù)拷貝且生命周期較短的暫存數(shù)據(jù),都建議存儲(chǔ)到堆外內(nèi)存。
典型應(yīng)用
DirectByteBuffer是Java用于實(shí)現(xiàn)堆外內(nèi)存的一個(gè)重要類(lèi),通常用在通信過(guò)程中做緩沖池,如在Netty、MINA等NIO框架中應(yīng)用廣泛。DirectByteBuffer對(duì)于堆外內(nèi)存的創(chuàng)建、使用、銷(xiāo)毀等邏輯均由Unsafe提供的堆外內(nèi)存API來(lái)實(shí)現(xiàn)。
下面的代碼為DirectByteBuffer構(gòu)造函數(shù),創(chuàng)建DirectByteBuffer的時(shí)候,通過(guò)Unsafe.allocateMemory分配內(nèi)存、Unsafe.setMemory進(jìn)行內(nèi)存初始化,而后構(gòu)建Cleaner對(duì)象用于跟蹤DirectByteBuffer對(duì)象的垃圾回收,以實(shí)現(xiàn)當(dāng)DirectByteBuffer被垃圾回收時(shí),分配的堆外內(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 對(duì)象的垃圾回收,實(shí)現(xiàn)堆外內(nèi)存的釋放
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
上面最后一句代碼通過(guò)Cleaner.create()來(lái)進(jìn)行對(duì)象監(jiān)控,釋放堆外內(nèi)存。這里是如何做到的呢?跟蹤一下Cleaner類(lèi):
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大引用類(lèi)型我們都知道。PhantomReference的作用于其他的Refenrence作用大有不同。像 SoftReference、WeakReference都是為了保證引用的類(lèi)對(duì)象能在不用的時(shí)候及時(shí)的被回收,但是 PhantomReference 并不會(huì)決定對(duì)象的生命周期。如果一個(gè)對(duì)象僅持有虛引用,那么它就和沒(méi)有任何引用一樣,對(duì)象不可達(dá)時(shí)就會(huì)被垃圾回收器回收,但是任何時(shí)候都無(wú)法通過(guò)虛引用獲得對(duì)象。虛引用主要用來(lái)跟蹤對(duì)象被垃圾回收器回收的活動(dòng)。
那他的作用到底是啥呢?準(zhǔn)確來(lái)說(shuō) PhantomReference 給使用者提供了一種機(jī)制-來(lái)監(jiān)控對(duì)象的垃圾回收的活動(dòng)。
可能這樣說(shuō)不是太明白,我來(lái)舉個(gè)例子:
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(),因?yàn)镻hantomReference必須的維護(hù)一個(gè)ReferenceQueue用來(lái)保存當(dāng)前被虛引用的對(duì)象。上例中手動(dòng)去調(diào)用referenceQueue.poll()方法,這里你需要注意的是并不是我們主動(dòng)去釋放queue中的對(duì)象,你跟蹤進(jìn)去 poll() 方法可以看到有一個(gè)全局鎖對(duì)象,只有當(dāng)當(dāng)前對(duì)象失去了引用之后才會(huì)釋放鎖,poll()方法才能執(zhí)行。在執(zhí)行poll()方法釋放對(duì)象的時(shí)候我們可以針對(duì)這個(gè)對(duì)象做一些監(jiān)控。這就是 PhantomReference 的意義所在。
說(shuō)回到 Cleaner, 通過(guò)看源碼,create()方法調(diào)用了add()方法,在Cleaner類(lèi)里面維護(hù)了一個(gè)雙向鏈表,將每一個(gè)add進(jìn)來(lái)的Cleaner對(duì)象都添加到這個(gè)鏈表中維護(hù)。那么在Cleaner 鏈表中的對(duì)象實(shí)在何時(shí)被釋放掉呢?
注意到 Cleaner中有一個(gè)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()方法是將該對(duì)象從內(nèi)部維護(hù)的雙向鏈表中清除。下面緊跟著是thunk.run() ,thunk = 我們通過(guò)create()方法傳進(jìn)來(lái)的參數(shù),在``DirectByteBuffer中那就是:Cleaner.create(this, new Deallocator(base, size, cap))`,Deallocator類(lèi)也是一個(gè)線(xiàn)程:
private static class Deallocator
implements Runnable
{
private static Unsafe unsafe = Unsafe.getUnsafe();
//省略無(wú)關(guān) 代碼
public void run() {
if (address == 0) {
// Paranoia
return;
}
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
}
看到在run方法中調(diào)用了freeMemory()去釋放掉對(duì)象。
在 Reference類(lèi)中調(diào)用了該方法,Reference 類(lèi)中的靜態(tài)代碼塊 有個(gè)一內(nèi)部類(lèi):ReferenceHandler,它繼承了 Thread,在run方法中調(diào)用了 tryHandlePending(),并且被設(shè)置為守護(hù)線(xiàn)程,意味著會(huì)循環(huán)不斷的處理pending鏈表中的對(duì)象引用。
這里要注意的點(diǎn)是:
Cleaner本身不帶有清理邏輯,所有的邏輯都封裝在thunk中,因此thunk是怎么實(shí)現(xiàn)的才是最關(guān)鍵的。
另外,Java 最新核心技術(shù)系列教程和示例源碼看這里: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;
//如果當(dāng)前Reference對(duì)象是Cleaner類(lèi)型的就進(jìn)行特殊處理
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 不為空的時(shí)候,走清理的邏輯
if (c != null) {
c.clean();
return true;
}
ReferenceQueue<? super Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}
tryHandlePending這段代碼的意思是:
如果一個(gè)對(duì)象經(jīng)過(guò)JVM檢測(cè)他已經(jīng)沒(méi)有強(qiáng)引用了,但是還有 弱引用 或者 軟引用 或者 虛引用的情況下,那么就會(huì)把此對(duì)象放到一個(gè)名為pending的鏈表里,這個(gè)鏈表是通過(guò)Reference.discovered域連接在一起的。
ReferenceHandler這個(gè)線(xiàn)程會(huì)一直從鏈表中取出被pending的對(duì)象,它可能是WeakReference,也可能是SoftReference,當(dāng)然也可能是PhantomReference和Cleaner。如果是Cleaner,那就直接調(diào)用Cleaner的clean方法,然后就結(jié)束了。其他的情況下,要交給這個(gè)對(duì)象所關(guān)聯(lián)的queue,以便于后續(xù)的處理。
關(guān)于堆外內(nèi)存分配和回收的代碼我們就先分析到這里。需要注意的是對(duì)外內(nèi)存回收的時(shí)機(jī)也是不確定的,所以不要持續(xù)分配一些大對(duì)象到堆外,如果沒(méi)有被回收掉,這是一件很可怕的事情。畢竟它無(wú)法被JVM檢測(cè)到。
內(nèi)存屏障
硬件層的內(nèi)存屏障分為兩種:Load Barrier 和 Store Barrier即讀屏障和寫(xiě)屏障。內(nèi)存屏障有兩個(gè)作用:阻止屏障兩側(cè)的指令重排序;強(qiáng)制把寫(xiě)緩沖區(qū)/高速緩存中的臟數(shù)據(jù)等寫(xiě)回主內(nèi)存,讓緩存中相應(yīng)的數(shù)據(jù)失效。在Unsafe中提供了三個(gè)方法來(lái)操作內(nèi)存屏障:
//讀屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前 public native void loadFence(); //寫(xiě)屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前 public native void storeFence(); //全能屏障,禁止load、store操作重排序 public native void fullFence();
先簡(jiǎn)單了解兩個(gè)指令:
- Store:將處理器緩存的數(shù)據(jù)刷新到內(nèi)存中。
- Load:將內(nèi)存存儲(chǔ)的數(shù)據(jù)拷貝到處理器的緩存中。
JVM平臺(tái)提供了一下幾種內(nèi)存屏障:
| 屏障類(lèi)型 | 指令示例 | 說(shuō)明 |
|---|---|---|
| LoadLoad Barriers | Load1;LoadLoad;Load2 | 該屏障確保Load1數(shù)據(jù)的裝載先于Load2及其后所有裝載指令的的操作 |
| StoreStore Barriers | Store1;StoreStore;Store2 | 該屏障確保Store1立刻刷新數(shù)據(jù)到內(nèi)存(使其對(duì)其他處理器可見(jiàn))該操作先于Store2及其后所有存儲(chǔ)指令的操作 |
| LoadStore Barriers | Load1;LoadStore;Store2 | 確保Load1的數(shù)據(jù)裝載先于Store2及其后所有的存儲(chǔ)指令刷新數(shù)據(jù)到內(nèi)存的操作 |
| StoreLoad Barriers | Store1;StoreLoad;Load2 | 該屏障確保Store1立刻刷新數(shù)據(jù)到內(nèi)存的操作先于Load2及其后所有裝載裝載指令的操作。它會(huì)使該屏障之前的所有內(nèi)存訪(fǎng)問(wèn)指令(存儲(chǔ)指令和訪(fǎng)問(wèn)指令)完成之后,才執(zhí)行該屏障之后的內(nèi)存訪(fǎng)問(wèn)指令 |
StoreLoad Barriers同時(shí)具備其他三個(gè)屏障的效果,因此也稱(chēng)之為全能屏障(mfence),是目前大多數(shù)處理器所支持的;但是相對(duì)其他屏障,該屏障的開(kāi)銷(xiāo)相對(duì)昂貴。
loadFence
實(shí)現(xiàn)了LoadLoad Barriers,該操作禁止了指令的重排序。
storeFence
實(shí)現(xiàn)了 StoreStore Barriers,確保屏障前的寫(xiě)操作能夠立刻刷入到主內(nèi)存,并且確保屏障前的寫(xiě)操作一定先于屏障后的寫(xiě)操作。即保證了內(nèi)存可見(jiàn)性和禁止指令重排序。
fullFence
實(shí)現(xiàn)了 StoreLoad Barriers,強(qiáng)制所有在mfence指令之前的store/load指令,都在該mfence指令執(zhí)行之前被執(zhí)行;所有在mfence指令之后的store/load指令,都在該mfence指令執(zhí)行之后被執(zhí)行。
在 JDK 中調(diào)用了 內(nèi)存屏障這幾個(gè)方法的實(shí)現(xiàn)類(lèi)有 StampedLock。關(guān)于StampedLock的實(shí)現(xiàn)我們后面會(huì)專(zhuān)門(mén)抽出一篇去講解。它并沒(méi)有去實(shí)現(xiàn)AQS隊(duì)列。而是采用了 其他方式實(shí)現(xiàn)。
系統(tǒng)相關(guān)
這部分包含兩個(gè)獲取系統(tǒng)相關(guān)信息的方法。
//返回系統(tǒng)指針的大小。返回值為4(32位系統(tǒng))或 8(64位系統(tǒng))。 public native int addressSize(); //內(nèi)存頁(yè)的大小,此值為2的冪次方。 public native int pageSize();
在 java.nio下的Bits類(lèi)中調(diào)用了pagesize()方法計(jì)算系統(tǒng)中頁(yè)大?。?/code>
private static int pageSize = -1;
static int pageSize() {
if (pageSize == -1)
pageSize = unsafe().pageSize();
return pageSize;
}
線(xiàn)程調(diào)度
線(xiàn)程調(diào)度中提供的方法包括:線(xiàn)程的掛起,恢復(fù) 和 對(duì)象鎖機(jī)制等,其中獲取對(duì)象的監(jiān)視器鎖方法已經(jīng)被標(biāo)記為棄用。
// 終止掛起的線(xiàn)程,恢復(fù)正常.java.util.concurrent包中掛起操作都是在LockSupport類(lèi)實(shí)現(xiàn)的,其底層正是使用這兩個(gè)方法 public native void unpark(Object thread); // 線(xiàn)程調(diào)用該方法,線(xiàn)程將一直阻塞直到超時(shí),或者是中斷條件出現(xiàn)。 public native void park(boolean isAbsolute, long time); //獲得對(duì)象鎖(可重入鎖) @Deprecated public native void monitorEnter(Object o); //釋放對(duì)象鎖 @Deprecated public native void monitorExit(Object o); //嘗試獲取對(duì)象鎖 @Deprecated public native boolean tryMonitorEnter(Object o);
將一個(gè)線(xiàn)程進(jìn)行掛起是通過(guò) park 方法實(shí)現(xiàn)的,調(diào)用park()后,線(xiàn)程將一直 阻塞 直到 超時(shí) 或者 中斷 等條件出現(xiàn)。unpark可以釋放一個(gè)被掛起的線(xiàn)程,使其恢復(fù)正常。整個(gè)并發(fā)框架中對(duì)線(xiàn)程的掛起操作被封裝在LockSupport類(lèi)中,LockSupport 類(lèi)中有各種版本 pack 方法,但最終都調(diào)用了Unsafe.park()方法。 我們來(lái)看一個(gè)例子:
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();
}
}
運(yùn)行上面的例子,那你會(huì)發(fā)現(xiàn)在第29行 park方法設(shè)置了超時(shí)時(shí)間為3秒后,會(huì)阻塞當(dāng)前主線(xiàn)程,直到超時(shí)時(shí)間到達(dá),下面的代碼才會(huì)繼續(xù)執(zhí)行。
對(duì)象操作
Unsafe類(lèi)中提供了多個(gè)方法來(lái)進(jìn)行 對(duì)象實(shí)例化 和 獲取對(duì)象的偏移地址 的操作:
// 傳入一個(gè)Class對(duì)象并創(chuàng)建該實(shí)例對(duì)象,但不會(huì)調(diào)用構(gòu)造方法 public native Object allocateInstance(Class<?> cls) throws InstantiationException; // 獲取字段f在實(shí)例對(duì)象中的偏移量 public native long objectFieldOffset(Field f); // 返回值就是f.getDeclaringClass() public native Object staticFieldBase(Field f); // 靜態(tài)屬性的偏移量,用于在對(duì)應(yīng)的Class對(duì)象中讀寫(xiě)靜態(tài)屬性 public native long staticFieldOffset(Field f); // 獲得給定對(duì)象偏移量上的int值,所謂的偏移量可以簡(jiǎn)單理解為指針指向該變量;的內(nèi)存地址, // 通過(guò)偏移量便可得到該對(duì)象的變量,進(jìn)行各種操作 public native int getInt(Object o, long offset); // 設(shè)置給定對(duì)象上偏移量的int值 public native void putInt(Object o, long offset, int x); // 獲得給定對(duì)象偏移量上的引用類(lèi)型的值 public native Object getObject(Object o, long offset); // 設(shè)置給定對(duì)象偏移量上的引用類(lèi)型的值 public native void putObject(Object o, long offset, Object x);); // 設(shè)置給定對(duì)象的int值,使用volatile語(yǔ)義,即設(shè)置后立馬更新到內(nèi)存對(duì)其他線(xiàn)程可見(jiàn) public native void putIntVolatile(Object o, long offset, int x); // 獲得給定對(duì)象的指定偏移量offset的int值,使用volatile語(yǔ)義,總能獲取到最新的int值。 public native int getIntVolatile(Object o, long offset); // 與putIntVolatile一樣,但要求被操作字段必須有volatile修飾 public native void putOrderedInt(Object o, long offset, int x);
allocateInstance方法在這幾個(gè)場(chǎng)景下很有用:跳過(guò)對(duì)象的實(shí)例化階段(通過(guò)構(gòu)造函數(shù))、忽略構(gòu)造函數(shù)的安全檢查(反射newInstance()時(shí))、你需要某類(lèi)的實(shí)例但該類(lèi)沒(méi)有public的構(gòu)造函數(shù)。
另外,Java 最新核心技術(shù)系列教程和示例源碼看這里:https://github.com/javastacks/javastack
舉個(gè)例子:
public class User {
private String name;
private int age;
private static String address = "beijing";
public User(){
name = "xiaoming";
}
public String getname(){
return name;
}
}
/**
* 實(shí)例化對(duì)象
* @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());
}
打印的結(jié)果可以看到最后輸出的是null,說(shuō)明構(gòu)造函數(shù)未被加載。可以進(jìn)一步實(shí)驗(yàn),將User類(lèi)中的構(gòu)造函數(shù)設(shè)置為 private,你會(huì)發(fā)現(xiàn)在前面兩種實(shí)例化方式檢查期就報(bào)錯(cuò)。但是第三種是可以用的。這是因?yàn)?code>allocateInstance只是給對(duì)象分配了內(nèi)存,它并不會(huì)初始化對(duì)象中的屬性。
下面是對(duì)象操作的使用示例:
public void testObject() throws Exception{
TestUsafe testUsafe = new TestUsafe();
Unsafe unsafe = testUsafe.getUnsafe();
//通過(guò)allocateInstance創(chuàng)建對(duì)象,為其分配內(nèi)存地址,不會(huì)加載構(gòu)造函數(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");
// 獲取實(shí)例域name和age在對(duì)象內(nèi)存中的偏移量并設(shè)置值
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字段的類(lèi)
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));
// 設(shè)置static變量address的值
unsafe.putObject(staticFieldBase, staticFieldOffset, "tianjin");
System.out.println(user + " " + user.getAddress());
}
對(duì)象實(shí)例布局與內(nèi)存大小
一個(gè)Java對(duì)象占用多大的內(nèi)存空間呢?這個(gè)問(wèn)題很值得讀者朋友去查一下。 因?yàn)檫@個(gè)輸出本篇的重點(diǎn)所以簡(jiǎn)單說(shuō)一下。一個(gè) Java 對(duì)象在內(nèi)存中由對(duì)象頭、示例數(shù)據(jù)和對(duì)齊填充構(gòu)成。對(duì)象頭存儲(chǔ)了對(duì)象運(yùn)行時(shí)的基本數(shù)據(jù),如 hashCode、鎖狀態(tài)、GC 分代年齡、類(lèi)型指針等等。實(shí)例數(shù)據(jù)是對(duì)象中的非靜態(tài)字段值,可能是一個(gè)原始類(lèi)型的值,也可能是一個(gè)指向其他對(duì)象的指針。對(duì)齊填充就是 padding,保證對(duì)象都采用 8 字節(jié)對(duì)齊。除此以外,在 64 位虛擬機(jī)中還可能會(huì)開(kāi)啟指針壓縮,將 8 字節(jié)的指針壓縮為 4 字節(jié),這里就不再過(guò)多介紹了。
也就是說(shuō)一個(gè) Java 對(duì)象在內(nèi)存中,首先是對(duì)象頭,然后是各個(gè)類(lèi)中字段的排列,這之間可能會(huì)有 padding 填充。這樣我們大概就能理解字段偏移量的含義了,它實(shí)際就是每個(gè)字段在內(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));
}
結(jié)果:
name-class java.lang.String: 16
age-int: 12
從上面的運(yùn)行結(jié)果中可以:
age:偏移值為12,即前面 12 個(gè)字節(jié)的對(duì)象頭;
name:name從16字節(jié)開(kāi)始,因?yàn)閕nt 類(lèi)型的age占了4個(gè)字節(jié)。
繼續(xù)算下去整個(gè)對(duì)象占用的空間,對(duì)象頭12,age 4,name 是指針類(lèi)型,開(kāi)啟指針壓縮占用4個(gè)字節(jié),那么User對(duì)象整個(gè)占用20字節(jié),因?yàn)樯厦嬲f(shuō)的padding填充,必須8字節(jié)對(duì)齊,那么實(shí)際上會(huì)補(bǔ)上4個(gè)字節(jié)的填充,即一共占用了24個(gè)字節(jié)。
按照這種計(jì)算方式,我們可以字節(jié)寫(xiě)一個(gè)計(jì)算size的工具類(lèi):
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);
}
}
//如果有繼承父類(lèi)的話(huà),父類(lèi)中的屬性也是要計(jì)算的
c = c.getSuperclass();
}
//計(jì)算每個(gè)字段的偏移量,因?yàn)榈谝粋€(gè)字段的偏移量即在對(duì)象頭的基礎(chǔ)上偏移的
//所以只需要比較當(dāng)前偏移量最大的字段即表示這是該對(duì)象最后一個(gè)字段的位置
long maxSize = 0;
for (Field f : fields) {
long offset = unsafe.objectFieldOffset(f);
if (offset > maxSize) {
maxSize = offset;
}
}
//上面計(jì)算的是對(duì)象最后一個(gè)字段的偏移量起始位置,java中對(duì)象最大長(zhǎng)度是8個(gè)字節(jié)(long)
//這里的計(jì)算方式是 將 當(dāng)前偏移量 / 8 + 8字節(jié) 的padding
return ((maxSize/8) + 1) * 8;
}
上面的工具類(lèi)計(jì)算的結(jié)果也是24。
class相關(guān)操作
//靜態(tài)屬性的偏移量,用于在對(duì)應(yīng)的Class對(duì)象中讀寫(xiě)靜態(tài)屬性
public native long staticFieldOffset(Field f);
//獲取一個(gè)靜態(tài)字段的對(duì)象指針
public native Object staticFieldBase(Field f);
//判斷是否需要初始化一個(gè)類(lèi),通常在獲取一個(gè)類(lèi)的靜態(tài)屬性的時(shí)候(因?yàn)橐粋€(gè)類(lèi)如果沒(méi)初始化,它的靜態(tài)屬性也不會(huì)初始化)使用。 當(dāng)且僅當(dāng)ensureClassInitialized方法不生效時(shí)返回false
public native boolean shouldBeInitialized(Class<?> c);
//確保類(lèi)被初始化
public native void ensureClassInitialized(Class<?> c);
//定義一個(gè)類(lèi),可用于動(dòng)態(tài)創(chuàng)建類(lèi),此方法會(huì)跳過(guò)JVM的所有安全檢查,默認(rèn)情況下,ClassLoader(類(lèi)加載器)和ProtectionDomain(保護(hù)域)實(shí)例來(lái)源于調(diào)用者
public native Class<?> defineClass(String name, byte[] b, int off, int len,
ClassLoader loader,
ProtectionDomain protectionDomain);
//定義一個(gè)匿名類(lèi),可用于動(dòng)態(tài)創(chuàng)建類(lèi)
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);
數(shù)組操作
數(shù)組操作主要有兩個(gè)方法:
//返回?cái)?shù)組中第一個(gè)元素的偏移地址 public native int arrayBaseOffset(Class<?> arrayClass); //返回?cái)?shù)組中一個(gè)元素占用的大小 public native int arrayIndexScale(Class<?> arrayClass);
CAS操作
相信所有的開(kāi)發(fā)者對(duì)這個(gè)詞都不陌生,在AQS類(lèi)中使用了無(wú)鎖的方式來(lái)進(jìn)行并發(fā)控制,主要就是CAS的功勞。
CAS的全稱(chēng)是Compare And Swap 即比較交換,其算法核心思想如下
執(zhí)行函數(shù):CAS(V,E,N)
包含3個(gè)參數(shù)
- V表示要更新的變量
- E表示預(yù)期值
- N表示新值
如果V值等于E值,則將V的值設(shè)為N。若V值和E值不同,則說(shuō)明已經(jīng)有其他線(xiàn)程做了更新,則當(dāng)前線(xiàn)程什么都不做。通俗的理解就是CAS操作需要我們提供一個(gè)期望值,當(dāng)期望值與當(dāng)前線(xiàn)程的變量值相同時(shí),說(shuō)明沒(méi)有別的線(xiàn)程修改該值,當(dāng)前線(xiàn)程可以進(jìn)行修改,也就是執(zhí)行CAS操作,但如果期望值與當(dāng)前線(xiàn)程不符,則說(shuō)明該值已被其他線(xiàn)程修改,此時(shí)不執(zhí)行更新操作,但可以選擇重新讀取該變量再?lài)L試再次修改該變量,也可以放棄操作。
Unsafe類(lèi)中提供了三個(gè)方法來(lái)進(jìn)行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中新增了幾個(gè) CAS 的方法,他們的實(shí)現(xiàn)是基于上面三個(gè)方法做的一層封裝:
//1.8新增,給定對(duì)象o,根據(jù)獲取內(nèi)存偏移量指向的字段,將其增加delta,
//這是一個(gè)CAS操作過(guò)程,直到設(shè)置成功方能退出循環(huán),返回舊值
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
//獲取內(nèi)存中最新值
v = getIntVolatile(o, offset);
//通過(guò)CAS操作
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
//1.8新增,方法作用同上,只不過(guò)這里操作的long類(lèi)型數(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新增,給定對(duì)象o,根據(jù)獲取內(nèi)存偏移量對(duì)于字段,將其 設(shè)置為新值newValue,
//這是一個(gè)CAS操作過(guò)程,直到設(shè)置成功方能退出循環(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類(lèi)型
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新增,同上,操作的是引用類(lèi)型數(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相關(guān)類(lèi)、Java AQS、CurrentHashMap等實(shí)現(xiàn)上有非常廣泛的應(yīng)用。
到此這篇關(guān)于Java 中的 Unsafe 魔法類(lèi)的作用大全的文章就介紹到這了,更多相關(guān)Java nsafe 魔法類(lèi)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java學(xué)習(xí)指南之字符串與正則表達(dá)式
在日常Java后端開(kāi)發(fā)過(guò)程中,免不了對(duì)數(shù)據(jù)字段的解析,自然就少不了對(duì)字符串的操作,這其中就包含了正則表達(dá)式這一塊的內(nèi)容,下面這篇文章主要給大家介紹了關(guān)于java學(xué)習(xí)指南之字符串與正則表達(dá)式的相關(guān)資料,需要的朋友可以參考下2023-05-05
IntelliJ IDEA 2021 Tomcat 8啟動(dòng)亂碼問(wèn)題的解決步驟
很多朋友遇到過(guò)IntelliJ IDEA 2021 Tomcat 8啟動(dòng)的時(shí)候出現(xiàn)各種奇葩問(wèn)題,最近有童鞋反映IntelliJ IDEA 2021 Tomcat 8啟動(dòng)亂碼,正好我也遇到這個(gè)問(wèn)題,下面我把解決方法分享給大家需要的朋友參考下吧2021-06-06
SpringBoot QQ郵箱發(fā)送郵件實(shí)例代碼
大家好,本篇文章主要講的是SpringBoot QQ郵箱發(fā)送郵件實(shí)例代碼,感興趣的同學(xué)趕快來(lái)看一看吧,對(duì)你有幫助的話(huà)記得收藏一下,方便下次瀏覽2021-12-12
微服務(wù)鏈路追蹤Spring Cloud Sleuth整合Zipkin解析
這篇文章主要為大家介紹了微服務(wù)鏈路追蹤Spring Cloud Sleuth整合Zipkin解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02
java面試應(yīng)用上線(xiàn)后Cpu使用率飆升如何排查
這篇文章主要為大家介紹了java面試中應(yīng)用上線(xiàn)后Cpu使用率飆升如何排查的方法示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07
ShardingSphere-Proxy5搭建使用過(guò)程分析
ShardingSphere-Proxy是跨語(yǔ)言的數(shù)據(jù)庫(kù)代理服務(wù)端,主要用來(lái)處理:分表、分庫(kù)、讀寫(xiě)分離 等,這篇文章主要介紹了ShardingSphere-Proxy5搭建使用過(guò)程,需要的朋友可以參考下2022-10-10

