JVM解剖樂園

clipboard.png

1、JVM鎖粗化和循環
原文標題:JVM Anatomy Quark #1: Lock Coarsening and Loops

衆所周知Hotsport編譯器會進行JVM鎖粗化和優化,它將相鄰的鎖區塊進行合併,有效減少鎖的的佔用成本,類似

synchronized (obj) {
  // statements 1
}
synchronized (obj) {
  // statements 2
}

優化成

synchronized (obj) {
  // statements 1
  // statements 2
}

那麼在循環體中是否也會進行相同的優化?類似

for (...) {
  synchronized (obj) {
    // something
  }
}

優化成

synchronized (this) {
  for (...) {
     // something
  }
}   

實際上是不會的,理論上來說是可以的,這有點像針對鎖的循環無關代碼外提。然而如此優化的缺點是將鎖的粒度增加太多,線程在執行循環時將會長時間獨佔鎖

翻譯修改摘錄自:

https://shipilev.net/jvm/anat...

2、透明大頁
原文標題:JVM Anatomy Quark #2: Transparent Huge Pages

進程都擁有自己的虛擬內存空間,虛擬內存空間會映射到實際內存。例如,兩個進程可以在相同的虛擬地址 0x42424242 中存儲不同數據,這些數據實際存放在不同的物理內存中。當程序訪問該地址時,通過某種機制會把虛擬地址轉換成實際物理地址

這個過程一般通過由操作系統維護的頁表實現,硬件通過"遍歷頁表"進行地址轉換。雖然以頁面爲單位進行地址轉換更容易,但由於每次訪問內存都會發生地址轉換會帶來不小開銷。爲此,引入TLB(轉換查找緩衝)緩存最近的轉換記錄。TLB要求至少要與 L1 緩存一樣快,因此通常緩存少於100條。對工作負載較大的情況,TLB缺失和由此引發的頁表遍歷需要很多時間

TLB容量比較小,但是我們可以將地址轉換的頁面容量增大,這個可以藉助系統內核的透明大頁機制輕鬆做到,那這樣是否會對性能有所幫助呢?

實際上它能有效提高應用程序性能,特別是當程序擁有大量數據和堆棧時

翻譯修改摘錄自:

https://shipilev.net/jvm/anat...

3、GC設計和停頓

原文標題:JVM Anatomy Quark #3: GC Design and Pauses

常見GC算法如下所示,其中黃色爲stop-the-world階段,綠色爲併發階段

clipboard.png

需要注意不同收集器在常規GC循環中何時會暫停

翻譯修改摘錄自:

https://shipilev.net/jvm/anat...

4、TLAB內存分配
原文標題:JVM Anatomy Quark #4: TLAB allocation

本小節將揭曉,什麼是Bump-the-pointer技術跟蹤?什麼是TLAB內存分配?

Bump-the-pointer技術跟蹤在eden區創建的最後一件對象,最後該對象會放在eden頂部,之後再創建對象時,只需要檢查最後一個對象就可以知道eden空間容量是否足夠,但是在多線程環境中就會出現問題,不過加鎖同步開銷太大,於是提出TLAB

TLAB(Thread-local allocation buffer)緩衝區,特點是每個線程獨享一份,也就意味着不存在數據共享也就不需要加鎖同步,同時它結合了Bump-the-pointer跟蹤技術實現快速的對象分配

翻譯修改摘錄自:

https://shipilev.net/jvm/anat...

5、TLAB與堆可解析性

原文標題:JVM Anatomy Quark #5: TLABs and Heap Parsability

好的垃圾回收器通常會保證堆的可解析性,意味着它不需要複雜的數據結構也能以某種方式解析成對象或者字段。雖然嚴格來說,它在分配週期中並不是始終以對象流的方式存在,但是它使得GC實現、測試、調戲變得輕易

翻譯修改摘錄自:

https://shipilev.net/jvm/anat...

6、創建對象階段
原文標題:JVM Anatomy Quark #6: New Object Stages

你可能聽說過分配並不是初始化。但是 Java 有構造方法!構造方法是分配?還是初始化?

Java語言中的new對應很多字節碼指令,比如

public Object t() {
  return new Object();
}

編譯爲

 public java.lang.Object t();
    descriptor: ()Ljava/lang/Object;
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: new           #4                  // class java/lang/Object
         3: dup
         4: invokespecial #1                  // Method java/lang/Object."<init>":()V
         7: areturn

給人感覺是,new關鍵會執行分配資源和系統初始化,同時調用構造方法執行用戶初始化,但是聰明的虛擬機會進行優化,比如在構造方法執行完成之前觀察對象使用情況然後選擇性合併任務

翻譯修改摘錄自:

https://shipilev.net/jvm/anat...

7、初始化開銷
原文標題:JVM Anatomy Quark #7: Initialization Costs

初始化對象或者數組是實例化過程中最主要的開銷,使用TLAB分配,對象或者數據初始化的開銷取決於元數據寫入和內容的初始化

翻譯修改摘錄自:

https://shipilev.net/jvm/anat...

