松花皮蛋的黑板報
  • 分享在京東工作的技術感悟,還有JAVA技術和業內最佳實踐,大部分都是務實的、能看懂的、可復現的

掃一掃
關注公眾號

ARTS-22-JVM解剖公園

博客首頁文章列表 松花皮蛋me 2019-08-11 22:00

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/anatomy-quarks/1-lock-coarsening-for-loops

2、透明大頁

原文標題:JVM Anatomy Quark #2: Transparent Huge Pages

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

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

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

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

翻譯修改摘錄自:

https://shipilev.net/jvm/anatomy-quarks/2-transparent-huge-pages/

3、GC設計和停頓

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

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

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

翻譯修改摘錄自:

https://shipilev.net/jvm/anatomy-quarks/3-gc-design-and-pauses/

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/anatomy-quarks/4-tlab-allocation/

5、TLAB與堆可解析性

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

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

翻譯修改摘錄自:

https://shipilev.net/jvm/anatomy-quarks/5-tlabs-and-heap-parsability/

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/anatomy-quarks/6-new-object-stages/

7、初始化開銷

原文標題:JVM Anatomy Quark #7: Initialization Costs

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

翻譯修改摘錄自:

https://shipilev.net/jvm/anatomy-quarks/7-initialization-costs/

8、局部變量可用性

原文標題:JVM Anatomy Quark #8: Local Variable Reachability

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

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

翻譯修改摘錄自:

https://shipilev.net/jvm/anatomy-quarks/8-local-var-reachability/

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/anatomy-quarks/10-string-intern/

11、移動GC與局部性

原文標題:JVM Anatomy Quark #11: Moving GC and Locality

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

翻譯修改摘錄自:

https://shipilev.net/jvm/anatomy-quarks/11-moving-gc-locality/

12、本地內存跟蹤

原文標題:JVM Anatomy Quark #12: Native Memory Tracking

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

翻譯修改摘錄自:https://shipilev.net/jvm/anatomy-quarks/12-native-memory-tracking/

13、屏障

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

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

翻譯修改摘錄自:

https://shipilev.net/jvm/anatomy-quarks/13-intergenerational-barriers/

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/anatomy-quarks/14-constant-variables/

15、即時常量

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

https://shipilev.net/jvm/anatomy-quarks/15-just-in-time-constants

16、超多態虛調用

https://shipilev.net/jvm/anatomy-quarks/16-megamorphic-virtual-calls/

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/anatomy-quarks/17-trust-nonstatic-final-fields/

18、字面量替換

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

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

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

翻譯修改摘錄自:

https://shipilev.net/jvm/anatomy-quarks/18-scalar-replacement/

19、鎖消除

原文標題:JVM Anatomy Quark #19: Lock Elision

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

翻譯修改摘錄自:

https://shipilev.net/jvm/anatomy-quarks/19-lock-elision/

20、FPU溢出

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

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

翻譯修改摘錄自:

https://shipilev.net/jvm/anatomy-quarks/20-fpu-spills/

21、堆內存歸還

原文標題:JVM Anatomy Quark #21: Heap Uncommit

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

翻譯修改摘錄自:

https://shipilev.net/jvm/anatomy-quarks/21-heap-uncommit/

22、安全點檢查

原文標題:JVM Anatomy Quark #22: Safepoint Polls

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

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

翻譯修改摘錄自:

https://shipilev.net/jvm/anatomy-quarks/22-safepoint-polls/

23、壓縮引用

原文標題:JVM Anatomy Quark #23: Compressed References

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

翻譯修改摘錄自:

https://shipilev.net/jvm/anatomy-quarks/23-compressed-references

24、對象對齊

原文標題:JVM Anatomy Quark #24: Object Alignment

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

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

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

翻譯修改摘錄自:

https://shipilev.net/jvm/anatomy-quarks/24-object-alignment/

黑龙江6+1开奖结果查询