<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>

          求你了,別再說Java對(duì)象都是在堆內(nèi)存上分配空間的了!

          共 3814字,需瀏覽 8分鐘

           ·

          2020-03-17 23:24

          △Hollis, 一個(gè)對(duì)Coding有著獨(dú)特追求的人△

          d11bd14eec7d85e42768893b9b48cd7a.webp這是Hollis的第?256篇原創(chuàng)分享作者 l Hollis來源 l Hollis(ID:hollischuang)

          Java作為一種面向?qū)ο蟮?,跨平臺(tái)語言,其對(duì)象、內(nèi)存等一直是比較難的知識(shí)點(diǎn),所以,即使是一個(gè)Java的初學(xué)者,也一定或多或少的對(duì)JVM有一些了解。可以說,關(guān)于JVM的相關(guān)知識(shí),基本是每個(gè)Java開發(fā)者必學(xué)的知識(shí)點(diǎn),也是面試的時(shí)候必考的知識(shí)點(diǎn)。

          在JVM的內(nèi)存結(jié)構(gòu)中,比較常見的兩個(gè)區(qū)域就是堆內(nèi)存和棧內(nèi)存(如無特指,本文提到的棧均指的是虛擬機(jī)棧),關(guān)于堆和棧的區(qū)別,很多開發(fā)者也是如數(shù)家珍,有很多書籍,或者網(wǎng)上的文章大概都是這樣介紹的:

          1、堆是線程共享的內(nèi)存區(qū)域,棧是線程獨(dú)享的內(nèi)存區(qū)域。

          2、堆中主要存放對(duì)象實(shí)例,棧中主要存放各種基本數(shù)據(jù)類型、對(duì)象的引用。

          但是,作者可以很負(fù)責(zé)任的告訴大家,以上兩個(gè)結(jié)論均不是完全正確的。

          在我之前的文章《Java堆內(nèi)存是線程共享的!面試官:你確定嗎?》中,介紹過了關(guān)于堆內(nèi)存并不是完完全全的線程共享有關(guān)的知識(shí)點(diǎn),本文就第二個(gè)話題來探討一下。

          對(duì)象內(nèi)存分配

          在《Java虛擬機(jī)規(guī)范》中,關(guān)于堆有這樣的描述:

          在Java虛擬機(jī)中,堆是可供各個(gè)線程共享的運(yùn)行時(shí)內(nèi)存區(qū)域,也是供所有類實(shí)例和數(shù)組對(duì)象分配內(nèi)存的區(qū)域。

          在《Java堆內(nèi)存是線程共享的!面試官:你確定嗎?》文章中,我們也介紹過,一個(gè)Java對(duì)象在堆上分配的時(shí)候,主要是在Eden區(qū)上,如果啟動(dòng)了TLAB的話會(huì)優(yōu)先在TLAB上分配,少數(shù)情況下也可能會(huì)直接分配在老年代中,分配規(guī)則并不是百分之百固定的,這取決于當(dāng)前使用的是哪一種垃圾收集器,還有虛擬機(jī)中與內(nèi)存有關(guān)的參數(shù)的設(shè)置。

          但是一般情況下是遵循以下原則的:

          • 對(duì)象優(yōu)先在Eden區(qū)分配
            • 優(yōu)先在Eden分配,如果Eden沒有足夠空間,會(huì)觸發(fā)一次Monitor GC
          • 大對(duì)象直接進(jìn)入老年代
            • 需要大量連續(xù)內(nèi)存空間的Java對(duì)象,當(dāng)對(duì)象需要的內(nèi)存大于-XX:PretenureSizeThreshold參數(shù)的值時(shí),對(duì)象會(huì)直接在老年代分配內(nèi)存。
          但是,雖然虛擬機(jī)規(guī)范中是有著這樣的要求,但是各個(gè)虛擬機(jī)廠商在實(shí)現(xiàn)虛擬機(jī)的時(shí)候,可能會(huì)針對(duì)對(duì)象的內(nèi)存分配做一些優(yōu)化。這其中最典型的就是HotSpot虛擬機(jī)中的JIT技術(shù)的成熟,使得對(duì)象在堆上分配內(nèi)存并不是一定的。其實(shí)在《深入理解Java虛擬機(jī)》中,作者也提出過類似的觀點(diǎn),因?yàn)镴IT技術(shù)的成熟使得"對(duì)象在堆上分配內(nèi)存"就不是那么絕對(duì)的了。但是書中并沒有展開介紹到底什么是JIT,也沒有介紹JIT優(yōu)化到底做了什么。那么接下來我們就來深入了解一下:

          JIT 技術(shù)

          我們大家都知道,通過 javac 將可以將Java程序源代碼編譯,轉(zhuǎn)換成 java 字節(jié)碼,JVM 通過解釋字節(jié)碼將其翻譯成對(duì)應(yīng)的機(jī)器指令,逐條讀入,逐條解釋翻譯。這就是傳統(tǒng)的JVM的解釋器(Interpreter)的功能。很顯然,Java編譯器經(jīng)過解釋執(zhí)行,其執(zhí)行速度必然會(huì)比直接執(zhí)行可執(zhí)行的二進(jìn)制字節(jié)碼慢很多。為了解決這種效率問題,引入了 JIT(Just In Time ,即時(shí)編譯) 技術(shù)。有了JIT技術(shù)之后,Java程序還是通過解釋器進(jìn)行解釋執(zhí)行,當(dāng)JVM發(fā)現(xiàn)某個(gè)方法或代碼塊運(yùn)行特別頻繁的時(shí)候,就會(huì)認(rèn)為這是“熱點(diǎn)代碼”(Hot Spot Code)。然后JIT會(huì)把部分“熱點(diǎn)代碼”翻譯成本地機(jī)器相關(guān)的機(jī)器碼,并進(jìn)行優(yōu)化,然后再把翻譯后的機(jī)器碼緩存起來,以備下次使用。

          熱點(diǎn)檢測(cè)

          上面我們說過,要想觸發(fā)JIT,首先需要識(shí)別出熱點(diǎn)代碼。目前主要的熱點(diǎn)代碼識(shí)別方式是熱點(diǎn)探測(cè)(Hot Spot Detection),HotSpot虛擬機(jī)中采用的主要是基于計(jì)數(shù)器的熱點(diǎn)探測(cè)
          基于計(jì)數(shù)器的熱點(diǎn)探測(cè)(Counter Based Hot Spot Detection)。采用這種方法的虛擬機(jī)會(huì)為每個(gè)方法,甚至是代碼塊建立計(jì)數(shù)器,統(tǒng)計(jì)方法的執(zhí)行次數(shù),某個(gè)方法超過閥值就認(rèn)為是熱點(diǎn)方法,觸發(fā)JIT編譯。

          編譯優(yōu)化

          JIT在做了熱點(diǎn)檢測(cè)識(shí)別出熱點(diǎn)代碼后,除了會(huì)對(duì)其字節(jié)碼進(jìn)行緩存,還會(huì)對(duì)代碼做各種優(yōu)化。這些優(yōu)化中,比較重要的幾個(gè)有:逃逸分析、 鎖消除、 鎖膨脹、 方法內(nèi)聯(lián)、 空值檢查消除、 類型檢測(cè)消除、 公共子表達(dá)式消除等。而這些優(yōu)化中的逃逸分析就和本文要介紹的內(nèi)容有關(guān)了。

          逃逸分析

          逃逸分析(Escape Analysis)是目前Java虛擬機(jī)中比較前沿的優(yōu)化技術(shù)。這是一種可以有效減少Java 程序中同步負(fù)載和內(nèi)存堆分配壓力的跨函數(shù)全局?jǐn)?shù)據(jù)流分析算法。通過逃逸分析,Hotspot編譯器能夠分析出一個(gè)新的對(duì)象的引用的使用范圍從而決定是否要將這個(gè)對(duì)象分配到堆上。逃逸分析的基本行為就是分析對(duì)象動(dòng)態(tài)作用域:當(dāng)一個(gè)對(duì)象在方法中被定義后,它可能被外部方法所引用,例如作為調(diào)用參數(shù)傳遞到其他地方中,稱為方法逃逸。例如:
          public?static?String?craeteStringBuffer(String?s1,?String?s2)?{

          ????StringBuffer?sb?=?new?StringBuffer();

          ????sb.append(s1);

          ????sb.append(s2);

          ????return?sb.toString();

          }
          sb是一個(gè)方法內(nèi)部變量,上述代碼中并沒有將他直接返回,這樣這個(gè)StringBuffer又不會(huì)被其他方法所改變,這樣它的作用域就只是在方法內(nèi)部。我們就可以說這個(gè)變量并沒有逃逸到方法外部。有了逃逸分析,我們可以判斷出一個(gè)方法中的變量是否有可能被其他線程所訪問或者改變,那么基于這個(gè)特性,JIT就可以做一些優(yōu)化:
          • 同步省略
          • 標(biāo)量替換
          • 棧上分配
          關(guān)于同步省略,大家可以參考我之前的《深入理解多線程(五)—— Java虛擬機(jī)的鎖優(yōu)化技術(shù)》中關(guān)于鎖消除技術(shù)的介紹。本文主要來分析下標(biāo)量替換和棧上分配。

          標(biāo)量替換、棧上分配

          我們說,JIT經(jīng)過逃逸分析之后,如果發(fā)現(xiàn)某個(gè)對(duì)象并沒有逃逸到方法體之外的話,就可能對(duì)其進(jìn)行優(yōu)化,而這一優(yōu)化最大的結(jié)果就是可能改變Java對(duì)象都是在堆上分配內(nèi)存的這一原則。對(duì)象要分配在堆上其實(shí)有很多原因,但是有一點(diǎn)比較關(guān)鍵的和本文有關(guān)的,那就是因?yàn)槎褍?nèi)存在訪問上是線程共享的,這樣一個(gè)線程創(chuàng)建出來的對(duì)象,其他線程也能訪問到。那么,試想下,如果我們?cè)谀骋粋€(gè)方法體內(nèi)部創(chuàng)建了一個(gè)對(duì)象,并且對(duì)象并沒有逃逸到方法外的話,那還有必要一定要把對(duì)象分配到堆上嗎?其實(shí)就沒有必要了,因?yàn)檫@個(gè)對(duì)象并不會(huì)被其他線程所訪問到,生命周期也只是在一個(gè)方法內(nèi)部,也就不用大費(fèi)周折的在堆上分配內(nèi)存,也減少了內(nèi)存回收的必要。那么,有了逃逸分析之后,發(fā)現(xiàn)一個(gè)對(duì)象并沒有逃逸到放法外的話,通過什么辦法可以進(jìn)行優(yōu)化,減少對(duì)象在堆上分配可能呢?這就是棧上分配。在HotSopt中,棧上分配并沒有正在的進(jìn)行實(shí)現(xiàn),而是通過標(biāo)量替換來實(shí)現(xiàn)的。所以我們重點(diǎn)介紹下,什么是標(biāo)量替換,如何通過標(biāo)量替換實(shí)現(xiàn)棧上分配。

          標(biāo)量替換

          標(biāo)量(Scalar)是指一個(gè)無法再分解成更小的數(shù)據(jù)的數(shù)據(jù)。Java中的原始數(shù)據(jù)類型就是標(biāo)量。相對(duì)的,那些還可以分解的數(shù)據(jù)叫做聚合量(Aggregate),Java中的對(duì)象就是聚合量,因?yàn)樗梢苑纸獬善渌酆狭亢蜆?biāo)量。在JIT階段,如果經(jīng)過逃逸分析,發(fā)現(xiàn)一個(gè)對(duì)象不會(huì)被外界訪問的話,那么經(jīng)過JIT優(yōu)化,就會(huì)把這個(gè)對(duì)象拆解成若干個(gè)其中包含的若干個(gè)成員變量來代替。這個(gè)過程就是標(biāo)量替換。
          public?static?void?main(String[]?args)?{

          ???alloc();

          }

          private?static?void?alloc()?{

          ???Point?point?=?new?Point(1,2);

          ???System.out.println("point.x="+point.x+";?point.y="+point.y);

          }

          class?Point{

          ????private?int?x;

          ????private?int?y;

          }
          以上代碼中,point對(duì)象并沒有逃逸出alloc方法,并且point對(duì)象是可以拆解成標(biāo)量的。那么,JIT就會(huì)不會(huì)直接創(chuàng)建Point對(duì)象,而是直接使用兩個(gè)標(biāo)量int x ,int y來替代Point對(duì)象。
          private?static?void?alloc()?{

          ???int?x?=?1;

          ???int?y?=?2;

          ???System.out.println("point.x="+x+";?point.y="+y);

          }
          可以看到,Point這個(gè)聚合量經(jīng)過逃逸分析后,發(fā)現(xiàn)他并沒有逃逸,就被替換成兩個(gè)聚合量了。通過標(biāo)量替換,原本的一個(gè)對(duì)象,被替換成了多個(gè)成員變量。而原本需要在堆上分配的內(nèi)存,也就不再需要了,完全可以在本地方法棧中完成對(duì)成員變量的內(nèi)存分配。

          實(shí)驗(yàn)證明

          Talk Is Cheap, Show Me The CodeNo Data, No BB;接下來我們就來通過一個(gè)實(shí)驗(yàn),來看一下逃逸分析是否可以生效,生效后是否真的會(huì)發(fā)生棧上分配,而棧上分配又有什么好處呢?我們來看以下代碼:
          public?static?void?main(String[]?args)?{

          ????long?a1?=?System.currentTimeMillis();

          ????for?(int?i?=?0;?i?1000000
          ;?i++)?{

          ????????alloc();

          ????}

          ????//?查看執(zhí)行時(shí)間

          ????long?a2?=?System.currentTimeMillis();

          ????System.out.println("cost?"?+?(a2?-?a1)?+?"?ms");

          ????//?為了方便查看堆內(nèi)存中對(duì)象個(gè)數(shù),線程sleep

          ????try?{

          ????????Thread.sleep(100000);

          ????}?catch?(InterruptedException?e1)?{

          ????????e1.printStackTrace();

          ????}

          }

          private?static?void?alloc()?{

          ????User?user?=?new?User();

          }

          static?class?User?{

          }
          其實(shí)代碼內(nèi)容很簡(jiǎn)單,就是使用for循環(huán),在代碼中創(chuàng)建100萬個(gè)User對(duì)象。我們?cè)赼lloc方法中定義了User對(duì)象,但是并沒有在方法外部引用他。也就是說,這個(gè)對(duì)象并不會(huì)逃逸到alloc外部。經(jīng)過JIT的逃逸分析之后,就可以對(duì)其內(nèi)存分配進(jìn)行優(yōu)化。我們指定以下JVM參數(shù)并運(yùn)行:
          -Xmx4G?-Xms4G?-XX:-DoEscapeAnalysis?-XX:+PrintGCDetails?-XX:+HeapDumpOnOutOfMemoryError?
          其中-XX:-DoEscapeAnalysis表示關(guān)閉逃逸分析。在程序打印出 cost XX ms 后,代碼運(yùn)行結(jié)束之前,我們使用jmap命令,來查看下當(dāng)前堆內(nèi)存中有多少個(gè)User對(duì)象:
          ???~?jmap?-histo?2809

          ?num?????#instances?????????#bytes??class?name
          ----------------------------------------------

          ???1:???????????524???????87282184??[I

          ???2:???????1000000???????16000000??StackAllocTest$User

          ???3:??????????6806????????2093136??[B

          ???4:??????????8006????????1320872??[C

          ???5:??????????4188?????????100512??java.lang.String

          ???6:???????????581??????????66304??java.lang.Class
          從上面的jmap執(zhí)行結(jié)果中我們可以看到,堆中共創(chuàng)建了100萬個(gè)StackAllocTest在關(guān)閉逃逸分析的情況下(-XX:-DoEscapeAnalysis),雖然在alloc方法中創(chuàng)建的User對(duì)象并沒有逃逸到方法外部,但是還是被分配在堆內(nèi)存中。也就說,如果沒有JIT編譯器優(yōu)化,沒有逃逸分析技術(shù),正常情況下就應(yīng)該是這樣的。即所有對(duì)象都分配到堆內(nèi)存中。接下來,我們開啟逃逸分析,再來執(zhí)行下以上代碼。
          -Xmx4G?-Xms4G?-XX:+DoEscapeAnalysis?-XX:+PrintGCDetails?-XX:+HeapDumpOnOutOfMemoryError?
          在程序打印出 cost XX ms 后,代碼運(yùn)行結(jié)束之前,我們使用jmap命令,來查看下當(dāng)前堆內(nèi)存中有多少個(gè)User對(duì)象:
          ???~?jmap?-histo?2859?num?????#instances?????????#bytes??class?name
          ----------------------------------------------

          ???1:???????????524??????101944280??[I

          ???2:??????????6806????????2093136??[B

          ???3:?????????83619????????1337904??StackAllocTest$User

          ???4:??????????8006????????1320872??[C

          ???5:??????????4188?????????100512??java.lang.String

          ???6:???????????581??????????66304??java.lang.Class
          從以上打印結(jié)果中可以發(fā)現(xiàn),開啟了逃逸分析之后(?XX:+DoEscapeAnalysis),在堆內(nèi)存中只有8萬多個(gè)StackAllocTestUser對(duì)象。也就是說在經(jīng)過JIT優(yōu)化之后,堆內(nèi)存中分配的對(duì)象數(shù)量,從100萬降到了8萬。除了以上通過jmap驗(yàn)證對(duì)象個(gè)數(shù)的方法以外,讀者還可以嘗試將堆內(nèi)存調(diào)小,然后執(zhí)行以上代碼,根據(jù)GC的次數(shù)來分析,也能發(fā)現(xiàn),開啟了逃逸分析之后,在運(yùn)行期間,GC次數(shù)會(huì)明顯減少。正是因?yàn)楹芏喽焉戏峙浔粌?yōu)化成了棧上分配,所以GC次數(shù)有了明顯的減少。

          逃逸分析并不成熟

          前面的例子中,開啟逃逸分析之后,對(duì)象數(shù)目從100萬變成了8萬,但是并不是0,說明JIT優(yōu)化并不會(huì)完完全全的所有情況都進(jìn)行優(yōu)化。關(guān)于逃逸分析的論文在1999年就已經(jīng)發(fā)表了,但直到JDK 1.6才有實(shí)現(xiàn),而且這項(xiàng)技術(shù)到如今也并不是十分成熟的。其根本原因就是無法保證逃逸分析的性能消耗一定能高于他的消耗。雖然經(jīng)過逃逸分析可以做標(biāo)量替換、棧上分配、和鎖消除。但是逃逸分析自身也是需要進(jìn)行一系列復(fù)雜的分析的,這其實(shí)也是一個(gè)相對(duì)耗時(shí)的過程。一個(gè)極端的例子,就是經(jīng)過逃逸分析之后,發(fā)現(xiàn)沒有一個(gè)對(duì)象是不逃逸的。那這個(gè)逃逸分析的過程就白白浪費(fèi)掉了。雖然這項(xiàng)技術(shù)并不十分成熟,但是他也是即時(shí)編譯器優(yōu)化技術(shù)中一個(gè)十分重要的手段。

          總結(jié)

          正常情況下,對(duì)象是要在堆上進(jìn)行內(nèi)存分配的,但是隨著編譯器優(yōu)化技術(shù)的成熟,雖然虛擬機(jī)規(guī)范是這樣要求的,但是具體實(shí)現(xiàn)上還是有些差別的。如HotSpot虛擬機(jī)引入了JIT優(yōu)化之后,會(huì)對(duì)對(duì)象進(jìn)行逃逸分析,如果發(fā)現(xiàn)某一個(gè)對(duì)象并沒有逃逸到方法外部,那么就可能通過標(biāo)量替換來實(shí)現(xiàn)棧上分配,而避免堆上分配內(nèi)存。所以,對(duì)象一定在堆上分配內(nèi)存,這是不對(duì)的。最后,我們留一個(gè)思考題,我們之前討論過了TLAB,今天又介紹了棧上分配。大家覺得這兩個(gè)優(yōu)化有什么相同點(diǎn)和不同點(diǎn)嗎?關(guān)于作者Hollis,一個(gè)對(duì)Coding有著獨(dú)特追求的人,現(xiàn)任阿里巴巴技術(shù)專家,個(gè)人技術(shù)博主,技術(shù)文章全網(wǎng)閱讀量數(shù)千萬,《程序員的三門課》聯(lián)合作者。b022a23cb63369644870baff9a249167.webp


          推薦閱讀:


          67f3e6c9c0a5569668b10229a475533e.webp喜歡我可以給我設(shè)為星標(biāo)哦67f3e6c9c0a5569668b10229a475533e.webp

          6533e282cd04bb665e68f9bd73e692b5.webp

          好文章,我?在看?

          03d7821363cb37070fd1988f4c052698.webp
          瀏覽 64
          點(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√天堂资源中文8 |