8、局部變量可用性
原文標題:JVM Anatomy Quark #8: Local Variable Reachability

離開了當前作用域,存儲在局部變量中的引用纔會被回收,這種說法正確嗎?在Java中並非如此,Java局部變量的可用性不由代碼塊決定,而與最後一次使用有關,並且可能會持續到最後一次使用爲止。使用像finalizer、強引用、弱引用、虛引用這樣的方法通知對象不可達,會受到“提前檢查”優化帶來的影響,使得代碼塊還沒有結束變量可能已不可用,這是一種很好的特性,使得GC能提前回收掉本地分配的大量緩存

當然如果想獲得C++編程那種代碼塊結束時才釋放的特性,你可以使用try-finally

翻譯修改摘錄自:

https://shipilev.net/jvm/anat...

9、JNI 臨界區 與 GC 鎖
原文標題:JVM Anatomy Quark #9: JNI Critical and GC Locker

10、String中的intern方法
原文標題:JVM Anatomy Quark #10: String.intern()

我們知道intern方法會從字符串常量池中查詢當前字符串是否存在,若不存在就會將當前字符串放入常量池中,從而使得字符串對象被緩存了一樣

JAVA使用JNI調用c++實現的StringTable的intern方法, StringTable的intern方法跟Java中的HashMap的實現是差不多的, 只是不能自動擴容。默認大小是1009

要注意的是,String的String Pool是一個固定大小的Hashtable,默認值大小長度是1009,如果放進String Pool的String非常多,就會造成Hash衝突嚴重,從而導致鏈表會很長,而鏈表長了後直接造成的影響就是調用String.intern時性能會大幅下降

翻譯修改摘錄自:

https://shipilev.net/jvm/anat...

11、移動GC與局部性
原文標題:JVM Anatomy Quark #11: Moving GC and Locality

標記-壓縮回收器可以保持堆中對象的分配順序,也可以對其任意重排。雖然任意順序能夠比其他標記-壓縮回收器速度更快,也不會帶來空間開銷,但是會破壞應用線程的局部性

翻譯修改摘錄自:

https://shipilev.net/jvm/anat...

12、本地內存跟蹤
原文標題:JVM Anatomy Quark #12: Native Memory Tracking

JVM的默認配置通常是爲長時間運行的服務器應用準備的,包括GC、內部數據結構的初始大小、堆棧大小等也是如此,而通過NMT探索虛擬機內存分配情況能讓我們立刻知道從哪裏入手優化應用佔用的內存,同時非常有助於在應用實際生產環境中調整JVM參數

翻譯修改摘錄自:

https://shipilev.net/jvm/anat...

13、屏障
原文標題:JVM Anatomy Quark #13: Intergenerational Barriers

GC通常會有屏障組,即使沒有實際發生回收,這些屏障也會影響應用程序的性能。即使串行、並行這樣非常基本的分代收集器,也至少有一個引用存儲屏障,而像G1這樣更高級的回收器會有更復雜的屏障跟蹤不同區域間的引用。某些情況下,這種開銷讓人非常痛苦

翻譯修改摘錄自:

https://shipilev.net/jvm/anat...

14、常量變量
原文標題:JVM Anatomy Quark #14: Constant Variables

停留2秒思考下面的代碼塊會輸出什麼

import java.lang.reflect.Field;

public class ConstantValues {

final int fieldInit = 42;
final int instanceInit;
final int constructor;

{
    instanceInit = 42;
}

public ConstantValues() {
    constructor = 42;
}

static void set(ConstantValues p, String field) throws Exception {
    Field f = ConstantValues.class.getDeclaredField(field);
    f.setAccessible(true);
    f.setInt(p, 9000);
}

public static void main(String... args) throws Exception {
    ConstantValues p = new ConstantValues();

    set(p, "fieldInit");
    set(p, "instanceInit");
    set(p, "constructor");

    System.out.println(p.fieldInit + " " + p.instanceInit + " " + p.constructor);
}

}

正常會打印出42 9000 9000,也就是說即使通過反射重寫了fieldInt字段的值,我們也無法觀察到最新的值,而更新另外兩個字段生效了,這個奇怪結果的解釋是方法內聯

翻譯修改摘錄自:

https://shipilev.net/jvm/anat...

15、即時常量

編譯器信任static final字段,因爲這個值不依賴特定對象,而且是不能改變的

https://shipilev.net/jvm/anat...

16、超多態虛調用
https://shipilev.net/jvm/anat...

17、信任非靜態Final字段

原文標題:JVM Anatomy Quark #17: Trust Nonstatic Final Fields

   class M {
      final int x;
      M(int x) { this.x = x; }
    }
    
    static final M KNOWN_M = new M(1337);
    
    void work() {
      // We know exactly the slot that holds the variable, can we just
      // inline the value 1337 here?
      return KNOWN_M.x;
    }

上面這段代碼是否會進行方法內聯優化呢?實際上是不會的,如果要信任實例final字段,那麼必須知道當前操作的對象,然而上面那段代碼是引用關係

翻譯修改摘錄自:

https://shipilev.net/jvm/anat...

18、字面量替換

