之前的文章
對象創建的內存分配
在對象創建的時候給對象分配內存總共是可能有如下的幾種可能:
(1)將對象分配在棧上 (2)使用TLAB (3)分配在elden
我們一點一點的來説下,每一點展開都是個知識點
棧上分配
這裏需要先説的一個是逃逸分析,在計算機語言編譯器優化原理中,逃逸分析是指分析指針動態範圍的方法,它同編譯器優化原理的指針分析和外形分析相關聯。當變量(或者對象)在方法中分配後,其指針有可能被返回或者被全局引用,這樣就會被其他過程或者線程所引用,這種現象稱作指針(或者引用)的逃逸(Escape)。而hotspot能夠在動態加載方法的時候對代碼進行逃逸分析,如果發現一個新對象的引用僅僅是在這個方法的範圍內,那麼這個對象的分配區域就會僅僅在棧上。為了證明我説的是對的,我需要用一段代碼來證明一下,在代碼開始之前,我先簡單介紹幾個會用到的jvm的參數,不然你可能會比較懵逼。
- -Xss 這個參數是指明棧空間的大小,我們這裏為了讓一個棧有足夠的大小,因此給2m的大小
- -Xms 這個是堆的初始化大小
- -Xmx 這個是堆的最大大小
- -XX:+PrintGCDetails 開啟打印垃圾回收日誌
- -XX:+UseConcMarkSweepGC 使用CMS垃圾回收器
- -XX:+PrintGCDateStamps 打印GC發生的時間,用的humanbeing的方式
- -XX:-DoEscapeAnalysis 關閉逃逸分析
我們仔細觀察上面的參數,發現有的參數前面帶着+或者-,jvm的參數有兩類,一類是需要設置具體的值的,另外一類只是單純的開啟和關閉的,
單純的開啟和關閉就用的是+和-,+指的是開啟,-指的是關閉,設置值的在後面跟上值就行了
複製代碼
上面是我使用的參數,具體的設置如下:
-Xss2048k
-Xms50m
-Xmx50m
-XX:+PrintGCDetails
-XX:+UseConcMarkSweepGC
-XX:+PrintGCDateStamps
-XX:-DoEscapeAnalysis
複製代碼
代碼如下:
public static void main(String[] args) throws IOException {
while (true) {
new MyEntity(1, "a");
}
}
// MyEntity類如下
public class MyEntity {
private Integer id;
private String name;
public MyEntity(Integer id, String name) {
this.id = id;
this.name = name;
}
}
複製代碼
然後我們開始跑main方法,如我們想的,GC會瘋狂運行,因為我們關閉了逃逸分析,GC日誌如下:
我們先不在本章教你讀GC日誌(這是個很長的話題),這裏貼出來的目的只是證明關閉逃逸分析的影響而已,我們再從另外一方面證明堆內存中有很大量的MyEntity對象,我們打開終端,windows是打開cmd,輸入jvisualVM,它的界面如下:
雙擊我畫紅框的進程,這個進程就是我們正在跑着的java程序,打開之後界面如下:
按順序選擇我畫紅框的按鈕,之後你可以看到的如下圖:
可以看到MyEntity實例數量在堆內存中非常多,其實它的數量應該是一會兒多一會兒少,因為在每次GC的時候總會有很多被回收掉(GC的細節我們下篇文章開始説)。
接下來我們證明下,逃逸分析開啟之後,對象是有可能被分配到棧上的。
參數如下:注意僅僅修改最後一項的-為+號
-Xss2048k
-Xms50m
-Xmx50m
-XX:+PrintGCDetails
-XX:+UseConcMarkSweepGC
-XX:+PrintGCDateStamps
-XX:+DoEscapeAnalysis
複製代碼
代碼和上面一樣,但是這次跑起來根本沒有GC日誌輸出,我的IDEA控制枱乾乾淨淨一片:
同時,繼續看jvisualVM的內存監控如下,你會發現內存中根本沒有MyEntity的實例
TLAB分配
TLAB全稱是Thread Local Allocate Buffer,即線程本地分配緩存區,這是一個線程專用的內存分配區域。 由於對象一般會分配在堆上,而堆是所有線程共享的。因此在同一時間,可能會有多個線程在堆上申請空間。因此,每次對象分配都必須要進行同步(jvm採用CAS配上失敗重試的方式保證更新操作的原子性),而在競爭激烈的場合分配的效率又會進一步下降。JVM使用TLAB來避免多線程衝突,在給對象分配內存時,每個線程使用自己的TLAB,這樣可以避免線程同步,提高了對象分配的效率。
TLAB本身佔用Eden區空間,在開啟TLAB的情況下,虛擬機會為每個Java線程分配一塊TLAB空間。參數-XX:+UseTLAB開啟TLAB,默認是開啟的。TLAB空間的內存非常小,缺省情況下僅佔有整個Eden空間的1%,當然可以通過選項-XX:TLABWasteTargetPercent設置TLAB空間所佔用Eden空間的百分比大小。
由於TLAB空間一般不會很大,因此大對象無法在TLAB上進行分配,總是會直接分配在堆上。TLAB空間由於比較小,因此很容易裝滿。比如,一個100K的空間,已經使用了80KB,當需要再分配一個30KB的對象時,肯定就無能為力了。這時虛擬機會有兩種選擇,第一,廢棄當前TLAB,這樣就會浪費20KB空間;第二,將這30KB的對象直接分配在堆上,保留當前的TLAB,這樣可以希望將來有小於20KB的對象分配請求可以直接使用這塊空間。實際上虛擬機內部會維護一個叫作refill_waste的值,當請求對象大於refill_waste時,會選擇在堆中分配,若小於該值,則會廢棄當前TLAB,新建TLAB來分配對象。這個閾值可以使用TLABRefillWasteFraction來調整,它表示TLAB中允許產生這種浪費的比例。默認值為64,即表示使用約為1/64的TLAB空間作為refill_waste。默認情況下,TLAB和refill_waste都會在運行時不斷調整的,使系統的運行狀態達到最優。如果想要禁用自動調整TLAB的大小,可以使用-XX:-ResizeTLAB禁用ResizeTLAB,並使用-XX:TLABSize手工指定一個TLAB的大小。 -XX:+PrintTLAB可以跟蹤TLAB的使用情況。
一般不建議手工修改TLAB相關參數,推薦使用虛擬機默認行為。
為什麼説不推薦修改TLAB相關的東西呢?這是因為TLAB的優化是極其難控制的,在不同的業務場景下對象創建的情況差別會非常大,
因此我們一般不會優化這裏的參數,只是使用默認的參數。
複製代碼
分配在elden區域
當jvm判斷不能使用前兩種分配方式的時候就會觸發這種分配方式,在這種情況下,會有兩種選擇:
1、指針碰撞:所謂的連續內存是指Java堆中的內存是絕對規整的,用過的內存在一邊,空閒的內存在另一邊。中間有個指針作為分界點,
這時如果要分配新內存,只要指針向空閒的內存一方移動一下就可以了。這種分配內存的方式就叫指針碰撞。
2、空閒列表:如果Java堆中的內存並不是完整的,也就是不是連續的。這時使用的內存和空閒的內存沒有任何規則,無法用指針碰撞的方
式來分配內存。這時虛擬機只能採取其它辦法來標識出哪些內存是使用的,哪些內存是空閒的,所以虛擬機就要維護一個列表,用來存儲哪些
內存是空閒的,分配內存時,只要從列表中劃分一塊區域存儲對象實例,並更新列表上的記錄就可以了。這種方式就叫空閒列表
複製代碼
具體使用的是哪種,取決於使用的垃圾回收器使用的哪種算法,一般來説,我們的hotspot在使用CMS和G1垃圾回收器的時候都是用的第二種。
其實分配在eden這種説法並不絕對,因為當一個對象非常大大到了eden都放不下的時候,這時候還要保證這個分配一定成功,這時候就會讓這個對象進入老年代,這個是jvm的內存分配擔保機制。
對象創建時候的內存分配就説這麼多,下一篇我們開始説垃圾回收