<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          聊聊Unsafe的一些使用技巧

          共 8251字,需瀏覽 17分鐘

           ·

          2021-10-16 22:42


          前言

          記得初學(xué) Java 那會(huì),剛學(xué)完語(yǔ)法基礎(chǔ),就接觸到了反射這個(gè) Java 提供的特性,盡管在現(xiàn)在看來(lái),這是非常基礎(chǔ)的知識(shí)點(diǎn),但那時(shí)候無(wú)疑是興奮的,瞬間覺(jué)得自己脫離了“Java 初學(xué)者”的隊(duì)伍。隨著工作經(jīng)驗(yàn)的積累,我也逐漸學(xué)習(xí)到了很多類似的讓我為之而興奮的知識(shí)點(diǎn),Unsafe 的使用技巧無(wú)疑便是其中一個(gè)。

          sun.misc.Unsafe 是 JDK 原生提供的一個(gè)工具類,包含了很多在 Java 語(yǔ)言看來(lái)很 cool 的操作,例如內(nèi)存分配與回收、CAS 操作、類實(shí)例化、內(nèi)存屏障等。正如其命名一樣,由于其可以直接操作內(nèi)存,執(zhí)行底層系統(tǒng)調(diào)用,其提供的操作也是比較危險(xiǎn)的。Unsafe 在擴(kuò)展 Java 語(yǔ)言表達(dá)能力、便于在更高層(Java層)代碼里實(shí)現(xiàn)原本要在更低層(C層)實(shí)現(xiàn)的核心庫(kù)功能上起到了很大的作用。

          從 JDK9 開始,Java 模塊化設(shè)計(jì)的限制,使得非標(biāo)準(zhǔn)庫(kù)的模塊都無(wú)法訪問(wèn)到 sun.misc.Unsafe。但在 JDK8 中,我們?nèi)匀豢梢灾苯硬僮?Unsafe,再不學(xué)習(xí),后面可能就沒(méi)機(jī)會(huì)了。

          使用 Unsafe

          Unsafe 被設(shè)計(jì)的初衷,并不是希望被一般開發(fā)者調(diào)用,所以我們不能通過(guò) new 或者工廠方法去實(shí)例化 Unsafe 對(duì)象,通常可以采用反射的方法獲取到 Unsafe 實(shí)例:

          public?static?final?Unsafe?unsafe?=?getUnsafe();

          static?sun.misc.Unsafe?getUnsafe()?{
          ????try?{
          ????????Field?field?=?Unsafe.class.getDeclaredField("theUnsafe");
          ????????field.setAccessible(true);
          ????????return??(Unsafe)?field.get(null);
          ????}?catch?(Exception?e)?{
          ????????throw?new?RuntimeException(e);
          ????}
          }

          拿到之后,便可以用這個(gè)全局的單例對(duì)象去為所欲為了。

          功能概覽


          圖片來(lái)源于網(wǎng)絡(luò),我直接借用過(guò)來(lái)了。上圖包含了 Unsafe 的眾多功能,還算全面。如果全部介紹,文章篇幅會(huì)過(guò)長(zhǎng),形式難免會(huì)流水賬,我打算結(jié)合我的一些項(xiàng)目經(jīng)驗(yàn)以及一些比賽經(jīng)驗(yàn),從實(shí)踐角度聊聊 Unsafe 的一些使用技巧。

          內(nèi)存分配&存取

          Java 其實(shí)也可以像 C++ 那樣直接操作內(nèi)存,借助 Unsafe 就可以。讓我們先來(lái)看一個(gè) ByteBuffer 的示例,我們將會(huì)開辟一個(gè) 16 字節(jié)的內(nèi)存空間,先后寫入并讀取 4 個(gè) int 類型的數(shù)據(jù)。

          public?static?void?testByteBuffer()?{
          ????ByteBuffer?directBuffer?=?ByteBuffer.allocateDirect(16);
          ????directBuffer.putInt(1);
          ????directBuffer.putInt(2);
          ????directBuffer.putInt(3);
          ????directBuffer.putInt(4);
          ????directBuffer.flip();
          ????System.out.println(directBuffer.getInt());
          ????System.out.println(directBuffer.getInt());
          ????System.out.println(directBuffer.getInt());
          ????System.out.println(directBuffer.getInt());
          }

          熟悉 nio 操作的同學(xué)對(duì)上面的示例應(yīng)該不會(huì)感到陌生,這是很基礎(chǔ)也是很標(biāo)準(zhǔn)的內(nèi)存使用方式。那換做是 Unsafe 怎么實(shí)現(xiàn)同樣的效果的?

          public?static?void?testUnsafe0()?{
          ????Unsafe?unsafe?=?Util.unsafe;
          ????long?address?=?unsafe.allocateMemory(16);
          ????unsafe.putInt(address,?1);
          ????unsafe.putInt(address?+?4,?2);
          ????unsafe.putInt(address?+?8,?3);
          ????unsafe.putInt(address?+?12,?4);

          ????System.out.println(unsafe.getInt(address));
          ????System.out.println(unsafe.getInt(address?+?4));
          ????System.out.println(unsafe.getInt(address?+?8));
          ????System.out.println(unsafe.getInt(address?+?12));
          }

          兩段代碼輸出結(jié)果一致:

          1
          2
          3
          4

          下面針對(duì)使用到的 Unsafe 的 API,逐個(gè)介紹:

          public?native?long?allocateMemory(long?var1);

          這個(gè) native 方法分配的是堆外內(nèi)存,返回的 long 類型數(shù)值,便是內(nèi)存的首地址,可以作為 Unsafe 其他 API 的入?yún)ⅰD闳绻?jiàn)過(guò) DirectByteBuffer 的源碼,會(huì)發(fā)現(xiàn)其實(shí)它內(nèi)部就是使用 Unsafe 封裝的。說(shuō)到 DirectByteBuffer,這里額外提一句,ByteBuffer.allocateDirect 分配的堆外內(nèi)存會(huì)受到 -XX:MaxDirectMemorySize 的限制,而 Unsafe 分配的堆外內(nèi)存則不會(huì)受到限制,當(dāng)然啦,也不會(huì)受到 -Xmx 的限制。如果你正在參加什么比賽并且受到了什么啟發(fā),可以把“爺懂了”打在公屏上。

          看到另外兩個(gè) API putIntgetInt ,你應(yīng)當(dāng)會(huì)意識(shí)到,肯定會(huì)有其他字節(jié)操作的 API,例如 putByte/putShort/putLong ,當(dāng)然 put 和 get 也是成對(duì)出現(xiàn)的。這一系列 API 里面也有注意點(diǎn),建議需要成對(duì)的使用,否則可能會(huì)因?yàn)樽止?jié)序問(wèn)題,導(dǎo)致解析失敗。可以看下面的例子:

          public?static?void?testUnsafe1()?{
          ????ByteBuffer?directBuffer?=?ByteBuffer.allocateDirect(4);
          ????long?directBufferAddress?=?((DirectBuffer)directBuffer).address();
          ????System.out.println("Unsafe.putInt(1)");
          ????Util.unsafe.putInt(directBufferAddress,?1);
          ????System.out.println("Unsafe.getInt()?==?"?+?Util.unsafe.getInt(directBufferAddress));
          ????directBuffer.position(0);
          ????directBuffer.limit(4);
          ????System.out.println("ByteBuffer.getInt()?==?"?+?directBuffer.getInt());
          ????directBuffer.position(0);
          ????directBuffer.limit(4);
          ????System.out.println("ByteBuffer.getInt()?reverseBytes?==?"?+?Integer.reverseBytes(directBuffer.getInt()));
          }

          輸出如下:

          Unsafe.putInt(1)
          Unsafe.getInt()?==?1
          ByteBuffer.getInt()?==?16777216
          ByteBuffer.getInt()?reverseBytes?==?1

          可以發(fā)現(xiàn)當(dāng)我們使用 Unsafe 進(jìn)行 putInt,再使用 ByteBuffer 進(jìn)行 getInt,結(jié)果會(huì)不符合預(yù)期,需要對(duì)結(jié)果進(jìn)行字節(jié)序變化之后,才恢復(fù)正確。這其實(shí)是因?yàn)椋珺yteBuffer 內(nèi)部判斷了當(dāng)前操作系統(tǒng)的字節(jié)序,對(duì)于 int 這種多字節(jié)的數(shù)據(jù)類型,我的測(cè)試機(jī)器使用大端序存儲(chǔ),而 Unsafe 默認(rèn)以小短序存儲(chǔ)導(dǎo)致。如果你拿捏不準(zhǔn),建議配套使用寫入和讀取 API,以避免字節(jié)序問(wèn)題。對(duì)字節(jié)序不了解的同學(xué)可以參考我的另外一篇文章:《“字節(jié)序”是個(gè)什么鬼》。

          內(nèi)存復(fù)制

          內(nèi)存復(fù)制在實(shí)際應(yīng)用場(chǎng)景中還是很常見(jiàn)的需求,例如上一篇文章我剛介紹過(guò)的,堆內(nèi)內(nèi)存寫入磁盤時(shí),需要先復(fù)制到堆外內(nèi)存,再例如我們做內(nèi)存聚合時(shí),需要緩沖一部分?jǐn)?shù)據(jù),也會(huì)涉及到內(nèi)存復(fù)制。你當(dāng)然也可以通過(guò) ByteBuffer 或者 set/get 去進(jìn)行操作,但肯定不如 native 方法來(lái)的高效。Unsafe 提供了內(nèi)存拷貝的 native 方法,可以實(shí)現(xiàn)堆內(nèi)到堆內(nèi)、堆外到堆外、堆外和堆內(nèi)互相拷貝,總之就是哪兒到哪兒都可以拷貝。

          public?native?void?copyMemory(Object?src,?long?offset,?Object?dst?,long?dstOffset,?long?size);

          對(duì)于堆內(nèi)內(nèi)存來(lái)說(shuō),我們可以直接給 src 傳入對(duì)象數(shù)組的首地址,并且指定 offset 為對(duì)應(yīng)數(shù)組類型的偏移量,可以通過(guò) arrayBaseOffset 方法獲取堆內(nèi)內(nèi)存存儲(chǔ)對(duì)象的偏移量

          public?native?int?arrayBaseOffset(Class?var1);

          例如獲取 byte[] 的固定偏移量可以這樣操作:unsafe.arrayBaseOffset(byte[].class)

          對(duì)于堆外內(nèi)存來(lái)說(shuō),會(huì)更加直觀一點(diǎn),dst 設(shè)為 null,dstOffset 設(shè)置為 Unsafe 獲取的內(nèi)存地址即可。

          堆內(nèi)內(nèi)存復(fù)制到堆外內(nèi)存的示例代碼:

          public?static?void?unsafeCopyMemory()??{
          ????ByteBuffer?heapBuffer?=?ByteBuffer.allocate(4);
          ????ByteBuffer?directBuffer?=?ByteBuffer.allocateDirect(4);
          ????heapBuffer.putInt(1234);
          ????long?address?=?((DirectBuffer)directBuffer).address();

          ????Util.unsafe.copyMemory(heapBuffer.array(),?16,?null,?address,?4);

          ????directBuffer.position(0);
          ????directBuffer.limit(4);

          ????System.out.println(directBuffer.getInt());
          }

          在實(shí)際應(yīng)用中,大多數(shù) ByteBuffer 相關(guān)的源碼在涉及到內(nèi)存復(fù)制時(shí),都使用了 copyMemory 方法。

          非常規(guī)實(shí)例化對(duì)象

          在 JDK9 模塊化之前,如果不希望將一些類開放給其他用戶使用,或者避免被隨意實(shí)例化(單例模式),通常有兩個(gè)常見(jiàn)做法

          案例一:私有化構(gòu)造器

          public?class?PrivateConstructorFoo?{

          ????private?PrivateConstructorFoo()?{
          ????????System.out.println("constructor?method?is?invoked");
          ????}

          ????public?void?hello()?{
          ????????System.out.println("hello?world");
          ????}

          }

          如果希望實(shí)例化該對(duì)象,第一時(shí)間想到的可能是反射創(chuàng)建

          public?static?void?reflectConstruction()?{
          ??PrivateConstructorFoo?privateConstructorFoo?=?PrivateConstructorFoo.class.newInstance();
          ??privateConstructorFoo.hello();
          }

          不出所料,我們獲得了一個(gè)異常

          java.lang.IllegalAccessException:?Class?io.openmessaging.Main?can?not?access?a?member?of?class?moe.cnkirito.PrivateConstructorFoo?with?modifiers?"private"

          稍作調(diào)整,調(diào)用構(gòu)造器創(chuàng)建實(shí)例

          public?static?void?reflectConstruction2()?{
          ???Constructor?constructor?=?PrivateConstructorFoo.class.getDeclaredConstructor();
          ???constructor.setAccessible(true);
          ???PrivateConstructorFoo?privateConstructorFoo?=?constructor.newInstance();
          ???privateConstructorFoo.hello();
          }

          it works!輸出如下:

          constructor?method?is?invoked
          hello?world

          當(dāng)然,Unsafe 也提供了 allocateInstance 方法

          public?native?Object?allocateInstance(Class?var1)?throws?InstantiationException;

          也可以實(shí)現(xiàn)實(shí)例化,而且更為直觀

          public?static?void?allocateInstance()?throws?InstantiationException?{
          ????PrivateConstructorFoo?privateConstructorFoo?=?(PrivateConstructorFoo)?Util.unsafe.allocateInstance(PrivateConstructorFoo.class);
          ????privateConstructorFoo.hello();
          }

          同樣 works!輸出如下:

          hello?world

          注意這里有一個(gè)細(xì)節(jié),allocateInstance 沒(méi)有觸發(fā)構(gòu)造方法。

          案例二:package level 實(shí)例

          package?moe.cnkirito;

          class?PackageFoo?{

          ????public?void?hello()?{
          ????????System.out.println("hello?world");
          ????}

          }

          注意,這里我定義了一個(gè) package 級(jí)別可訪問(wèn)的對(duì)象 PackageFoo,只有 moe.cnkirito 包下的類可以訪問(wèn)。

          我們同樣先嘗試使用反射

          package?com.bellamm;

          public?static?void?reflectConstruction()?{
          ??Class?aClass?=?Class.forName("moe.cnkirito.PackageFoo");
          ??aClass.newInstance();
          }

          得到了意料之中的報(bào)錯(cuò):

          java.lang.IllegalAccessException:?Class?io.openmessaging.Main?can?not?access?a?member?of?class?moe.cnkirito.PackageFoo?with?modifiers?""

          再試試 Unsafe 呢?

          package?com.bellamm;

          public?static?void?allocateInstance()?throws?Exception{
          ????Class?fooClass?=?Class.forName("moe.cnkirito.PackageFoo");
          ????Object?foo?=?Util.unsafe.allocateInstance(fooClass);
          ????Method?helloMethod?=?fooClass.getDeclaredMethod("hello");
          ????helloMethod.setAccessible(true);
          ????helloMethod.invoke(foo);
          }

          由于在 com.bellamm 包下,我們甚至無(wú)法在編譯期定義 PackageFoo 類,只能通過(guò)反射機(jī)制在運(yùn)行時(shí),獲取 moe.cnkirito.PackageFoo 的方法,配合 Unsafe 實(shí)例化,最終實(shí)現(xiàn)調(diào)用,成功輸出 hello world

          我們花了這么大的篇幅進(jìn)行實(shí)驗(yàn)來(lái)說(shuō)明了兩種限制案例,以及 Unsafe 的解決方案,還需要有實(shí)際的應(yīng)用場(chǎng)景佐證 Unsafe#allocateInstance 的價(jià)值。我簡(jiǎn)單列舉兩個(gè)場(chǎng)景:

          1. 序列化框架在使用反射無(wú)法創(chuàng)建對(duì)象時(shí),可以嘗試使用 Unsafe 創(chuàng)建,作為兜底邏輯。
          2. 獲取包級(jí)別保護(hù)的類,再借助于反射機(jī)制,可以魔改一些源碼實(shí)現(xiàn)或者調(diào)用一些 native 方法,此法慎用,不建議在生產(chǎn)使用。

          示例代碼:動(dòng)態(tài)修改堆外內(nèi)存限制,覆蓋 JVM 啟動(dòng)參數(shù):-XX:MaxDirectMemorySize

          ????private?void?hackMaxDirectMemorySize()?{
          ????????try?{
          ????????????Field?directMemoryField?=?VM.class.getDeclaredField("directMemory");
          ????????????directMemoryField.setAccessible(true);
          ????????????directMemoryField.set(new?VM(),?8L?*?1024?*?1024?*?1024);

          ????????????Object?bits?=?Util.unsafe.allocateInstance(Class.forName("java.nio.Bits"));
          ????????????Field?maxMemory?=?bits.getClass().getDeclaredField("maxMemory");
          ????????????maxMemory.setAccessible(true);
          ????????????maxMemory.set(bits,?8L?*?1024?*?1024?*?1024);

          ????????}?catch?(Exception?e)?{
          ????????????throw?new?RuntimeException(e);
          ????????}

          ????????System.out.println(VM.maxDirectMemory());

          ????}

          總結(jié)

          先大概介紹這三個(gè) Unsafe 用法吧,已經(jīng)是我個(gè)人認(rèn)為比較常用的幾個(gè) Unsafe 案例了。

          Unsafe 這個(gè)東西,會(huì)用的人基本都知道不能瞎用;不會(huì)用的話,看個(gè)熱鬧,知道 Java 有這個(gè)機(jī)制總比不知道強(qiáng)對(duì)吧。當(dāng)然,本文也介紹了一些實(shí)際場(chǎng)景可能必須得用 Unsafe,但更多還是出現(xiàn)在各個(gè)底層源碼之中。

          如果還有讀者想看到更多騷操作的話,歡迎轉(zhuǎn)發(fā)本文,閱讀過(guò) 1500,繼續(xù)加更一期,一鍵三連,這次一定。

          -?END?-

          「技術(shù)分享」某種程度上,是讓作者和讀者,不那么孤獨(dú)的東西。歡迎關(guān)注我的微信公眾號(hào):「Kirito的技術(shù)分享」


          瀏覽 122
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  成人免费大片黄在线播放 | A 片成人网 | 污网站18 | 欧美成人在线无码 | 永久免费看黄色视频 |