原文標題:JVM Anatomy Quark #18: Scalar Replacement

利用逃逸分析然後編譯器優化可以實現在棧上分配而不是堆上分配,方法退出後直接彈出釋放,無助藉助垃圾回收器處理,很神奇,對嗎?

不過一旦發生了逃逸現象,我們需要將實體對象完整地複製到堆中。而且由於實現起來需要更改大量假設了"對象只能在堆上分配"的代碼,因爲HotSpot虛擬機並沒有採用棧上分配,而是標量替換這麼一項技術。這個優化技術,可以看到將原本對對象的字段訪問,替換爲一個局部變量的訪問。該對象沒有被實際分配,因此和棧上分配一樣,它同樣可以減輕垃圾回收的壓力

翻譯修改摘錄自:

https://shipilev.net/jvm/anat...

19、鎖消除
原文標題:JVM Anatomy Quark #19: Lock Elision

目前的內存模型中,對不共享的對象進行加鎖操作是無效的,編譯器不會對它做任何事情。由於其他線程不能獲取該鎖對象,因此也無法基於該鎖對象構造兩個線程之間的happens-before規則。那麼編譯器只需證明鎖對象不會發生逃逸,便可以進行鎖消除

翻譯修改摘錄自:

https://shipilev.net/jvm/anat...

20、FPU溢出
原文標題:JVM Anatomy Quark #20: FPU Spills

寄存器分配器的職責是,維護在特定的編譯單元中程序需要的所有操作數的程序表示,並且映射這些虛操作數到實際的機器寄存器,也就是爲它們分配寄存器。在許多真實的程序中,在給定程序位置,虛操作數的數量會大於可用機器寄存器的數量,那麼寄存器分配器就需要將某些操作數放到寄存器之外的其它位置比如放到棧上,這種就稱爲FPU溢出,有效緩解了寄存器壓力

翻譯修改摘錄自:

https://shipilev.net/jvm/anat...

21、堆內存歸還
原文標題:JVM Anatomy Quark #21: Heap Uncommit

許多GC已經實現了在合適的時機歸還堆內存:Shenandoah異步執行堆內存歸還,即使沒有GC請求;G1在顯式GC請求中執行堆內存歸還;Serial和Parallel在某些條件下也會執行。不過歸還內存可能會耗費一些時間,所以實際的實現會在歸還內存之前會增加一個超時時間

翻譯修改摘錄自:

https://shipilev.net/jvm/anat...

22、安全點檢查
原文標題:JVM Anatomy Quark #22: Safepoint Polls

在大部分機器上停止運行的線程實際上是很簡單的:向線程發送一個信號,強制處理器中斷,停止線程正在執行的操作,將控制權轉交給別處。然而,這還不足以讓Java線程在任意位置停止,特別是如果你需要精確的垃圾回收。在這種情況下,你需要知道寄存器和棧中的內容,這些內容可能是你需要處理的對象引用。或者如果你想要取消偏向鎖,你需要精確的知道線程的狀態和獲取的鎖

因此Hotspot實現了協作機制:線程經常詢問是否應該將控制權交給VM,在線程生命週期中某些已知的位置,線程的狀態是已知的。當所有線程都在已知的位置停止的時候,VM 被認爲是到達了安全點。檢查安全點請求的代碼片段因此被稱爲安全點檢查

翻譯修改摘錄自:

https://shipilev.net/jvm/anat...

23、壓縮引用
原文標題:JVM Anatomy Quark #23: Compressed References

大部分JVM實現將Java引用轉換爲機器指針,沒有額外的迂迴,這簡化了性能問題,不過通常情況下會使得引用的表示比機器指針的寬度小,也就是進行壓縮引用,比如你可以使用XX:+UseCompressedOops選項,使得在64位系統中對象指針可以使用32bit的Compressed版本。壓縮方法可以是比特右移,稱爲“基於零的壓縮普通對象指針”,但是基於零的壓縮引用仍然依賴堆內存映射在較低地址的假設。如果不是,我們可以使用非零的堆內存起始地址來解碼

翻譯修改摘錄自:

https://shipilev.net/jvm/anat...

24、對象對齊
原文標題:JVM Anatomy Quark #24: Object Alignment

許多硬件實現要求對數據的訪問是對齊的,也就是N字節寬度數據的訪問地址總是N的倍數,否則會直接拒絕操作,產生SIGBUS信號或者其他硬件異常

在Hotspot中最小的對象對齊是8字節,我們可以通過-XX:ObjectAlignmentInBytes選項進行調整,不過會有正面和負面的後果

負面的後果是每個對象平均的內存空間浪費將會增加,如果啓用壓縮引用,這個增加會變得不那麼明顯,不過內存對齊會導致壓縮引用閾值被移動,因爲它依賴引用中有多少低位比特是零,這很有趣,總之,利器當慎用

翻譯修改摘錄自:

https://shipilev.net/jvm/anat...

文章來源:www.liangsonghua.me

作者介紹:京東資深工程師-樑鬆華,在穩定性保障、敏捷開發、JAVA高級、微服務架構方面有深入的理解

clipboard.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章