您好,登錄后才能下訂單哦!
本篇內容主要講解“Java中對象是不是都分配在堆上”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“Java中對象是不是都分配在堆上”吧!
前言
我們在學習使用Java的過程中,一般認為new出來的對象都是被分配在堆上,但是這個結論不是那么的絕對,通過對Java對象分配的過程分析,可以知道有兩個地方會導致Java中new出來的對象并不一定分別在所認為的堆上。這兩個點分別是Java中的逃逸分析和TLAB(Thread Local Allocation Buffer)。本文首先對這兩者進行介紹,而后對Java對象分配過程進行介紹。
1. 逃逸分析
1.1 逃逸分析的定義
逃逸分析,是一種可以有效減少Java 程序中同步負載和內存堆分配壓力的跨函數全局數據流分析算法。通過逃逸分析,Java Hotspot編譯器能夠分析出一個新的對象的引用的使用范圍從而決定是否要將這個對象分配到堆上。
在計算機語言編譯器優化原理中,逃逸分析是指分析指針動態范圍的方法,它同編譯器優化原理的指針分析和外形分析相關聯。當變量(或者對象)在方法中分配后,其指針有可能被返回或者被全局引用,這樣就會被其他過程或者線程所引用,這種現象稱作指針(或者引用)的逃逸(Escape)。
Java在Java SE 6u23以及以后的版本中支持并默認開啟了逃逸分析的選項。Java的 HotSpot JIT編譯器,能夠在方法重載或者動態加載代碼的時候對代碼進行逃逸分析,同時Java對象在堆上分配和內置線程的特點使得逃逸分析成Java的重要功能。
1.2 逃逸分析的方法
Java Hotspot編譯器使用的是
[plain] view plain copy Choi J D, Gupta M, Serrano M, et al. Escape analysis for Java[J]. Acm Sigplan Notices, 1999, 34(10): 1-19.
Jong-Deok Choi, Manish Gupta, Mauricio Seffano,Vugranam C. Sreedhar, Sam Midkiff等在論文《Escape Analysis for Java》中描述的算法進行逃逸分析的。該算法引入了連通圖,用連通圖來構建對象和對象引用之間的可達性關系,并在次基礎上,提出一種組合數據流分析法。由于算法是上下文相關和流敏感的,并且模擬了對象任意層次的嵌套關系,所以分析精度較高,只是運行時間和內存消耗相對較大。
絕大多數逃逸分析的實現都基于一個所謂“封閉世界(closed world)”的前提:所有可能被執行的,方法在做逃逸分析前都已經得知,并且,程序的實際運行不會改變它們之間的調用關系 。但當真實的 Java 程序運行時,這樣的假設并不成立。Java 程序擁有的許多特性,例如動態類加載、調用本地函數以及反射程序調用等等,都將打破所謂“封閉世界”的約定。
不管是在“封閉世界”還是在“開放世界”,逃逸分析,作為一種算法而非編程語言的存在,吸引了國內外大量的學者對其進行研究。
1.3 逃逸分析后的處理
經過逃逸分析之后,可以得到三種對象的逃逸狀態。
GlobalEscape(全局逃逸), 即一個對象的引用逃出了方法或者線程。例如,一個對象的引用是復制給了一個類變量,或者存儲在在一個已經逃逸的對象當中,或者這個對象的引用作為方法的返回值返回給了調用方法。
ArgEscape(參數級逃逸),即在方法調用過程當中傳遞對象的應用給一個方法。這種狀態可以通過分析被調方法的二進制代碼確定。
NoEscape(沒有逃逸),一個可以進行標量替換的對象。可以不將這種對象分配在傳統的堆上。
編譯器可以使用逃逸分析的結果,對程序進行一下優化。
堆分配對象變成棧分配對象。一個方法當中的對象,對象的引用沒有發生逃逸,那么這個方法可能會被分配在棧內存上而非常見的堆內存上。
消除同步。線程同步的代價是相當高的,同步的后果是降低并發性和性能。逃逸分析可以判斷出某個對象是否始終只被一個線程訪問,如果只被一個線程訪問,那么對該對象的同步操作就可以轉化成沒有同步保護的操作,這樣就能大大提高并發程度和性能。
矢量替代。逃逸分析方法如果發現對象的內存存儲結構不需要連續進行的話,就可以將對象的部分甚至全部都保存在CPU寄存器內,這樣能大大提高訪問速度。
下面,我們看一下逃逸分析的例子。
class Main { public static void main(String[] args) { example(); } public static void example() { Foo foo = new Foo(); //alloc Bar bar = new Bar(); //alloc bar.setFoo(foo); } } class Foo {} class Bar { private Foo foo; public void setFoo(Foo foo) { this.foo = foo; } }
在這個例子當中,我們創建了兩個對象,Foo對象和Bar對象,同時我們把Foo對象的應用賦值給了Bar對象的方法。此時,如果Bar對在堆上就會引起Foo對象的逃逸,但是,在本例當中,編譯器通過逃逸分析,可以知道Bar對象沒有逃出example()方法,因此這也意味著Foo也沒有逃出example方法。因此,編譯器可以將這兩個對象分配到棧上。
1.4 編譯器經過逃逸分析的效果
測試代碼:
package com.yang.test2; /** * Created by yangzl2008 on 2015/1/29. */ class EscapeAnalysis { private static class Foo { private int x; private static int counter; public Foo() { x = (++counter); } } public static void main(String[] args) { long start = System.nanoTime(); for (int i = 0; i < 1000 * 1000 * 10; ++i) { Foo foo = new Foo(); } long end = System.nanoTime(); System.out.println("Time cost is " + (end - start)); } }
設置JVM運行參數:
未開啟逃逸分析設置為:
-server -verbose:gc
開啟逃逸分析設置為:
-server -verbose:gc -XX:+DoEscapeAnalysis
在未開啟逃逸分析的狀況下運行情況如下:
[GC 5376K->427K(63872K), 0.0006051 secs] [GC 5803K->427K(63872K), 0.0003928 secs] [GC 5803K->427K(63872K), 0.0003639 secs] [GC 5803K->427K(69248K), 0.0003770 secs] [GC 11179K->427K(69248K), 0.0003987 secs] [GC 11179K->427K(79552K), 0.0003817 secs] [GC 21931K->399K(79552K), 0.0004342 secs] [GC 21903K->399K(101120K), 0.0002175 secs] [GC 43343K->399K(101184K), 0.0001421 secs] Time cost is 58514571
開啟逃逸分析的狀況下,運行情況如下:
Time cost is 10031306
未開啟逃逸分析時,運行上訴代碼,JVM執行了GC操作,而在開啟逃逸分析情況下,JVM并沒有執行GC操作。同時,操作時間上,開啟逃逸分析的程序運行時間是未開啟逃逸分析時間的1/5。
2. TLAB
JVM在內存新生代Eden Space中開辟了一小塊線程私有的區域,稱作TLAB(Thread-local allocation buffer)。默認設定為占用Eden Space的1%。在Java程序中很多對象都是小對象且用過即丟,它們不存在線程共享也適合被快速GC,所以對于小對象通常JVM會優先分配在TLAB上,并且TLAB上的分配由于是線程私有所以沒有鎖開銷。因此在實踐中分配多個小對象的效率通常比分配一個大對象的效率要高。
也就是說,Java中每個線程都會有自己的緩沖區稱作TLAB(Thread-local allocation buffer),每個TLAB都只有一個線程可以操作,TLAB結合bump-the-pointer技術可以實現快速的對象分配,而不需要任何的鎖進行同步,也就是說,在對象分配的時候不用鎖住整個堆,而只需要在自己的緩沖區分配即可。
關于對象分配的JDK源碼可以參見JVM 之 Java對象創建[初始化]中對OpenJDK源碼的分析。
3. Java對象分配的過程
編譯器通過逃逸分析,確定對象是在棧上分配還是在堆上分配。如果是在堆上分配,則進入選項2.
如果tlab_top + size <= tlab_end,則在在TLAB上直接分配對象并增加tlab_top 的值,如果現有的TLAB不足以存放當前對象則3.
重新申請一個TLAB,并再次嘗試存放當前對象。如果放不下,則4.
在Eden區加鎖(這個區是多線程共享的),如果eden_top + size <= eden_end則將對象存放在Eden區,增加eden_top 的值,如果Eden區不足以存放,則5.
執行一次Young GC(minor collection)。
經過Young GC之后,如果Eden區任然不足以存放當前對象,則直接分配到老年代。
對象不在堆上分配主要的原因還是堆是共享的,在堆上分配有鎖的開銷。無論是TLAB還是棧都是線程私有的,私有即避免了競爭(當然也可能產生額外的問題例如可見性問題),這是典型的用空間換效率的做法。
到此,相信大家對“Java中對象是不是都分配在堆上”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。