對象分配過程
- 1)依據(jù)逃逸分析效览,判斷是否能棧上分配?
- 如果可以刹孔,使用標量替換方式啡省,把對象分配到
VM Stack
中。如果 線程銷毀或方法調(diào)用結(jié)束后髓霞,自動銷毀卦睹,不需要 GC 回收器 介入。 - 否則方库,繼續(xù)下一步结序。
- 如果可以刹孔,使用標量替換方式啡省,把對象分配到
- 2)判斷是否大對象?
- 如果是纵潦,直接分配到堆上
Old Generation
老年代上徐鹤。如果對象變?yōu)槔螅衫夏甏鶪C 收集器(比如 Parallel Old, CMS, G1)回收邀层。 - 否則返敬,繼續(xù)下一步。
- 如果是纵潦,直接分配到堆上
- 3)判斷是否可以在
TLAB
中分配寥院?- 如果是劲赠,在
TLAB
中分配堆上Eden
區(qū)。 - 否則,在
TLAB
外堆上的Eden
區(qū)分配凛澎。
- 如果是劲赠,在
棧上分配
本質(zhì)上是JVM提供的一個優(yōu)化技術(shù)霹肝。
- 基本思想:將線程私有的對象打散分配在棧
VM Stack
上 - 優(yōu)點:
- 可以在函數(shù)調(diào)用結(jié)束后自行銷毀對象,不需要垃圾回收器的介入塑煎,有效避免垃圾回收帶來的負面影響
- 棧上分配速度快沫换,提高系統(tǒng)性能
- 局限性:
- 棧空間小轧叽,對于大對象無法實現(xiàn)棧上分配
- 技術(shù)基礎:
逃逸分析
苗沧、標量替換
什么是逃逸分析?
關(guān)于 Java 逃逸分析的定義:
逃逸分析(Escape Analysis)簡單來講就是炭晒,Java Hotspot 虛擬機可以分析新創(chuàng)建對象的使用范圍待逞,并決定是否在 Java 堆上分配內(nèi)存的一項技術(shù)。
逃逸分析的 JVM 參數(shù)如下:
- 開啟逃逸分析:
-XX:+DoEscapeAnalysis
- 關(guān)閉逃逸分析:
-XX:-DoEscapeAnalysis
- 顯示分析結(jié)果:
-XX:+PrintEscapeAnalysis
逃逸分析技術(shù)在 Java SE 6u23+ 開始支持网严,并默認設置為啟用狀態(tài)识樱,可以不用額外加這個參數(shù)。
逃逸分析優(yōu)化
針對上面第三點震束,當一個對象沒有逃逸時怜庸,可以得到以下幾個虛擬機的優(yōu)化。
1) 鎖消除
我們知道線程同步鎖是非常犧牲性能的垢村,當編譯器確定當前對象只有當前線程使用割疾,那么就會移除該對象的同步鎖。
例如嘉栓,StringBuffer 和 Vector 都是用 synchronized 修飾線程安全的宏榕,但大部分情況下,它們都只是在當前線程中用到侵佃,這樣編譯器就會優(yōu)化移除掉這些鎖操作麻昼。
鎖消除的 JVM 參數(shù)如下:
- 開啟鎖消除:
-XX:+EliminateLocks
- 關(guān)閉鎖消除:
-XX:-EliminateLocks
鎖消除在 JDK8 中都是默認開啟的,并且鎖消除都要建立在逃逸分析的基礎上馋辈。
2) 標量替換
首先要明白標量和聚合量抚芦,基礎類型和對象的引用可以理解為標量,它們不能被進一步分解迈螟。而能被進一步分解的量就是聚合量叉抡,比如:對象。
對象是聚合量答毫,它又可以被進一步分解成標量褥民,將其成員變量分解為分散的變量,這就叫做標量替換
烙常。
這樣,如果一個對象沒有發(fā)生逃逸,那壓根就不用創(chuàng)建它蚕脏,只會在椪旄保或者寄存器上創(chuàng)建它用到的成員標量,節(jié)省了內(nèi)存空間驼鞭,也提升了應用程序性能秦驯。
標量替換的 JVM 參數(shù)如下:
- 開啟標量替換:
-XX:+EliminateAllocations
- 關(guān)閉標量替換:
-XX:-EliminateAllocations
- 顯示標量替換詳情:
-XX:+PrintEliminateAllocations
標量替換同樣在 JDK8 中都是默認開啟的,并且都要建立在逃逸分析的基礎上挣棕。
3) 棧上分配
當對象沒有發(fā)生逃逸時译隘,該對象就可以通過標量替換分解成成員標量分配在棧內(nèi)存中,和方法的生命周期一致洛心,隨著棧幀出棧時銷毀固耘,減少了 GC 壓力,提高了應用程序性能词身。
示例代碼
import java.time.Instant;
/**
* 棧上分配厅目,依賴于逃逸分析和標量替換
*
* @author Sven Augustus
*/
public class TestTLAB {
// private static User u;
/**
* 一個User對象的大小:markdown 8 + class pointer 4 + int 4 + string (oops) 4 + padding 4 = 24B <br> 如果分配 100_000_000 個法严,則需要
* 2400_000_000 字節(jié)损敷, 約 2.24 GB。
*/
static class User {
private int id;
private String name;
public User(int id, String name) {
this.id = id;
this.name = name;
}
}
private static void alloc() {
User u = new User(1, "SvenAugustus");
// u = new User(1, "SvenAugustus");
}
public static void main(String[] args) throws InterruptedException {
long start = Instant.now().toEpochMilli();
for (int i = 0; i < 100_000_000; i++) {
alloc();
}
System.out.println(Instant.now().toEpochMilli() - start);
}
}
上述代碼調(diào)用了1億次alloc()深啤,如果是分配到堆上拗馒,大概需要 2.2 GB的堆空間,如果堆空間小于該值溯街,必然會觸發(fā)GC诱桂。
使用如下VM參數(shù)運行,發(fā)現(xiàn)不會觸發(fā)GC:
-server -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-UseTLAB -XX:+EliminateAllocations
使用如下參數(shù)(任意一行)運行苫幢,會發(fā)現(xiàn)觸大量 GC:
//不使用逃逸分析
-server -Xmx15m -Xms15m -XX:+PrintGCDetails -XX:-UseTLAB -XX:-DoEscapeAnalysis -XX:+EliminateAllocations
//不使用標量替換
-server -Xmx15m -Xms15m -XX:+PrintGCDetails -XX:-UseTLAB -XX:+DoEscapeAnalysis -XX:-EliminateAllocations
TLAB 分配
TLAB访诱,全稱Thread Local Allocation Buffer, 即:線程本地分配緩存。這是一塊線程專用的內(nèi)存分配區(qū)域韩肝。
TLAB占用的是eden區(qū)的空間触菜。
在TLAB啟用的情況下(默認開啟),JVM會為每一個線程分配一塊TLAB區(qū)域哀峻。
為什么需要TLAB涡相?
這是為了加速對象的分配。
由于對象一般分配在堆上剩蟀,而堆是線程共用的催蝗,因此可能會有多個線程在堆上申請空間,而每一次的對象分配都必須線程同步育特,會使分配的效率下降丙号。
考慮到對象分配幾乎是Java中最常用的操作先朦,因此JVM使用了TLAB這樣的線程專有區(qū)域來避免多線程沖突,提高對象分配的效率犬缨。
- 局限性: TLAB空間一般不會太大(占用eden區(qū))喳魏,所以大對象無法進行
TLAB
分配,只能直接分配到堆Heap
上怀薛。
大對象
大對象的 JVM 參數(shù)如下:
- 大對象到底多大:
-XX:PreTenureSizeThreshold=n
(僅適用于DefNew
/ParNew
新生代垃圾回收器 ) https://bugs.openjdk.java.net/browse/JDK-8050209 -
G1
回收器的大對象判斷刺彩,則依據(jù)Region
的大小(-XX:G1HeapRegionSize
)來判斷枝恋,如果對象大于Region
50%以上创倔,就判斷為大對象Humongous Object
。
by Sven Augustus https://my.oschina.net/langxSpirit