JVM解剖公園

文章首發於公衆號:松花皮蛋的黑板報
做者就任於京東,在穩定性保障、敏捷開發、高級JAVA、微服務架構有深刻的理解java

clipboard.png

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

衆所周知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...數據結構

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

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

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

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

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

翻譯修改摘錄自:

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

三、GC設計和停頓

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

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

clipboard.png

須要注意不一樣收集器在常規GC循環中什麼時候會暫停

翻譯修改摘錄自:

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

四、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...

五、TLAB與堆可解析性

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

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

翻譯修改摘錄自:

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

六、建立對象階段
原文標題: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...

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

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

翻譯修改摘錄自:

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

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

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

固然若是想得到C++編程那種代碼塊結束時才釋放的特性,你可使用try-finally

翻譯修改摘錄自:

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

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

十、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...

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

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

翻譯修改摘錄自:

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

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

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

翻譯修改摘錄自:

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

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

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

翻譯修改摘錄自:

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

1四、常量變量
原文標題: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...

1五、即時常量

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

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

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

1七、信任非靜態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...

1八、字面量替換

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

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

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

翻譯修改摘錄自:

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

1九、鎖消除
原文標題: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...

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

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

翻譯修改摘錄自:

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

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

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

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

翻譯修改摘錄自:

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

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

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

翻譯修改摘錄自:

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

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

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

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

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

翻譯修改摘錄自:

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

文章來源:www.liangsonghua.me

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

clipboard.png

相關文章
相關標籤/搜索