new 的對象如何不分配在堆而分配在棧上(方法逃逸等)


當能夠明確對象不會發生逃逸時,就可以對這個對象做一個優化,不將其分配到堆上,而是直接分配到棧上,這樣在方法結束時,這個對象就會隨着方法的出棧而銷毀,這樣就可以減少垃圾回收的壓力。

 

如方法逃逸。

 

逃逸分析,是一種可以有效減少Java 程序中同步負載和內存堆分配壓力的跨函數全局數據流分析算法。

 

通過逃逸分析,Java Hotspot編譯器能夠分析出一個新的對象的引用使用范圍從而決定是否要將這個對象分配到上。 


在計算機語言編譯器優化原理中,逃逸分析是指分析指針動態范圍的方法,它同編譯器優化原理的指針分析和外形分析相關聯。

變量(或者對象)在方法中分配后,其指針有可能被返回或者被全局引用,這樣就會被其他過程或者線程所引用,這種現象稱作指針(或者引用)的逃逸(Escape)

 
Java在Java SE 6u23以及以后的版本中支持並默認開啟了逃逸分析的選項。Java的 HotSpot JIT編譯器,能夠在方法重載或者動態加載代碼的時候對代碼進行逃逸分析,同時Java對象在堆上分配和內置線程的特點使得逃逸分析成Java的重要功能。

 

通過逃逸分析來決定某些實例或者變量是否要在中進行分配,如果開啟了逃逸分析,即可將這些變量直接在上進行分配,而非堆上進行分配。這些變量的指針可以被全局所引用,或者其其它線程所引用。

 

上的空間一般而言是非常小的,只能存放若干變化的數據結構,大容量的存儲結構是做不到。


所以,逃逸分析的效果只能在特定場景下,滿足高頻高數量的容量比較小的變量分配結構,才可以生效。

 

在Java中每一個對象都有一定的作用域,理論上,一個對象在一塊代碼中構造,那么也應該在這塊代碼中被回收,但是實際上,我們經常會讓一個對象存活更長的時間,超過定義它的代碼塊,這就好比一個人逃出了生他養他的地方,我們將這種現象稱為逃逸

 

逃逸按照行為不同有可以分為方法逃逸線程逃逸

方法逃逸是指在某一個方法中構造的對象,在該方法外部可以繼續訪問這個對象。產生方法逃逸一般是由於返回值返回,或者是將對象的引用設置到傳入的參數中,如下圖展示了兩種產生方法逃逸的例子。

 

線程逃逸則是在一個線程中構造的對象,能夠在另一個線程中使用。這種情況是由於同一個對象被多個線程使用,產生資源占用而導致。下圖就是一個通過共享靜態變量,來實現線程逃逸的例子,這個例子中的resource對象在多線程的環境下會產生線程逃逸。

 

 

 

 

至此,我們了解了如何判斷一個對象是否會產生逃逸,那么對象逃逸有什么用呢?如果我們能夠明確肯定一個對象不會產生逃逸,那么就可以對其進行很多的優化。下面本文就來介紹一下,Java虛擬機在確定對象不發生逃逸的情況下,所進行的一些高效的優化。

 

  1. 棧上分配

    眾所周知,Java中對象時分配在堆上的,在初始化時,會在堆上分配一塊空間,當這個對象不再使用時,會在之后發生垃圾回收時被回收,這是一個Java對象正常的生命周期。但是當能夠明確對象不會發生逃逸時,就可以對這個對象做一個優化,不將其分配到堆上,而是直接分配到上,這樣在方法結束時,這個對象就會隨着方法的出棧銷毀,這樣就可以減少垃圾回收的壓力

  2. 同步消除

    多線程中,對於一個變量操作進行同步操作是效率很低的,當我們確定一個對象不會發生逃逸時,那么就沒有必要對這個對象進行同步操作,所以如果代碼中有對這種變量操作的同步操作,JVM將會取消同步,從而提升性能。

  3. 標量替換

    標量指的是沒有辦法再分解為更小的數據的類型,即Java中的基本類型,我們平時定義的類都屬於聚合量。標量替換即是將一個聚合量拆成多個標量來替換,即用一些基本類型來代替一個對象。如果明確對象不會發生逃逸,並且可以進行標量替換的話,那么就可以不創建這個對象,而是直接使用基本類型來代替,這樣也就可以節省創建和銷毀對象的開銷。

 

雖然基於逃逸技術的優化能夠提升程序運行時的性能,但是在實際生產中,對象逃逸的分析默認是不開啟的。這是因為分析一個對象是否會發生逃逸消耗比較大,所以,開啟逃逸分析並進行這些優化之后得到的效果,並不一定就比不進行優化更好。如果確定開啟逃逸分析效率更好,那么可以使用參數-XX:+DoEscapeAnalysis來開啟逃逸分析。

 

 

TLAB

JVM在內存在新生代Eden Space中開辟了一小塊線程私有的區域,稱作TLAB(Thread-local allocation(美: [.ælə'keɪʃ(ə)n] ) buffer)
默認設定為占用Eden Space的1%。在Java程序中很多對象都是小對象且用過即丟,它們不存在線程共享也適合被快速GC,所以對於小對象通常JVM會優先分配在TLAB上,並且TLAB上的分配由於是線程私有所以沒有鎖開銷。因此在實踐中分配多個小對象的效率通常比分配一個大對象的效率要高。
也就是說,Java中每個線程都會有自己的緩沖區稱作TLAB(Thread-local allocation buffer),每個TLAB都只有一個線程可以操作,TLAB結合bump-the-pointer技術可以實現快速的對象分配,而不需要任何的鎖進行同步,也就是說,在對象分配的時候不用鎖住整個堆,而只需要在自己的緩沖區分配即可。
 
總結: 多個線程同時new對象會分配內存, 容易造成線程安全問題(多個線程操作同一塊內存), 所以多個線程new小對象時, 如果使用同一塊內存, 則需要加鎖, 但是如果每個線程有一些自己new對象分配內存獨有的區域(TLAB), 就不用加鎖, 沒有鎖開銷, 也是提高了效率.
 

Java對象分配的過程

  1. 編譯器通過逃逸分析,確定對象是在上分配還是在上分配。如果是在堆上分配,則進入選項2.
  2. 如果tlab_top + size <= tlab_end,則在在TLAB上直接分配對象並增加tlab_top 的值,如果現有的TLAB不足以存放當前對象則3.
  3. 重新申請一個TLAB,並再次嘗試存放當前對象。如果放不下,則4.
  4. 在Eden區加鎖(這個區是多線程共享的),如果eden_top + size <= eden_end則將對象存放在Eden區,增加eden_top 的值,如果Eden區不足以存放,則5.
  5. 執行一次Young GC(minor collection)。
  6. 經過Young GC之后,如果Eden區任然不足以存放當前對象,則直接分配到老年代。

 

 

來源:https://blog.csdn.net/blueheart20/article/details/76167489

來源:https://blog.csdn.net/u011277123/article/details/53908270

來源:https://blog.csdn.net/yangzl2008/article/details/43202969


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM