重磅硬核 | 一文聊透對(duì)象在JVM中的內(nèi)存布局,以及內(nèi)存對(duì)齊和壓縮指針的原理及應(yīng)用

大家好,我是bin岩灭,又到了每周我們見(jiàn)面的時(shí)刻了,我的公眾號(hào)在1月10號(hào)那天發(fā)布了第一篇文章《從內(nèi)核角度看IO模型的演變》赂鲤,在這篇文章中我們通過(guò)圖解的方式以一個(gè)C10k的問(wèn)題為主線噪径,從內(nèi)核角度詳細(xì)闡述了5種IO模型的演變過(guò)程窘拯,以及兩種IO線程模型的介紹苦丁,最后引出了Netty的網(wǎng)絡(luò)IO線程模型葫掉。讀者朋友們后臺(tái)留言都覺(jué)得非常的硬核款违,在大家的支持下這篇文章的目前閱讀量為2038舆乔,點(diǎn)贊量為80汁雷,在看為32跳座。這對(duì)于剛剛誕生一個(gè)多月的小號(hào)來(lái)說(shuō)杯巨,是一種莫大的鼓勵(lì)。在這里bin再次感謝大家的認(rèn)可和鼓勵(lì)支持~~

今天bin將再來(lái)為大家?guī)?lái)一篇硬核的技術(shù)文章练般,本文我們將從計(jì)算機(jī)組成原理的角度詳細(xì)闡述對(duì)象在JVM內(nèi)存中是如何布局的矗漾,以及什么是內(nèi)存對(duì)齊,如果我們頭比較鐵薄料,就是不進(jìn)行內(nèi)存對(duì)齊會(huì)造成什么樣的后果敞贡,最后引出壓縮指針的原理和應(yīng)用。同時(shí)我們還介紹了在高并發(fā)場(chǎng)景下摄职,F(xiàn)alse Sharing產(chǎn)生的原因以及帶來(lái)的性能影響誊役。

相信大家看完本文后,一定會(huì)收獲很多谷市,話不多說(shuō)蛔垢,下面我們正式開(kāi)始本文的內(nèi)容~~

本文概要.png

在我們的日常工作中,有時(shí)候我們?yōu)榱朔乐咕€上應(yīng)用發(fā)生OOM迫悠,所以我們需要在開(kāi)發(fā)的過(guò)程中計(jì)算一些核心對(duì)象在內(nèi)存中的占用大小鹏漆,目的是為了更好的了解我們的應(yīng)用程序內(nèi)存占用的一個(gè)大概情況。

進(jìn)而根據(jù)我們服務(wù)器的內(nèi)存資源限制以及預(yù)估的對(duì)象創(chuàng)建數(shù)量級(jí)計(jì)算出應(yīng)用程序占用內(nèi)存的高低水位線创泄,如果內(nèi)存占用量超過(guò)高水位線艺玲,那么就有可能有發(fā)生OOM的風(fēng)險(xiǎn)。

我們可以在程序中根據(jù)估算出的高低水位線鞠抑,做一些防止OOM的處理邏輯或者發(fā)出告警饭聚。

那么核心問(wèn)題是如何計(jì)算一個(gè)Java對(duì)象在內(nèi)存中的占用大小呢?搁拙?

在為大家解答這個(gè)問(wèn)題之前秒梳,筆者先來(lái)介紹下Java對(duì)象在內(nèi)存中的布局,也就是本文的主題箕速。

1. Java對(duì)象的內(nèi)存布局

Java對(duì)象的內(nèi)存布局.png

如圖所示酪碘,Java對(duì)象在JVM中是用instanceOopDesc結(jié)構(gòu)表示而Java對(duì)象在JVM堆中的內(nèi)存布局可以分為三部分:

1.1 對(duì)象頭(Header)

每個(gè)Java對(duì)象都包含一個(gè)對(duì)象頭,對(duì)象頭中包含了兩類信息:

  • MarkWord:在JVM中用markOopDesc結(jié)構(gòu)表示用于存儲(chǔ)對(duì)象自身運(yùn)行時(shí)的數(shù)據(jù)弧满。比如:hashcode婆跑,GC分代年齡此熬,鎖狀態(tài)標(biāo)志庭呜,線程持有的鎖,偏向線程Id犀忱,偏向時(shí)間戳等募谎。在32位操作系統(tǒng)和64位操作系統(tǒng)中MarkWord分別占用4B和8B大小的內(nèi)存。

  • 類型指針:JVM中的類型指針?lè)庋b在klassOopDesc結(jié)構(gòu)中阴汇,類型指針指向了InstanceKclass對(duì)象数冬,Java類在JVM中是用InstanceKclass對(duì)象封裝的,里邊包含了Java類的元信息,比如:繼承結(jié)構(gòu)拐纱,方法铜异,靜態(tài)變量,構(gòu)造函數(shù)等秸架。

    • 在不開(kāi)啟指針壓縮的情況下(-XX:-UseCompressedOops)揍庄。在32位操作系統(tǒng)和64位操作系統(tǒng)中類型指針?lè)謩e占用4B和8B大小的內(nèi)存。
    • 在開(kāi)啟指針壓縮的情況下(-XX:+UseCompressedOops)东抹。在32位操作系統(tǒng)和64位操作系統(tǒng)中類型指針?lè)謩e占用4B和4B大小的內(nèi)存蚂子。
  • 如果Java對(duì)象是一個(gè)數(shù)組類型的話,那么在數(shù)組對(duì)象的對(duì)象頭中還會(huì)包含一個(gè)4B大小的用于記錄數(shù)組長(zhǎng)度的屬性缭黔。

由于在對(duì)象頭中用于記錄數(shù)組長(zhǎng)度大小的屬性只占4B的內(nèi)存食茎,所以Java數(shù)組可以申請(qǐng)的最大長(zhǎng)度為:2^32

1.2 實(shí)例數(shù)據(jù)(Instance Data)

Java對(duì)象在內(nèi)存中的實(shí)例數(shù)據(jù)區(qū)用來(lái)存儲(chǔ)Java類中定義的實(shí)例字段馏谨,包括所有父類中的實(shí)例字段别渔。也就是說(shuō),雖然子類無(wú)法訪問(wèn)父類的私有實(shí)例字段惧互,或者子類的實(shí)例字段隱藏了父類的同名實(shí)例字段钠糊,但是子類的實(shí)例還是會(huì)為這些父類實(shí)例字段分配內(nèi)存。

Java對(duì)象中的字段類型分為兩大類:

  • 基礎(chǔ)類型:Java類中實(shí)例字段定義的基礎(chǔ)類型在實(shí)例數(shù)據(jù)區(qū)的內(nèi)存占用如下:

    • long | double占用8個(gè)字節(jié)壹哺。
    • int | float占用4個(gè)字節(jié)抄伍。
    • short | char占用2個(gè)字節(jié)。
    • byte | boolean占用1個(gè)字節(jié)管宵。
  • 引用類型:Java類中實(shí)例字段的引用類型在實(shí)例數(shù)據(jù)區(qū)內(nèi)存占用分為兩種情況:

    • 不開(kāi)啟指針壓縮(-XX:-UseCompressedOops):在32位操作系統(tǒng)中引用類型的內(nèi)存占用為4個(gè)字節(jié)截珍。在64位操作系統(tǒng)中引用類型的內(nèi)存占用為8個(gè)字節(jié)。
    • 開(kāi)啟指針壓縮(-XX:+UseCompressedOops):在64為操作系統(tǒng)下箩朴,引用類型內(nèi)存占用則變?yōu)闉?個(gè)字節(jié)岗喉,32位操作系統(tǒng)中引用類型的內(nèi)存占用繼續(xù)為4個(gè)字節(jié)。

為什么32位操作系統(tǒng)的引用類型占4個(gè)字節(jié)炸庞,而64位操作系統(tǒng)引用類型占8字節(jié)钱床?

在Java中,引用類型所保存的是被引用對(duì)象的內(nèi)存地址埠居。在32位操作系統(tǒng)中內(nèi)存地址是由32個(gè)bit表示查牌,因此需要4個(gè)字節(jié)來(lái)記錄內(nèi)存地址,能夠記錄的虛擬地址空間是2^32大小滥壕,也就是只能夠表示4G大小的內(nèi)存纸颜。

而在64位操作系統(tǒng)中內(nèi)存地址是由64個(gè)bit表示,因此需要8個(gè)字節(jié)來(lái)記錄內(nèi)存地址绎橘,但在 64 位系統(tǒng)里只使用了低 48 位胁孙,所以它的虛擬地址空間是 2^48大小,能夠表示256T大小的內(nèi)存,其中低 128T 的空間劃分為用戶空間涮较,高 128T 劃分為內(nèi)核空間稠鼻,可以說(shuō)是非常大了。

在我們從整體上介紹完Java對(duì)象在JVM中的內(nèi)存布局之后狂票,下面我們來(lái)看下Java對(duì)象中定義的這些實(shí)例字段在實(shí)例數(shù)據(jù)區(qū)是如何排列布局的:

2. 字段重排列

其實(shí)我們?cè)诰帉慗ava源代碼文件的時(shí)候定義的那些實(shí)例字段的順序會(huì)被JVM重新分配排列枷餐,這樣做的目的其實(shí)是為了內(nèi)存對(duì)齊,那么什么是內(nèi)存對(duì)齊苫亦,為什么要進(jìn)行內(nèi)存對(duì)齊毛肋,筆者會(huì)隨著文章深入的解讀為大家逐層揭曉答案~~

本小節(jié)中,筆者先來(lái)為大家介紹一下JVM字段重排列的規(guī)則:

JVM重新分配字段的排列順序受-XX:FieldsAllocationStyle參數(shù)的影響屋剑,默認(rèn)值為1润匙,實(shí)例字段的重新分配策略遵循以下規(guī)則:

  1. 如果一個(gè)字段占用X個(gè)字節(jié),那么這個(gè)字段的偏移量OFFSET需要對(duì)齊至NX

偏移量是指字段的內(nèi)存地址與Java對(duì)象的起始內(nèi)存地址之間的差值唉匾。比如long類型的字段孕讳,它內(nèi)存占用8個(gè)字節(jié),那么它的OFFSET應(yīng)該是8的倍數(shù)8N巍膘。不足8N的需要填充字節(jié)厂财。

  1. 在開(kāi)啟了壓縮指針的64位JVM中,Java類中的第一個(gè)字段的OFFSET需要對(duì)齊至4N峡懈,在關(guān)閉壓縮指針的情況下類中第一個(gè)字段的OFFSET需要對(duì)齊至8N璃饱。

  2. JVM默認(rèn)分配字段的順序?yàn)椋簂ong / double,int / float肪康,short / char荚恶,byte / boolean,oops(Ordianry Object Point 引用類型指針)磷支,并且父類中定義的實(shí)例變量會(huì)出現(xiàn)在子類實(shí)例變量之前谒撼。當(dāng)設(shè)置JVM參數(shù)-XX +CompactFields時(shí)(默認(rèn)),占用內(nèi)存小于long / double 的字段會(huì)允許被插入到對(duì)象中第一個(gè) long / double字段之前的間隙中雾狈,以避免不必要的內(nèi)存填充廓潜。

CompactFields選項(xiàng)參數(shù)在JDK14中以被標(biāo)記為過(guò)期了,并在將來(lái)的版本中很可能被刪除善榛。詳細(xì)細(xì)節(jié)可查看issue:https://bugs.openjdk.java.net/browse/JDK-8228750

上邊的三條字段重排列規(guī)則非常非常重要辩蛋,但是讀起來(lái)比較繞腦,很抽象不容易理解锭弊,筆者把它們先列出來(lái)的目的是為了讓大家先有一個(gè)朦朦朧朧的感性認(rèn)識(shí)堪澎,下面筆者舉一個(gè)具體的例子來(lái)為大家詳細(xì)說(shuō)明下擂错,在閱讀這個(gè)例子的過(guò)程中也方便大家深刻的理解這三條重要的字段重排列規(guī)則味滞。

假設(shè)現(xiàn)在我們有這樣一個(gè)類定義


public class Parent {
    long l;
    int i;
}

public class Child extends Parent {
    long l;
    int i;
}
  • 根據(jù)上面介紹的規(guī)則3我們知道父類中的變量是出現(xiàn)在子類變量之前的,并且字段分配順序應(yīng)該是long型字段l,應(yīng)該在int型字段i之前剑鞍。

如果JVM開(kāi)啟了-XX +CompactFields時(shí)昨凡,int型字段是可以插入對(duì)象中的第一個(gè)long型字段(也就是Parent.l字段)之前的空隙中的。如果JVM設(shè)置了-XX -CompactFields則int型字段的這種插入行為是不被允許的蚁署。

  • 根據(jù)規(guī)則1我們知道long型字段在實(shí)例數(shù)據(jù)區(qū)的OFFSET需要對(duì)齊至8N便脊,而int型字段的OFFSET需要對(duì)齊至4N。

  • 根據(jù)規(guī)則2我們知道如果開(kāi)啟壓縮指針-XX:+UseCompressedOops光戈,Child對(duì)象的第一個(gè)字段的OFFSET需要對(duì)齊至4N哪痰,關(guān)閉壓縮指針時(shí)-XX:-UseCompressedOops,Child對(duì)象的第一個(gè)字段的OFFSET需要對(duì)齊至8N久妆。

由于JVM參數(shù)UseCompressedOopsCompactFields的存在晌杰,導(dǎo)致Child對(duì)象在實(shí)例數(shù)據(jù)區(qū)字段的排列順序分為四種情況,下面我們結(jié)合前邊提煉出的這三點(diǎn)規(guī)則來(lái)看下字段排列順序在這四種情況下的表現(xiàn)筷弦。

2.1 -XX:+UseCompressedOops -XX -CompactFields 開(kāi)啟壓縮指針肋演,關(guān)閉字段壓縮

image.png
  • 偏移量OFFSET = 8的位置存放的是類型指針,由于開(kāi)啟了壓縮指針?biāo)哉加?個(gè)字節(jié)烂琴。對(duì)象頭總共占用12個(gè)字節(jié):MarkWord(8字節(jié)) + 類型指針(4字節(jié))爹殊。

  • 根據(jù)規(guī)則3:父類Parent中的字段是要出現(xiàn)在子類Child的字段之前的并且long型字段在int型字段之前。

  • 根據(jù)規(guī)則2:在開(kāi)啟壓縮指針的情況下奸绷,Child對(duì)象中的第一個(gè)字段需要對(duì)齊至4N梗夸。這里Parent.l字段的OFFSET可以是12也可以是16。

  • 根據(jù)規(guī)則1:long型字段在實(shí)例數(shù)據(jù)區(qū)的OFFSET需要對(duì)齊至8N号醉,所以這里Parent.l字段的OFFSET只能是16绒瘦,因此OFFSET = 12的位置就需要被填充。Child.l字段只能在OFFSET = 32處存儲(chǔ)扣癣,不能夠使用OFFSET = 28位置惰帽,因?yàn)?8的位置不是8的倍數(shù)無(wú)法對(duì)齊8N,因此OFFSET = 28的位置被填充了4個(gè)字節(jié)父虑。

規(guī)則1也規(guī)定了int型字段的OFFSET需要對(duì)齊至4N该酗,所以Parent.i與Child.i分別存儲(chǔ)以O(shè)FFSET = 24和OFFSET = 40的位置。

因?yàn)镴VM中的內(nèi)存對(duì)齊除了存在于字段與字段之間還存在于對(duì)象與對(duì)象之間士嚎,Java對(duì)象之間的內(nèi)存地址需要對(duì)齊至8N呜魄。

所以Child對(duì)象的末尾處被填充了4個(gè)字節(jié),對(duì)象大小由開(kāi)始的44字節(jié)被填充到48字節(jié)莱衩。

2.2 -XX:+UseCompressedOops -XX +CompactFields 開(kāi)啟壓縮指針爵嗅,開(kāi)啟字段壓縮

image.png
  • 在第一種情況的分析基礎(chǔ)上,我們開(kāi)啟了-XX +CompactFields壓縮字段笨蚁,所以導(dǎo)致int型的Parent.i字段可以插入到OFFSET = 12的位置處睹晒,以避免不必要的字節(jié)填充趟庄。

  • 根據(jù)規(guī)則2:Child對(duì)象的第一個(gè)字段需要對(duì)齊至4N,這里我們看到int型的Parent.i字段是符合這個(gè)規(guī)則的伪很。

  • 根據(jù)規(guī)則1:Child對(duì)象的所有l(wèi)ong型字段都對(duì)齊至8N戚啥,所有的int型字段都對(duì)齊至4N。

最終得到Child對(duì)象大小為36字節(jié)锉试,由于Java對(duì)象與對(duì)象之間的內(nèi)存地址需要對(duì)齊至8N猫十,所以最后Child對(duì)象的末尾又被填充了4個(gè)字節(jié)最終變?yōu)?0字節(jié)。

這里我們可以看到在開(kāi)啟字段壓縮-XX +CompactFields的情況下呆盖,Child對(duì)象的大小由48字節(jié)變成了40字節(jié)拖云。

2.3 -XX:-UseCompressedOops -XX -CompactFields 關(guān)閉壓縮指針,關(guān)閉字段壓縮

image.png

首先在關(guān)閉壓縮指針-UseCompressedOops的情況下应又,對(duì)象頭中的類型指針占用字節(jié)變成了8字節(jié)江兢。導(dǎo)致對(duì)象頭的大小在這種情況下變?yōu)榱?6字節(jié)。

  • 根據(jù)規(guī)則1:long型的變量OFFSET需要對(duì)齊至8N丁频。根據(jù)規(guī)則2:在關(guān)閉壓縮指針的情況下杉允,Child對(duì)象的第一個(gè)字段Parent.l需要對(duì)齊至8N。所以這里的Parent.l字段的OFFSET = 16席里。

  • 由于long型的變量OFFSET需要對(duì)齊至8N叔磷,所以Child.l字段的OFFSET
    需要是32,因此OFFSET = 28的位置被填充了4個(gè)字節(jié)奖磁。

這樣計(jì)算出來(lái)的Child對(duì)象大小為44字節(jié)改基,但是考慮到Java對(duì)象與對(duì)象的內(nèi)存地址需要對(duì)齊至8N,于是又在對(duì)象末尾處填充了4個(gè)字節(jié)咖为,最終Child對(duì)象的內(nèi)存占用為48字節(jié)秕狰。

2.4 -XX:-UseCompressedOops -XX +CompactFields 關(guān)閉壓縮指針,開(kāi)啟字段壓縮

在第三種情況的分析基礎(chǔ)上躁染,我們來(lái)看下第四種情況的字段排列情況:

image.png

由于在關(guān)閉指針壓縮的情況下類型指針的大小變?yōu)榱?個(gè)字節(jié)鸣哀,所以導(dǎo)致Child對(duì)象中第一個(gè)字段Parent.l前邊并沒(méi)有空隙,剛好對(duì)齊8N吞彤,并不需要int型變量的插入我衬。所以即使開(kāi)啟了字段壓縮-XX +CompactFields,字段的總體排列順序還是不變的饰恕。

默認(rèn)情況下指針壓縮-XX:+UseCompressedOops以及字段壓縮-XX +CompactFields都是開(kāi)啟的

3. 對(duì)齊填充(Padding)

在前一小節(jié)關(guān)于實(shí)例數(shù)據(jù)區(qū)字段重排列的介紹中為了內(nèi)存對(duì)齊而導(dǎo)致的字節(jié)填充不僅會(huì)出現(xiàn)在字段與字段之間挠羔,還會(huì)出現(xiàn)在對(duì)象與對(duì)象之間。

前邊我們介紹了字段重排列需要遵循的三個(gè)重要規(guī)則埋嵌,其中規(guī)則1破加,規(guī)則2定義了字段與字段之間的內(nèi)存對(duì)齊規(guī)則。 規(guī)則3定義的是對(duì)象字段之間的排列規(guī)則雹嗦。

為了內(nèi)存對(duì)齊的需要范舀,對(duì)象頭與字段之間合是,以及字段與字段之間需要填充一些不必要的字節(jié)。

比如前邊提到的字段重排列的第一種情況-XX:+UseCompressedOops -XX -CompactFields尿背。

image.png

而以上提到的四種情況都會(huì)在對(duì)象實(shí)例數(shù)據(jù)區(qū)的后邊在填充4字節(jié)大小的空間端仰,原因是除了需要滿足字段與字段之間的內(nèi)存對(duì)齊之外捶惜,還需要滿足對(duì)象與對(duì)象之間的內(nèi)存對(duì)齊田藐。

Java 虛擬機(jī)堆中對(duì)象之間的內(nèi)存地址需要對(duì)齊至8N(8的倍數(shù)),如果一個(gè)對(duì)象占用內(nèi)存不到8N個(gè)字節(jié)吱七,那么就必須在對(duì)象后填充一些不必要的字節(jié)對(duì)齊至8N個(gè)字節(jié)汽久。

虛擬機(jī)中內(nèi)存對(duì)齊的選項(xiàng)為-XX:ObjectAlignmentInBytes,默認(rèn)為8踊餐。也就是說(shuō)對(duì)象與對(duì)象之間的內(nèi)存地址需要對(duì)齊至多少倍景醇,是由這個(gè)JVM參數(shù)控制的。

我們還是以上邊第一種情況為例說(shuō)明:圖中對(duì)象實(shí)際占用是44個(gè)字節(jié)吝岭,但是不是8的倍數(shù)三痰,那么就需要再填充4個(gè)字節(jié),內(nèi)存對(duì)齊至48個(gè)字節(jié)窜管。

以上這些為了內(nèi)存對(duì)齊的目的而在字段與字段之間散劫,對(duì)象與對(duì)象之間填充的不必要字節(jié),我們就稱之為對(duì)齊填充(Padding)幕帆。

4. 對(duì)齊填充的應(yīng)用

在我們知道了對(duì)齊填充的概念之后获搏,大家可能好奇了,為啥我們要進(jìn)行對(duì)齊填充失乾,是要解決什么問(wèn)題嗎常熙?

那么就讓我們帶著這個(gè)問(wèn)題,來(lái)接著聽(tīng)筆者往下聊~~

4.1 解決偽共享問(wèn)題帶來(lái)的對(duì)齊填充

除了以上介紹的兩種對(duì)齊填充的場(chǎng)景(字段與字段之間碱茁,對(duì)象與對(duì)象之間)裸卫,在JAVA中還有一種對(duì)齊填充的場(chǎng)景,那就是通過(guò)對(duì)齊填充的方式來(lái)解決False Sharing(偽共享)的問(wèn)題纽竣。

在介紹False Sharing(偽共享)之前彼城,筆者先來(lái)介紹下CPU讀取內(nèi)存中數(shù)據(jù)的方式。

4.1.1 CPU緩存

根據(jù)摩爾定律:芯片中的晶體管數(shù)量每隔18個(gè)月就會(huì)翻一番退个。導(dǎo)致CPU的性能和處理速度變得越來(lái)越快募壕,而提升CPU的運(yùn)行速度比提升內(nèi)存的運(yùn)行速度要容易和便宜的多,所以就導(dǎo)致了CPU與內(nèi)存之間的速度差距越來(lái)越大语盈。

為了彌補(bǔ)CPU與內(nèi)存之間巨大的速度差異舱馅,提高CPU的處理效率和吞吐,于是人們引入了L1,L2,L3高速緩存集成到CPU中刀荒。當(dāng)然還有L0也就是寄存器代嗤,寄存器離CPU最近棘钞,訪問(wèn)速度也最快,基本沒(méi)有時(shí)延干毅。

CPU緩存結(jié)構(gòu).png

一個(gè)CPU里面包含多個(gè)核心宜猜,我們?cè)谫?gòu)買電腦的時(shí)候經(jīng)常會(huì)看到這樣的處理器配置,比如4核8線程硝逢。意思是這個(gè)CPU包含4個(gè)物理核心8個(gè)邏輯核心姨拥。4個(gè)物理核心表示在同一時(shí)間可以允許4個(gè)線程并行執(zhí)行,8個(gè)邏輯核心表示處理器利用超線程的技術(shù)將一個(gè)物理核心模擬出了兩個(gè)邏輯核心渠鸽,一個(gè)物理核心在同一時(shí)間只會(huì)執(zhí)行一個(gè)線程叫乌,而超線程芯片可以做到線程之間快速切換,當(dāng)一個(gè)線程在訪問(wèn)內(nèi)存的空隙徽缚,超線程芯片可以馬上切換去執(zhí)行另外一個(gè)線程憨奸。因?yàn)榍袚Q速度非常快凿试,所以在效果上看到是8個(gè)線程在同時(shí)執(zhí)行排宰。

圖中的CPU核心指的是物理核心亚茬。

從圖中我們可以看到L1Cache是離CPU核心最近的高速緩存刻像,緊接著就是L2Cache,L3Cache酒贬,內(nèi)存吧恃。

離CPU核心越近的緩存訪問(wèn)速度也越快虾啦,造價(jià)也就越高,當(dāng)然容量也就越小痕寓。

其中L1Cache和L2Cache是CPU物理核心私有的(注意:這里是物理核心不是邏輯核心

而L3Cache是整個(gè)CPU所有物理核心共享的傲醉。

CPU邏輯核心共享其所屬物理核心的L1Cache和L2Cache

L1Cache

L1Cache離CPU是最近的,它的訪問(wèn)速度最快呻率,容量也最小硬毕。

從圖中我們看到L1Cache分為兩個(gè)部分,分別是:Data Cache和Instruction Cache礼仗。它們一個(gè)是存儲(chǔ)數(shù)據(jù)的吐咳,一個(gè)是存儲(chǔ)代碼指令的。

我們可以通過(guò)cd /sys/devices/system/cpu/來(lái)查看linux機(jī)器上的CPU信息元践。

image.png

/sys/devices/system/cpu/目錄里韭脊,我們可以看到CPU的核心數(shù),當(dāng)然這里指的是邏輯核心单旁。

筆者機(jī)器上的處理器并沒(méi)有使用超線程技術(shù)所以這里其實(shí)是4個(gè)物理核心沪羔。

下面我們進(jìn)入其中一顆CPU核心(cpu0)中去看下L1Cache的情況:

CPU緩存的情況在/sys/devices/system/cpu/cpu0/cache目錄下查看:

image.png

index0描述的是L1Cache中DataCache的情況:

image.png
  • level:表示該cache信息屬于哪一級(jí),1表示L1Cache象浑。
  • type:表示屬于L1Cache的DataCache蔫饰。
  • size:表示DataCache的大小為32K琅豆。
  • shared_cpu_list:之前我們提到L1Cache和L2Cache是CPU物理核所私有的,而由物理核模擬出來(lái)的邏輯核是共享L1Cache和L2Cache的篓吁,/sys/devices/system/cpu/目錄下描述的信息是邏輯核茫因。shared_cpu_list描述的正是哪些邏輯核共享這個(gè)物理核。

index1描述的是L1Cache中Instruction Cache的情況:

image.png

我們看到L1Cache中的Instruction Cache大小也是32K杖剪。

L2Cache

L2Cache的信息存儲(chǔ)在index2目錄下:

image.png

L2Cache的大小為256K冻押,比L1Cache要大些。

L3Cache

L3Cache的信息存儲(chǔ)在index3目錄下:

image.png

到這里我們可以看到L1Cache中的DataCache和InstructionCache大小一樣都是32K而L2Cache的大小為256K摘盆,L3Cache的大小為6M翼雀。

當(dāng)然這些數(shù)值在不同的CPU配置上會(huì)是不同的饱苟,但是總體上來(lái)說(shuō)L1Cache的量級(jí)是幾十KB孩擂,L2Cache的量級(jí)是幾百KB,L3Cache的量級(jí)是幾MB箱熬。

4.1.2 CPU緩存行

前邊我們介紹了CPU的高速緩存結(jié)構(gòu)类垦,引入高速緩存的目的在于消除CPU與內(nèi)存之間的速度差距,根據(jù)程序的局部性原理我們知道城须,CPU的高速緩存肯定是用來(lái)存放熱點(diǎn)數(shù)據(jù)的蚤认。

程序局部性原理表現(xiàn)為:時(shí)間局部性和空間局部性。時(shí)間局部性是指如果程序中的某條指令一旦執(zhí)行糕伐,則不久之后該指令可能再次被執(zhí)行砰琢;如果某塊數(shù)據(jù)被訪問(wèn),則不久之后該數(shù)據(jù)可能再次被訪問(wèn)良瞧∨闫空間局部性是指一旦程序訪問(wèn)了某個(gè)存儲(chǔ)單元,則不久之后褥蚯,其附近的存儲(chǔ)單元也將被訪問(wèn)挚冤。

那么在高速緩存中存取數(shù)據(jù)的基本單位又是什么呢?赞庶?

事實(shí)上熱點(diǎn)數(shù)據(jù)在CPU高速緩存中的存取并不是我們想象中的以單獨(dú)的變量或者單獨(dú)的指針為單位存取的训挡。

CPU高速緩存中存取數(shù)據(jù)的基本單位叫做緩存行cache line。緩存行存取字節(jié)的大小為2的倍數(shù)歧强,在不同的機(jī)器上澜薄,緩存行的大小范圍在32字節(jié)到128字節(jié)之間。目前所有主流的處理器中緩存行的大小均為64字節(jié)注意:這里的單位是字節(jié))摊册。

image.png

從圖中我們可以看到L1Cache,L2Cache,L3Cache中緩存行的大小都是64字節(jié)肤京。

這也就意味著每次CPU從內(nèi)存中獲取數(shù)據(jù)或者寫入數(shù)據(jù)的大小為64個(gè)字節(jié),即使你只讀一個(gè)bit丧靡,CPU也會(huì)從內(nèi)存中加載64字節(jié)數(shù)據(jù)進(jìn)來(lái)蟆沫。同樣的道理籽暇,CPU從高速緩存中同步數(shù)據(jù)到內(nèi)存也是按照64字節(jié)的單位來(lái)進(jìn)行。

比如你訪問(wèn)一個(gè)long型數(shù)組饭庞,當(dāng)CPU去加載數(shù)組中第一個(gè)元素時(shí)也會(huì)同時(shí)將后邊的7個(gè)元素一起加載進(jìn)緩存中戒悠。這樣一來(lái)就加快了遍歷數(shù)組的效率。

long類型在Java中占用8個(gè)字節(jié)舟山,一個(gè)緩存行可以存放8個(gè)long型變量绸狐。

事實(shí)上,你可以非忱鄣粒快速的遍歷在連續(xù)的內(nèi)存塊中分配的任意數(shù)據(jù)結(jié)構(gòu)寒矿,如果你的數(shù)據(jù)結(jié)構(gòu)中的項(xiàng)在內(nèi)存中不是彼此相鄰的(比如:鏈表),這樣就無(wú)法利用CPU緩存的優(yōu)勢(shì)若债。由于數(shù)據(jù)在內(nèi)存中不是連續(xù)存放的符相,所以在這些數(shù)據(jù)結(jié)構(gòu)中的每一個(gè)項(xiàng)都可能會(huì)出現(xiàn)緩存行未命中(程序局部性原理)的情況。

還記得我們?cè)?a target="_blank">《Reactor在Netty中的實(shí)現(xiàn)(創(chuàng)建篇)》中介紹Selector的創(chuàng)建時(shí)提到蠢琳,Netty利用數(shù)組實(shí)現(xiàn)的自定義SelectedSelectionKeySet類型替換掉了JDK利用HashSet類型實(shí)現(xiàn)的sun.nio.ch.SelectorImpl#selectedKeys啊终。目的就是利用CPU緩存的優(yōu)勢(shì)來(lái)提高IO活躍的SelectionKeys集合的遍歷性能

4.2 False Sharing(偽共享)

我們先來(lái)看一個(gè)這樣的例子傲须,筆者定義了一個(gè)示例類FalseSharding蓝牲,類中有兩個(gè)long型的volatile字段a,b泰讽。

public class FalseSharding {

    volatile long a;

    volatile long b;

}

字段a例衍,b之間邏輯上是獨(dú)立的,它們之間一點(diǎn)關(guān)系也沒(méi)有已卸,分別用來(lái)存儲(chǔ)不同的數(shù)據(jù)佛玄,數(shù)據(jù)之間也沒(méi)有關(guān)聯(lián)。

FalseSharding類中字段之間的內(nèi)存布局如下:

image.png

FalseSharding類中的字段a,b在內(nèi)存中是相鄰存儲(chǔ)咬最,分別占用8個(gè)字節(jié)翎嫡。

如果恰好字段a,b被CPU讀進(jìn)了同一個(gè)緩存行永乌,而此時(shí)有兩個(gè)線程惑申,線程a用來(lái)修改字段a,同時(shí)線程b用來(lái)讀取字段b翅雏。

falsesharding1.png

在這種場(chǎng)景下圈驼,會(huì)對(duì)線程b的讀取操作造成什么影響呢

我們知道聲明了volatile關(guān)鍵字的變量可以在多線程處理環(huán)境下望几,確保內(nèi)存的可見(jiàn)性绩脆。計(jì)算機(jī)硬件層會(huì)保證對(duì)被volatile關(guān)鍵字修飾的共享變量進(jìn)行寫操作后的內(nèi)存可見(jiàn)性,而這種內(nèi)存可見(jiàn)性是由Lock前綴指令以及緩存一致性協(xié)議(MESI控制協(xié)議)共同保證的。

  • Lock前綴指令可以使修改線程所在的處理器中的相應(yīng)緩存行數(shù)據(jù)被修改后立馬刷新回內(nèi)存中靴迫,并同時(shí)鎖定所有處理器核心中緩存了該修改變量的緩存行惕味,防止多個(gè)處理器核心并發(fā)修改同一緩存行。

  • 緩存一致性協(xié)議主要是用來(lái)維護(hù)多個(gè)處理器核心之間的CPU緩存一致性以及與內(nèi)存數(shù)據(jù)的一致性玉锌。每個(gè)處理器會(huì)在總線上嗅探其他處理器準(zhǔn)備寫入的內(nèi)存地址名挥,如果這個(gè)內(nèi)存地址在自己的處理器中被緩存的話,就會(huì)將自己處理器中對(duì)應(yīng)的緩存行置為無(wú)效主守,下次需要讀取的該緩存行中的數(shù)據(jù)的時(shí)候禀倔,就需要訪問(wèn)內(nèi)存獲取。

基于以上volatile關(guān)鍵字原則参淫,我們首先來(lái)看第一種影響

falsesharding2.png
  • 當(dāng)線程a在處理器core0中對(duì)字段a進(jìn)行修改時(shí)救湖,Lock前綴指令會(huì)將所有處理器中緩存了字段a的對(duì)應(yīng)緩存行進(jìn)行鎖定這樣就會(huì)導(dǎo)致線程b在處理器core1中無(wú)法讀取和修改自己緩存行的字段b涎才。

  • 處理器core0將修改后的字段a所在的緩存行刷新回內(nèi)存中鞋既。

從圖中我們可以看到此時(shí)字段a的值在處理器core0的緩存行中以及在內(nèi)存中已經(jīng)發(fā)生變化了。但是處理器core1中字段a的值還沒(méi)有變化憔维,并且core1中字段a所在的緩存行處于鎖定狀態(tài)涛救,無(wú)法讀取也無(wú)法寫入字段b畏邢。

從上述過(guò)程中我們可以看出即使字段a业扒,b之間邏輯上是獨(dú)立的,它們之間一點(diǎn)關(guān)系也沒(méi)有舒萎,但是線程a對(duì)字段a的修改程储,導(dǎo)致了線程b無(wú)法讀取字段b。

第二種影響

faslesharding3.png

當(dāng)處理器core0將字段a所在的緩存行刷新回內(nèi)存的時(shí)候臂寝,處理器core1會(huì)在總線上嗅探到字段a的內(nèi)存地址正在被其他處理器修改章鲤,所以將自己的緩存行置為失效。當(dāng)線程b在處理器core1中讀取字段b的值時(shí)咆贬,發(fā)現(xiàn)緩存行已被置為失效败徊,core1需要重新從內(nèi)存中讀取字段b的值即使字段b沒(méi)有發(fā)生任何變化。

從以上兩種影響我們看到字段a與字段b實(shí)際上并不存在共享掏缎,它們之間也沒(méi)有相互關(guān)聯(lián)關(guān)系皱蹦,理論上線程a對(duì)字段a的任何操作,都不應(yīng)該影響線程b對(duì)字段b的讀取或者寫入眷蜈。

但事實(shí)上線程a對(duì)字段a的修改導(dǎo)致了字段b在core1中的緩存行被鎖定(Lock前綴指令)沪哺,進(jìn)而使得線程b無(wú)法讀取字段b。

線程a所在處理器core0將字段a所在緩存行同步刷新回內(nèi)存后酌儒,導(dǎo)致字段b在core1中的緩存行被置為失效(緩存一致性協(xié)議)辜妓,進(jìn)而導(dǎo)致線程b需要重新回到內(nèi)存讀取字段b的值無(wú)法利用CPU緩存的優(yōu)勢(shì)。

由于字段a和字段b在同一個(gè)緩存行中,導(dǎo)致了字段a和字段b事實(shí)上的共享(原本是不應(yīng)該被共享的)籍滴。這種現(xiàn)象就叫做False Sharing(偽共享)酪夷。

在高并發(fā)的場(chǎng)景下,這種偽共享的問(wèn)題孽惰,會(huì)對(duì)程序性能造成非常大的影響捶索。

如果線程a對(duì)字段a進(jìn)行修改,與此同時(shí)線程b對(duì)字段b也進(jìn)行修改灰瞻,這種情況對(duì)性能的影響更大腥例,因?yàn)檫@會(huì)導(dǎo)致core0和core1中相應(yīng)的緩存行相互失效。

4.3 False Sharing的解決方案

既然導(dǎo)致False Sharing出現(xiàn)的原因是字段a和字段b在同一個(gè)緩存行導(dǎo)致的酝润,那么我們就要想辦法讓字段a和字段b不在一個(gè)緩存行中窟她。

那么我們?cè)趺醋霾拍軌蚴沟米侄蝍和字段b一定不會(huì)被分配到同一個(gè)緩存行中呢蹭秋?

這時(shí)候,本小節(jié)的主題字節(jié)填充就派上用場(chǎng)了~~

在Java8之前我們通常會(huì)在字段a和字段b前后分別填充7個(gè)long型變量(緩存行大小64字節(jié)),目的是讓字段a和字段b各自獨(dú)占一個(gè)緩存行避免False Sharing晤愧。

比如我們將一開(kāi)始的實(shí)例代碼修改成這個(gè)這樣子,就可以保證字段a和字段b各自獨(dú)占一個(gè)緩存行了禀挫。

public class FalseSharding {

    long p1,p2,p3,p4,p5,p6,p7;
    volatile long a;
    long p8,p9,p10,p11,p12,p13,p14;
    volatile long b;
    long p15,p16,p17,p18,p19,p20,p21;

}

修改后的對(duì)象在內(nèi)存中布局如下:

image.png

我們看到為了解決False Sharing問(wèn)題阐肤,我們將原本占用32字節(jié)的FalseSharding示例對(duì)象硬生生的填充到了200字節(jié)。這對(duì)內(nèi)存的消耗是非郴肴可觀的借跪。通常為了極致的性能,我們會(huì)在一些高并發(fā)框架或者JDK的源碼中看到False Sharing的解決場(chǎng)景酌壕。因?yàn)樵诟卟l(fā)場(chǎng)景中掏愁,任何微小的性能損失比如False Sharing,都會(huì)被無(wú)限放大卵牍。

但解決False Sharing的同時(shí)又會(huì)帶來(lái)巨大的內(nèi)存消耗果港,所以即使在高并發(fā)框架比如disrupter或者JDK中也只是針對(duì)那些在多線程場(chǎng)景下被頻繁寫入的共享變量

這里筆者想強(qiáng)調(diào)的是在我們?nèi)粘9ぷ髦?/strong>糊昙,我們不能因?yàn)樽约菏掷锬弥N子辛掠,就滿眼都是釘子,看到任何釘子都想上去錘兩下释牺。

image.png

我們要清晰的分辨出一個(gè)問(wèn)題會(huì)帶來(lái)哪些影響和損失萝衩,這些影響和損失在我們當(dāng)前業(yè)務(wù)階段是否可以接受?是否是瓶頸船侧?同時(shí)我們也要清晰的了解要解決這些問(wèn)題我們所要付出的代價(jià)欠气。一定要綜合評(píng)估,講究一個(gè)投入產(chǎn)出比镜撩。某些問(wèn)題雖然是問(wèn)題预柒,但是在某些階段和場(chǎng)景下并不需要我們投入解決队塘。而有些問(wèn)題則對(duì)于我們當(dāng)前業(yè)務(wù)發(fā)展階段是瓶頸,我們不得不去解決宜鸯。我們?cè)诩軜?gòu)設(shè)計(jì)或者程序設(shè)計(jì)中憔古,方案一定要簡(jiǎn)單合適淋袖。并預(yù)估一些提前量留有一定的演化空間鸿市。

4.3.1 @Contended注解

在Java8中引入了一個(gè)新注解@Contended,用于解決False Sharing的問(wèn)題即碗,同時(shí)這個(gè)注解也會(huì)影響到Java對(duì)象中的字段排列焰情。

在上一小節(jié)的內(nèi)容介紹中,我們通過(guò)手段填充字段的方式解決了False Sharing的問(wèn)題剥懒,但是這里也有一個(gè)問(wèn)題内舟,因?yàn)槲覀冊(cè)谑謩?dòng)填充字段的時(shí)候還需要考慮CPU緩存行的大小,因?yàn)殡m然現(xiàn)在所有主流的處理器緩存行大小均為64字節(jié)初橘,但是也還是有處理器的緩存行大小為32字節(jié)验游,有的甚至是128字節(jié)。我們需要考慮很多硬件的限制因素保檐。

Java8中通過(guò)引入@Contended注解幫我們解決了這個(gè)問(wèn)題耕蝉,我們不在需要去手動(dòng)填充字段了。下面我們就來(lái)看下@Contended注解是如何幫助我們來(lái)解決這個(gè)問(wèn)題的~~

上小節(jié)介紹的手動(dòng)填充字節(jié)是在共享變量前后填充64字節(jié)大小的空間夜只,這樣只能確保程序在緩存行大小為32字節(jié)或者64字節(jié)的CPU下獨(dú)占緩存行垒在。但是如果CPU的緩存行大小為128字節(jié),這樣依然存在False Sharing的問(wèn)題盐肃。

引入@Contended注解可以使我們忽略底層硬件設(shè)備的差異性爪膊,做到Java語(yǔ)言的初衷:平臺(tái)無(wú)關(guān)性。

@Contended注解默認(rèn)只是在JDK內(nèi)部起作用砸王,如果我們的程序代碼中需要使用到@Contended注解,那么需要開(kāi)啟JVM參數(shù)-XX:-RestrictContended才會(huì)生效峦阁。

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface Contended {
    //contention group tag
    String value() default "";
}

@Contended注解可以標(biāo)注在類上也可以標(biāo)注在類中的字段上谦铃,被@Contended標(biāo)注的對(duì)象會(huì)獨(dú)占緩存行,不會(huì)和任何變量或者對(duì)象共享緩存行榔昔。

  • @Contended標(biāo)注在類上表示該類對(duì)象中的實(shí)例數(shù)據(jù)整體需要獨(dú)占緩存行驹闰。不能與其他實(shí)例數(shù)據(jù)共享緩存行。

  • @Contended標(biāo)注在類中的字段上表示該字段需要獨(dú)占緩存行撒会。

  • 除此之外@Contended還提供了分組的概念嘹朗,注解中的value屬性表示contention group。屬于統(tǒng)一分組下的變量诵肛,它們?cè)趦?nèi)存中是連續(xù)存放的屹培,可以允許共享緩存行。不同分組之間不允許共享緩存行。

下面我們來(lái)分別看下@Contended注解在這三種使用場(chǎng)景下是怎樣影響字段之間的排列的褪秀。

@Contended標(biāo)注在類上
@Contended
public class FalseSharding {
    volatile long a;
    volatile long b;

    volatile int c;
    volatile int d;
}

當(dāng)@Contended標(biāo)注在FalseSharding示例類上時(shí)蓄诽,表示FalseSharding示例對(duì)象中的整個(gè)實(shí)例數(shù)據(jù)區(qū)需要獨(dú)占緩存行,不能與其他對(duì)象或者變量共享緩存行媒吗。

這種情況下的內(nèi)存布局:

image.png

如圖中所示仑氛,F(xiàn)alseSharding示例類被標(biāo)注了@Contended之后,JVM會(huì)在FalseSharding示例對(duì)象的實(shí)例數(shù)據(jù)區(qū)前后填充128個(gè)字節(jié)闸英,保證實(shí)例數(shù)據(jù)區(qū)內(nèi)的字段之間內(nèi)存是連續(xù)的锯岖,并且保證整個(gè)實(shí)例數(shù)據(jù)區(qū)獨(dú)占緩存行,不會(huì)與實(shí)例數(shù)據(jù)區(qū)之外的數(shù)據(jù)共享緩存行甫何。

細(xì)心的朋友可能已經(jīng)發(fā)現(xiàn)了問(wèn)題嚎莉,我們之前不是提到緩存行的大小為64字節(jié)嗎?為什么這里會(huì)填充128字節(jié)呢沛豌?

而且之前介紹的手動(dòng)填充也是填充的64字節(jié)趋箩,為什么@Contended注解會(huì)采用兩倍的緩存行大小來(lái)填充呢?

其實(shí)這里的原因有兩個(gè):

  1. 首先第一個(gè)原因加派,我們之前也已經(jīng)提到過(guò)了叫确,目前大部分主流的CPU緩存行是64字節(jié),但是也有部分CPU緩存行是32字節(jié)或者128字節(jié)芍锦,如果只填充64字節(jié)的話竹勉,在緩存行大小為32字節(jié)和64字節(jié)的CPU中是可以做到獨(dú)占緩存行從而避免FalseSharding的,但在緩存行大小為128字節(jié)的CPU中還是會(huì)出現(xiàn)FalseSharding問(wèn)題娄琉,這里Java采用了悲觀的一種做法次乓,默認(rèn)都是填充128字節(jié),雖然對(duì)于大部分情況下比較浪費(fèi)孽水,但是屏蔽了底層硬件的差異票腰。

不過(guò)@Contended注解填充字節(jié)的大小我們可以通過(guò)JVM參數(shù)
-XX:ContendedPaddingWidth指定,有效值范圍0 - 8192女气,默認(rèn)為128杏慰。

  • 第二個(gè)原因其實(shí)是最為核心的一個(gè)原因,主要是為了防止CPU Adjacent Sector Prefetch(CPU相鄰扇區(qū)預(yù)攘毒稀)特性所帶來(lái)的FalseSharding問(wèn)題缘滥。

CPU Adjacent Sector Prefetch:https://www.techarp.com/bios-guide/cpu-adjacent-sector-prefetch/

CPU Adjacent Sector Prefetch是Intel處理器特有的BIOS功能特性,默認(rèn)是enabled谒主。主要作用就是利用程序局部性原理朝扼,當(dāng)CPU從內(nèi)存中請(qǐng)求數(shù)據(jù),并讀取當(dāng)前請(qǐng)求數(shù)據(jù)所在緩存行時(shí)霎肯,會(huì)進(jìn)一步預(yù)取與當(dāng)前緩存行相鄰的下一個(gè)緩存行擎颖,這樣當(dāng)我們的程序在順序處理數(shù)據(jù)時(shí)榛斯,會(huì)提高CPU處理效率。這一點(diǎn)也體現(xiàn)了程序局部性原理中的空間局部性特征肠仪。

當(dāng)CPU Adjacent Sector Prefetch特性被disabled禁用時(shí)肖抱,CPU就只會(huì)獲取當(dāng)前請(qǐng)求數(shù)據(jù)所在的緩存行,不會(huì)預(yù)取下一個(gè)緩存行异旧。

所以在當(dāng)CPU Adjacent Sector Prefetch啟用(enabled)的時(shí)候意述,CPU其實(shí)同時(shí)處理的是兩個(gè)緩存行,在這種情況下吮蛹,就需要填充兩倍緩存行大谢绯纭(128字節(jié))來(lái)避免CPU Adjacent Sector Prefetch所帶來(lái)的的FalseSharding問(wèn)題。

@Contended標(biāo)注在字段上
public class FalseSharding {

    @Contended
    volatile long a;
    @Contended
    volatile long b;

    volatile int c;
    volatile long d;
}
image.png

這次我們將 @Contended注解標(biāo)注在了FalseSharding示例類中的字段a和字段b上潮针,這樣帶來(lái)的效果是字段a和字段b各自獨(dú)占緩存行术荤。從內(nèi)存布局上看,字段a和字段b前后分別被填充了128個(gè)字節(jié)每篷,來(lái)確保字段a和字段b不與任何數(shù)據(jù)共享緩存行瓣戚。

而沒(méi)有被@Contended注解標(biāo)注字段c和字段d則在內(nèi)存中連續(xù)存儲(chǔ),可以共享緩存行焦读。

@Contended分組
public class FalseSharding {

    @Contended("group1")
    volatile int a;
    @Contended("group1")
    volatile long b;

    @Contended("group2")
    volatile long  c;
    @Contended("group2")
    volatile long d;
}
image.png

這次我們將字段a與字段b放在同一content group下子库,字段c與字段d放在另一個(gè)content group下。

這樣處在同一分組group1下的字段a與字段b在內(nèi)存中是連續(xù)存儲(chǔ)的矗晃,可以共享緩存行仑嗅。

同理處在同一分組group2下的字段c與字段d在內(nèi)存中也是連續(xù)存儲(chǔ)的,也允許共享緩存行张症。

但是分組之間是不能共享緩存行的仓技,所以在字段分組的前后各填充128字節(jié),來(lái)保證分組之間的變量不能共享緩存行俗他。

5. 內(nèi)存對(duì)齊

通過(guò)以上內(nèi)容我們了解到Java對(duì)象中的實(shí)例數(shù)據(jù)區(qū)字段需要進(jìn)行內(nèi)存對(duì)齊而導(dǎo)致在JVM中會(huì)被重排列以及通過(guò)填充緩存行避免false sharding的目的所帶來(lái)的字節(jié)對(duì)齊填充脖捻。

我們也了解到內(nèi)存對(duì)齊不僅發(fā)生在對(duì)象與對(duì)象之間,也發(fā)生在對(duì)象中的字段之間拯辙。

那么在本小節(jié)中筆者將為大家介紹什么是內(nèi)存對(duì)齊郭变,在本節(jié)的內(nèi)容開(kāi)始之前筆者先來(lái)拋出兩個(gè)問(wèn)題:

  • 為什么要進(jìn)行內(nèi)存對(duì)齊?如果就是頭比較鐵涯保,就是不內(nèi)存對(duì)齊,會(huì)產(chǎn)生什么樣的后果周伦?

  • Java 虛擬機(jī)堆中對(duì)象的起始地址為什么需要對(duì)齊至 8的倍數(shù)夕春?為什么不對(duì)齊至4的倍數(shù)或16的倍數(shù)或32的倍數(shù)呢?

帶著這兩個(gè)問(wèn)題专挪,下面我們正式開(kāi)始本節(jié)的內(nèi)容~~~

5.1 內(nèi)存結(jié)構(gòu)

我們平時(shí)所稱的內(nèi)存也叫隨機(jī)訪問(wèn)存儲(chǔ)器(random-access memory)也叫RAM及志。而RAM分為兩類:

  • 一類是靜態(tài)RAM(SRAM)片排,這類SRAM用于前邊介紹的CPU高速緩存L1Cache,L2Cache速侈,L3Cache率寡。其特點(diǎn)是訪問(wèn)速度快,訪問(wèn)速度為1 - 30個(gè)時(shí)鐘周期倚搬,但是容量小冶共,造價(jià)高。

  • 另一類則是動(dòng)態(tài)RAM(DRAM)每界,這類DRAM用于我們常說(shuō)的主存上捅僵,其特點(diǎn)的是訪問(wèn)速度慢(相對(duì)高速緩存),訪問(wèn)速度為50 - 200個(gè)時(shí)鐘周期眨层,但是容量大庙楚,造價(jià)便宜些(相對(duì)高速緩存)。

內(nèi)存由一個(gè)一個(gè)的存儲(chǔ)器模塊(memory module)組成趴樱,它們插在主板的擴(kuò)展槽上仁讨。常見(jiàn)的存儲(chǔ)器模塊通常以64位為單位(8個(gè)字節(jié))傳輸數(shù)據(jù)到存儲(chǔ)控制器上或者從存儲(chǔ)控制器傳出數(shù)據(jù)。

image.png

如圖所示內(nèi)存條上黑色的元器件就是存儲(chǔ)器模塊(memory module)法挨。多個(gè)存儲(chǔ)器模塊連接到存儲(chǔ)控制器上焕毫,就聚合成了主存。

內(nèi)存結(jié)構(gòu).png

而前邊介紹到的DRAM芯片就包裝在存儲(chǔ)器模塊中航揉,每個(gè)存儲(chǔ)器模塊中包含8個(gè)DRAM芯片塞祈,依次編號(hào)為0 - 7

存儲(chǔ)器模塊.png

而每一個(gè)DRAM芯片的存儲(chǔ)結(jié)構(gòu)是一個(gè)二維矩陣帅涂,二維矩陣中存儲(chǔ)的元素我們稱為超單元(supercell)议薪,每個(gè)supercell大小為一個(gè)字節(jié)(8 bit)。每個(gè)supercell都由一個(gè)坐標(biāo)地址(i媳友,j)斯议。

i表示二維矩陣中的行地址,在計(jì)算機(jī)中行地址稱為RAS(row access strobe醇锚,行訪問(wèn)選通脈沖)哼御。
j表示二維矩陣中的列地址,在計(jì)算機(jī)中列地址稱為CAS(column access strobe,列訪問(wèn)選通脈沖)焊唬。

下圖中的supercell的RAS = 2恋昼,CAS = 2。

DRAM結(jié)構(gòu).png

DRAM芯片中的信息通過(guò)引腳流入流出DRAM芯片赶促。每個(gè)引腳攜帶1 bit的信號(hào)液肌。

圖中DRAM芯片包含了兩個(gè)地址引腳(addr),因?yàn)槲覀円ㄟ^(guò)RAS鸥滨,CAS來(lái)定位要獲取的supercell嗦哆。還有8個(gè)數(shù)據(jù)引腳(data),因?yàn)镈RAM芯片的IO單位為一個(gè)字節(jié)(8 bit),所以需要8個(gè)data引腳從DRAM芯片傳入傳出數(shù)據(jù)谤祖。

注意這里只是為了解釋地址引腳和數(shù)據(jù)引腳的概念,實(shí)際硬件中的引腳數(shù)量是不一定的老速。

5.2 DRAM芯片的訪問(wèn)

我們現(xiàn)在就以讀取上圖中坐標(biāo)地址為(2粥喜,2)的supercell為例,來(lái)說(shuō)明訪問(wèn)DRAM芯片的過(guò)程橘券。

DRAM芯片訪問(wèn).png
  1. 首先存儲(chǔ)控制器將行地址RAS = 2通過(guò)地址引腳發(fā)送給DRAM芯片额湘。

  2. DRAM芯片根據(jù)RAS = 2將二維矩陣中的第二行的全部?jī)?nèi)容拷貝到內(nèi)部行緩沖區(qū)中。

  3. 接下來(lái)存儲(chǔ)控制器會(huì)通過(guò)地址引腳發(fā)送CAS = 2到DRAM芯片中约郁。

  4. DRAM芯片從內(nèi)部行緩沖區(qū)中根據(jù)CAS = 2拷貝出第二列的supercell并通過(guò)數(shù)據(jù)引腳發(fā)送給存儲(chǔ)控制器缩挑。

DRAM芯片的IO單位為一個(gè)supercell,也就是一個(gè)字節(jié)(8 bit)鬓梅。

5.3 CPU如何讀寫主存

前邊我們介紹了內(nèi)存的物理結(jié)構(gòu)供置,以及如何訪問(wèn)內(nèi)存中的DRAM芯片獲取supercell中存儲(chǔ)的數(shù)據(jù)(一個(gè)字節(jié))。

本小節(jié)我們來(lái)介紹下CPU是如何訪問(wèn)內(nèi)存的绽快。

CPU與內(nèi)存之間的總線結(jié)構(gòu).png

其中關(guān)于CPU芯片的內(nèi)部結(jié)構(gòu)我們?cè)诮榻Bfalse sharding的時(shí)候已經(jīng)詳細(xì)的介紹過(guò)了芥丧,這里我們主要聚焦在CPU與內(nèi)存之間的總線架構(gòu)上。

5.3.1 總線結(jié)構(gòu)

CPU與內(nèi)存之間的數(shù)據(jù)交互是通過(guò)總線(bus)完成的坊罢,而數(shù)據(jù)在總線上的傳送是通過(guò)一系列的步驟完成的续担,這些步驟稱為總線事務(wù)(bus transaction)。

其中數(shù)據(jù)從內(nèi)存?zhèn)魉偷紺PU稱之為讀事務(wù)(read transaction)活孩,數(shù)據(jù)從CPU傳送到內(nèi)存稱之為寫事務(wù)(write transaction)物遇。

總線上傳輸?shù)男盘?hào)包括:地址信號(hào),數(shù)據(jù)信號(hào)憾儒,控制信號(hào)询兴。其中控制總線上傳輸?shù)目刂菩盘?hào)可以同步事務(wù),并能夠標(biāo)識(shí)出當(dāng)前正在被執(zhí)行的事務(wù)信息:

  • 當(dāng)前這個(gè)事務(wù)是到內(nèi)存的起趾?還是到磁盤的诗舰?或者是到其他IO設(shè)備的?
  • 這個(gè)事務(wù)是讀還是寫训裆?
  • 總線上傳輸?shù)牡刂沸盘?hào)(內(nèi)存地址)眶根,還是數(shù)據(jù)信號(hào)(數(shù)據(jù))?边琉。

還記得我們前邊講到的MESI緩存一致性協(xié)議嗎属百?當(dāng)core0修改字段a的值時(shí),其他CPU核心會(huì)在總線上嗅探字段a的內(nèi)存地址变姨,如果嗅探到總線上出現(xiàn)字段a的內(nèi)存地址诸老,說(shuō)明有人在修改字段a,這樣其他CPU核心就會(huì)失效自己緩存字段a所在的cache line钳恕。

如上圖所示别伏,其中系統(tǒng)總線是連接CPU與IO bridge的,存儲(chǔ)總線是來(lái)連接IO bridge和主存的忧额。

IO bridge負(fù)責(zé)將系統(tǒng)總線上的電子信號(hào)轉(zhuǎn)換成存儲(chǔ)總線上的電子信號(hào)厘肮。IO bridge也會(huì)將系統(tǒng)總線和存儲(chǔ)總線連接到IO總線(磁盤等IO設(shè)備)上。這里我們看到IO bridge其實(shí)起的作用就是轉(zhuǎn)換不同總線上的電子信號(hào)睦番。

5.3.2 CPU從內(nèi)存讀取數(shù)據(jù)過(guò)程

假設(shè)CPU現(xiàn)在要將內(nèi)存地址為A的內(nèi)容加載到寄存器中進(jìn)行運(yùn)算类茂。

CPU讀取內(nèi)存.png

首先CPU芯片中的總線接口會(huì)在總線上發(fā)起讀事務(wù)(read transaction)。 該讀事務(wù)分為以下步驟進(jìn)行:

  1. CPU將內(nèi)存地址A放到系統(tǒng)總線上托嚣。隨后IO bridge將信號(hào)傳遞到存儲(chǔ)總線上巩检。

  2. 主存感受到存儲(chǔ)總線上的地址信號(hào)并通過(guò)存儲(chǔ)控制器將存儲(chǔ)總線上的內(nèi)存地址A讀取出來(lái)。

  3. 存儲(chǔ)控制器通過(guò)內(nèi)存地址A定位到具體的存儲(chǔ)器模塊示启,從DRAM芯片中取出內(nèi)存地址A對(duì)應(yīng)的數(shù)據(jù)X兢哭。

  4. 存儲(chǔ)控制器將讀取到的數(shù)據(jù)X放到存儲(chǔ)總線上,隨后IO bridge將存儲(chǔ)總線上的數(shù)據(jù)信號(hào)轉(zhuǎn)換為系統(tǒng)總線上的數(shù)據(jù)信號(hào)夫嗓,然后繼續(xù)沿著系統(tǒng)總線傳遞迟螺。

  5. CPU芯片感受到系統(tǒng)總線上的數(shù)據(jù)信號(hào),將數(shù)據(jù)從系統(tǒng)總線上讀取出來(lái)并拷貝到寄存器中舍咖。

以上就是CPU讀取內(nèi)存數(shù)據(jù)到寄存器中的完整過(guò)程矩父。

但是其中還涉及到一個(gè)重要的過(guò)程,這里我們還是需要攤開(kāi)來(lái)介紹一下排霉,那就是存儲(chǔ)控制器如何通過(guò)內(nèi)存地址A從主存中讀取出對(duì)應(yīng)的數(shù)據(jù)X的窍株?

接下來(lái)我們結(jié)合前邊介紹的內(nèi)存結(jié)構(gòu)以及從DRAM芯片讀取數(shù)據(jù)的過(guò)程,來(lái)總體介紹下如何從主存中讀取數(shù)據(jù)攻柠。

5.3.3 如何根據(jù)內(nèi)存地址從主存中讀取數(shù)據(jù)

前邊介紹到球订,當(dāng)主存中的存儲(chǔ)控制器感受到了存儲(chǔ)總線上的地址信號(hào)時(shí),會(huì)將內(nèi)存地址從存儲(chǔ)總線上讀取出來(lái)辙诞。

隨后會(huì)通過(guò)內(nèi)存地址定位到具體的存儲(chǔ)器模塊辙售。還記得內(nèi)存結(jié)構(gòu)中的存儲(chǔ)器模塊嗎?飞涂?

內(nèi)存結(jié)構(gòu).png

而每個(gè)存儲(chǔ)器模塊中包含了8個(gè)DRAM芯片旦部,編號(hào)從0 - 7

存儲(chǔ)器模塊.png

存儲(chǔ)控制器會(huì)將內(nèi)存地址轉(zhuǎn)換為DRAM芯片中supercell在二維矩陣中的坐標(biāo)地址(RAS较店,CAS)士八。并將這個(gè)坐標(biāo)地址發(fā)送給對(duì)應(yīng)的存儲(chǔ)器模塊。隨后存儲(chǔ)器模塊會(huì)將RASCAS廣播到存儲(chǔ)器模塊中的所有DRAM芯片梁呈。依次通過(guò)(RAS婚度,CAS)從DRAM0到DRAM7讀取到相應(yīng)的supercell。

DRAM芯片訪問(wèn).png

我們知道一個(gè)supercell存儲(chǔ)了8 bit數(shù)據(jù)官卡,這里我們從DRAM0到DRAM7
依次讀取到了8個(gè)supercell也就是8個(gè)字節(jié)蝗茁,然后將這8個(gè)字節(jié)返回給存儲(chǔ)控制器醋虏,由存儲(chǔ)控制器將數(shù)據(jù)放到存儲(chǔ)總線上。

CPU總是以word size為單位從內(nèi)存中讀取數(shù)據(jù)哮翘,在64位處理器中的word size為8個(gè)字節(jié)颈嚼。64位的內(nèi)存也只能每次吞吐8個(gè)字節(jié)。

CPU每次會(huì)向內(nèi)存讀寫一個(gè)cache line大小的數(shù)據(jù)(64個(gè)字節(jié))饭寺,但是內(nèi)存一次只能吞吐8個(gè)字節(jié)阻课。

所以在內(nèi)存地址對(duì)應(yīng)的存儲(chǔ)器模塊中,DRAM0芯片存儲(chǔ)第一個(gè)低位字節(jié)(supercell)艰匙,DRAM1芯片存儲(chǔ)第二個(gè)字節(jié)限煞,......依次類推DRAM7芯片存儲(chǔ)最后一個(gè)高位字節(jié)。

內(nèi)存一次讀取和寫入的單位是8個(gè)字節(jié)员凝。而且在程序員眼里連續(xù)的內(nèi)存地址實(shí)際上在物理上是不連續(xù)的署驻。因?yàn)檫@連續(xù)的8個(gè)字節(jié)其實(shí)是存儲(chǔ)于不同的DRAM芯片上的。每個(gè)DRAM芯片存儲(chǔ)一個(gè)字節(jié)(supercell)绊序。

讀取存儲(chǔ)器模塊數(shù)據(jù).png

5.3.4 CPU向內(nèi)存寫入數(shù)據(jù)過(guò)程

我們現(xiàn)在假設(shè)CPU要將寄存器中的數(shù)據(jù)X寫到內(nèi)存地址A中硕舆。同樣的道理,CPU芯片中的總線接口會(huì)向總線發(fā)起寫事務(wù)(write transaction)骤公。寫事務(wù)步驟如下:

  1. CPU將要寫入的內(nèi)存地址A放入系統(tǒng)總線上抚官。

  2. 通過(guò)IO bridge的信號(hào)轉(zhuǎn)換,將內(nèi)存地址A傳遞到存儲(chǔ)總線上阶捆。

  3. 存儲(chǔ)控制器感受到存儲(chǔ)總線上的地址信號(hào)凌节,將內(nèi)存地址A從存儲(chǔ)總線上讀取出來(lái),并等待數(shù)據(jù)的到達(dá)洒试。

  4. CPU將寄存器中的數(shù)據(jù)拷貝到系統(tǒng)總線上倍奢,通過(guò)IO bridge的信號(hào)轉(zhuǎn)換,將數(shù)據(jù)傳遞到存儲(chǔ)總線上垒棋。

  5. 存儲(chǔ)控制器感受到存儲(chǔ)總線上的數(shù)據(jù)信號(hào)卒煞,將數(shù)據(jù)從存儲(chǔ)總線上讀取出來(lái)。

  6. 存儲(chǔ)控制器通過(guò)內(nèi)存地址A定位到具體的存儲(chǔ)器模塊叼架,最后將數(shù)據(jù)寫入存儲(chǔ)器模塊中的8個(gè)DRAM芯片中畔裕。

6. 為什么要內(nèi)存對(duì)齊

我們?cè)诹私饬藘?nèi)存結(jié)構(gòu)以及CPU讀寫內(nèi)存的過(guò)程之后,現(xiàn)在我們回過(guò)頭來(lái)討論下本小節(jié)開(kāi)頭的問(wèn)題:為什么要內(nèi)存對(duì)齊乖订?

下面筆者從三個(gè)方面來(lái)介紹下要進(jìn)行內(nèi)存對(duì)齊的原因:

速度

CPU讀取數(shù)據(jù)的單位是根據(jù)word size來(lái)的扮饶,在64位處理器中word size = 8字節(jié),所以CPU向內(nèi)存讀寫數(shù)據(jù)的單位為8字節(jié)乍构。

在64位內(nèi)存中甜无,內(nèi)存IO單位為8個(gè)字節(jié),我們前邊也提到內(nèi)存結(jié)構(gòu)中的存儲(chǔ)器模塊通常以64位為單位(8個(gè)字節(jié))傳輸數(shù)據(jù)到存儲(chǔ)控制器上或者從存儲(chǔ)控制器傳出數(shù)據(jù)。因?yàn)槊看蝺?nèi)存IO讀取數(shù)據(jù)都是從數(shù)據(jù)所在具體的存儲(chǔ)器模塊中包含的這8個(gè)DRAM芯片中以相同的(RAM,CAS)依次讀取一個(gè)字節(jié)岂丘,然后在存儲(chǔ)控制器中聚合成8個(gè)字節(jié)返回給CPU陵究。

讀取存儲(chǔ)器模塊數(shù)據(jù).png

由于存儲(chǔ)器模塊中這種由8個(gè)DRAM芯片組成的物理存儲(chǔ)結(jié)構(gòu)的限制,內(nèi)存讀取數(shù)據(jù)只能是按照地址順序8個(gè)字節(jié)的依次讀取----8個(gè)字節(jié)8個(gè)字節(jié)地來(lái)讀取數(shù)據(jù)元潘。

內(nèi)存IO單位.png
  • 假設(shè)我們現(xiàn)在讀取0x0000 - 0x0007這段連續(xù)內(nèi)存地址上的8個(gè)字節(jié)畔乙。由于內(nèi)存讀取是按照8個(gè)字節(jié)為單位依次順序讀取的,而我們要讀取的這段內(nèi)存地址的起始地址是0(8的倍數(shù))翩概,所以0x0000 - 0x0007中每個(gè)地址的坐標(biāo)都是相同的(RAS,CAS)。所以他可以在8個(gè)DRAM芯片中通過(guò)相同的(RAS,CAS)一次性讀取出來(lái)返咱。

  • 如果我們現(xiàn)在讀取0x0008 - 0x0015這段連續(xù)內(nèi)存上的8個(gè)字節(jié)也是一樣的钥庇,因?yàn)閮?nèi)存段起始地址為8(8的倍數(shù)),所以這段內(nèi)存上的每個(gè)內(nèi)存地址在DREAM芯片中的坐標(biāo)地址(RAS,CAS)也是相同的咖摹,我們也可以一次性讀取出來(lái)评姨。

注意:0x0000 - 0x0007內(nèi)存段中的坐標(biāo)地址(RAS,CAS)與0x0008 - 0x0015內(nèi)存段中的坐標(biāo)地址(RAS,CAS)是不相同的。

  • 但如果我們現(xiàn)在讀取0x0007 - 0x0014這段連續(xù)內(nèi)存上的8個(gè)字節(jié)情況就不一樣了萤晴,由于起始地址0x0007在DRAM芯片中的(RAS,CAS)與后邊地址0x0008 - 0x0014的(RAS,CAS)不相同吐句,所以CPU只能先從0x0000 - 0x0007讀取8個(gè)字節(jié)出來(lái)先放入結(jié)果寄存器中并左移7個(gè)字節(jié)(目的是只獲取0x0007),然后CPU在從0x0008 - 0x0015讀取8個(gè)字節(jié)出來(lái)放入臨時(shí)寄存器中并右移1個(gè)字節(jié)(目的是獲取0x0008 - 0x0014)最后與結(jié)果寄存器或運(yùn)算店读。最終得到0x0007 - 0x0014地址段上的8個(gè)字節(jié)嗦枢。

從以上分析過(guò)程來(lái)看,當(dāng)CPU訪問(wèn)內(nèi)存對(duì)齊的地址時(shí)屯断,比如0x00000x0008這兩個(gè)起始地址都是對(duì)齊至8的倍數(shù)文虏。CPU可以通過(guò)一次read transaction讀取出來(lái)。

但是當(dāng)CPU訪問(wèn)內(nèi)存沒(méi)有對(duì)齊的地址時(shí)殖演,比如0x0007這個(gè)起始地址就沒(méi)有對(duì)齊至8的倍數(shù)氧秘。CPU就需要兩次read transaction才能將數(shù)據(jù)讀取出來(lái)。

還記得筆者在小節(jié)開(kāi)頭提出的問(wèn)題嗎 趴久?
"Java 虛擬機(jī)堆中對(duì)象的起始地址為什么需要對(duì)齊至 8的倍數(shù)丸相?為什么不對(duì)齊至4的倍數(shù)或16的倍數(shù)或32的倍數(shù)呢?"
現(xiàn)在你能回答了嗎彼棍?亭饵??

原子性

CPU可以原子地操作一個(gè)對(duì)齊的word size memory萨惑。64位處理器中word size = 8字節(jié)劈猿。

盡量分配在一個(gè)緩存行中

前邊在介紹false sharding的時(shí)候我們提到目前主流處理器中的cache line大小為64字節(jié),堆中對(duì)象的起始地址通過(guò)內(nèi)存對(duì)齊至8的倍數(shù)坎吻,可以讓對(duì)象盡可能的分配到一個(gè)緩存行中缆蝉。一個(gè)內(nèi)存起始地址未對(duì)齊的對(duì)象可能會(huì)跨緩存行存儲(chǔ),這樣會(huì)導(dǎo)致CPU的執(zhí)行效率慢2倍

其中對(duì)象中字段內(nèi)存對(duì)齊的其中一個(gè)重要原因也是讓字段只出現(xiàn)在同一 CPU 的緩存行中刊头。如果字段不是對(duì)齊的黍瞧,那么就有可能出現(xiàn)跨緩存行的字段。也就是說(shuō)原杂,該字段的讀取可能需要替換兩個(gè)緩存行印颤,而該字段的存儲(chǔ)也會(huì)同時(shí)污染兩個(gè)緩存行。這兩種情況對(duì)程序的執(zhí)行效率而言都是不利的穿肄。

另外在《2. 字段重排列》這一小節(jié)介紹的三種字段對(duì)齊規(guī)則年局,是保證在字段內(nèi)存對(duì)齊的基礎(chǔ)上使得實(shí)例數(shù)據(jù)區(qū)占用內(nèi)存盡可能的小

7. 壓縮指針

在介紹完關(guān)于內(nèi)存對(duì)齊的相關(guān)內(nèi)容之后咸产,我們來(lái)介紹下前邊經(jīng)常提到的壓縮指針矢否。可以通過(guò)JVM參數(shù)XX:+UseCompressedOops開(kāi)啟脑溢,當(dāng)然默認(rèn)是開(kāi)啟的僵朗。

在本小節(jié)內(nèi)容開(kāi)啟之前,我們先來(lái)討論一個(gè)問(wèn)題屑彻,那就是為什么要使用壓縮指針验庙??

假設(shè)我們現(xiàn)在正在準(zhǔn)備將32位系統(tǒng)切換到64位系統(tǒng)社牲,起初我們可能會(huì)期望系統(tǒng)性能會(huì)立馬得到提升粪薛,但現(xiàn)實(shí)情況可能并不是這樣的。

在JVM中導(dǎo)致性能下降的最主要原因就是64位系統(tǒng)中的對(duì)象引用膳沽。在前邊我們也提到過(guò)汗菜,64位系統(tǒng)中對(duì)象的引用以及類型指針占用64 bit也就是8個(gè)字節(jié)。

這就導(dǎo)致了在64位系統(tǒng)中的對(duì)象引用占用的內(nèi)存空間是32位系統(tǒng)中的兩倍大小挑社,因此間接的導(dǎo)致了在64位系統(tǒng)中更多的內(nèi)存消耗以及更頻繁的GC發(fā)生陨界,GC占用的CPU時(shí)間越多,那么我們的應(yīng)用程序占用CPU的時(shí)間就越少痛阻。

另外一個(gè)就是對(duì)象的引用變大了菌瘪,那么CPU可緩存的對(duì)象相對(duì)就少了,增加了對(duì)內(nèi)存的訪問(wèn)阱当。綜合以上幾點(diǎn)從而導(dǎo)致了系統(tǒng)性能的下降俏扩。

從另一方面來(lái)說(shuō),在64位系統(tǒng)中內(nèi)存的尋址空間為2^48 = 256T弊添,在現(xiàn)實(shí)情況中我們真的需要這么大的尋址空間嗎录淡??好像也沒(méi)必要吧~~

于是我們就有了新的想法:那么我們是否應(yīng)該切換回32位系統(tǒng)呢油坝?

如果我們切換回32位系統(tǒng)嫉戚,我們?cè)趺唇鉀Q在32位系統(tǒng)中擁有超過(guò)4G的內(nèi)存尋址空間呢刨裆?因?yàn)楝F(xiàn)在4G的內(nèi)存大小對(duì)于現(xiàn)在的應(yīng)用來(lái)說(shuō)明顯是不夠的。

我想以上的這些問(wèn)題彬檀,也是當(dāng)初JVM的開(kāi)發(fā)者需要面對(duì)和解決的帆啃,當(dāng)然他們也交出了非常完美的答卷,那就是使用壓縮指針可以在64位系統(tǒng)中利用32位的對(duì)象引用獲得超過(guò)4G的內(nèi)存尋址空間窍帝。

7.1 壓縮指針是如何做到的呢努潘?

還記得之前我們?cè)诮榻B對(duì)齊填充和內(nèi)存對(duì)齊小節(jié)中提到的,在Java虛擬機(jī)堆中對(duì)象的起始地址必須對(duì)齊至8的倍數(shù)嗎坤学?

由于堆中對(duì)象的起始地址均是對(duì)齊至8的倍數(shù)疯坤,所以對(duì)象引用在開(kāi)啟壓縮指針情況下的32位二進(jìn)制的后三位始終是0(因?yàn)樗鼈兪冀K可以被8整除)。

既然JVM已經(jīng)知道了這些對(duì)象的內(nèi)存地址后三位始終是0拥峦,那么這些無(wú)意義的0就沒(méi)必要在堆中繼續(xù)存儲(chǔ)贴膘。相反,我們可以利用存儲(chǔ)0的這3位bit存儲(chǔ)一些有意義的信息略号,這樣我們就多出3位bit的尋址空間。

這樣在存儲(chǔ)的時(shí)候洋闽,JVM還是按照32位來(lái)存儲(chǔ)玄柠,只不過(guò)后三位原本用來(lái)存儲(chǔ)0的bit現(xiàn)在被我們用來(lái)存放有意義的地址空間信息。

當(dāng)尋址的時(shí)候诫舅,JVM將這32位的對(duì)象引用左移3位(后三位補(bǔ)0)羽利。這就導(dǎo)致了在開(kāi)啟壓縮指針的情況下,我們?cè)?2位的內(nèi)存尋址空間一下變成了35位刊懈≌饣。可尋址的內(nèi)存空間變?yōu)?^32 * 2^3 = 32G。

壓縮指針.png

這樣一來(lái)虚汛,JVM雖然額外的執(zhí)行了一些位運(yùn)算但是極大的提高了尋址空間匾浪,并且將對(duì)象引用占用內(nèi)存大小降低了一半,節(jié)省了大量空間卷哩。況且這些位運(yùn)算對(duì)于CPU來(lái)說(shuō)是非常容易且輕量的操作

通過(guò)壓縮指針的原理我挖掘到了內(nèi)存對(duì)齊的另一個(gè)重要原因就是通過(guò)內(nèi)存對(duì)齊至8的倍數(shù)蛋辈,我們可以在64位系統(tǒng)中使用壓縮指針通過(guò)32位的對(duì)象引用將尋址空間提升至32G.

從Java7開(kāi)始,當(dāng)maximum heap size小于32G的時(shí)候将谊,壓縮指針是默認(rèn)開(kāi)啟的冷溶。但是當(dāng)maximum heap size大于32G的時(shí)候,壓縮指針就會(huì)關(guān)閉尊浓。

那么我們?nèi)绾卧趬嚎s指針開(kāi)啟的情況下進(jìn)一步擴(kuò)大尋址空間呢逞频??栋齿?

7.2 如何進(jìn)一步擴(kuò)大尋址空間

前邊提到我們?cè)贘ava虛擬機(jī)堆中對(duì)象起始地址均需要對(duì)其至8的倍數(shù)苗胀,不過(guò)這個(gè)數(shù)值我們可以通過(guò)JVM參數(shù)-XX:ObjectAlignmentInBytes來(lái)改變(默認(rèn)值為8)襟诸。當(dāng)然這個(gè)數(shù)值的必須是2的次冪,數(shù)值范圍需要在8 - 256之間柒巫。

正是因?yàn)閷?duì)象地址對(duì)齊至8的倍數(shù)励堡,才會(huì)多出3位bit讓我們存儲(chǔ)額外的地址信息,進(jìn)而將4G的尋址空間提升至32G堡掏。

同樣的道理应结,如果我們將ObjectAlignmentInBytes的數(shù)值設(shè)置為16呢?

對(duì)象地址均對(duì)齊至16的倍數(shù)泉唁,那么就會(huì)多出4位bit讓我們存儲(chǔ)額外的地址信息鹅龄。尋址空間變?yōu)?^32 * 2^4 = 64G。

通過(guò)以上規(guī)律亭畜,我們就能知道扮休,在64位系統(tǒng)中開(kāi)啟壓縮指針的情況,尋址范圍的計(jì)算公式:4G * ObjectAlignmentInBytes = 尋址范圍拴鸵。

但是筆者并不建議大家貿(mào)然這樣做玷坠,因?yàn)樵龃罅?code>ObjectAlignmentInBytes雖然能擴(kuò)大尋址范圍,但是這同時(shí)也可能增加了對(duì)象之間的字節(jié)填充劲藐,導(dǎo)致壓縮指針沒(méi)有達(dá)到原本節(jié)省空間的效果八堡。

8. 數(shù)組對(duì)象的內(nèi)存布局

前邊大量的篇幅我們都是在討論Java普通對(duì)象在內(nèi)存中的布局情況,最后這一小節(jié)我們?cè)賮?lái)說(shuō)下Java中的數(shù)組對(duì)象在內(nèi)存中是如何布局的聘芜。

8.1 基本類型數(shù)組的內(nèi)存布局

基本類型數(shù)組內(nèi)存布局.png

上圖表示的是基本類型數(shù)組在內(nèi)存中的布局兄渺,基本類型數(shù)組在JVM中用typeArrayOop結(jié)構(gòu)體表示,基本類型數(shù)組類型元信息用TypeArrayKlass結(jié)構(gòu)體表示汰现。

數(shù)組的內(nèi)存布局大體上和普通對(duì)象的內(nèi)存布局差不多挂谍,唯一不同的是在數(shù)組類型對(duì)象頭中多出了4個(gè)字節(jié)用來(lái)表示數(shù)組長(zhǎng)度的部分。

我們還是分別以開(kāi)啟指針壓縮和關(guān)閉指針壓縮兩種情況瞎饲,通過(guò)下面的例子來(lái)進(jìn)行說(shuō)明:

long[] longArrayLayout = new long[1];

開(kāi)啟指針壓縮 -XX:+UseCompressedOops

image.png

我們看到紅框部分即為數(shù)組類型對(duì)象頭中多出來(lái)一個(gè)4字節(jié)大小用來(lái)表示數(shù)組長(zhǎng)度的部分口叙。

因?yàn)槲覀兪纠械膌ong型數(shù)組只有一個(gè)元素,所以實(shí)例數(shù)據(jù)區(qū)的大小只有8字節(jié)企软。如果我們示例中的long型數(shù)組變?yōu)閮蓚€(gè)元素庐扫,那么實(shí)例數(shù)據(jù)區(qū)的大小就會(huì)變?yōu)?6字節(jié),以此類推................仗哨。

關(guān)閉指針壓縮 -XX:-UseCompressedOops

image.png

當(dāng)關(guān)閉了指針壓縮時(shí)形庭,對(duì)象頭中的MarkWord還是占用8個(gè)字節(jié),但是類型指針從4個(gè)字節(jié)變?yōu)榱?個(gè)字節(jié)厌漂。數(shù)組長(zhǎng)度屬性還是不變保持4個(gè)字節(jié)萨醒。

這里我們發(fā)現(xiàn)是實(shí)例數(shù)據(jù)區(qū)與對(duì)象頭之間發(fā)生了對(duì)齊填充。大家還記得這是為什么嗎苇倡?富纸?

我們前邊在字段重排列小節(jié)介紹了三種字段排列規(guī)則在這里繼續(xù)適用:

  • 規(guī)則1:如果一個(gè)字段占用X個(gè)字節(jié)囤踩,那么這個(gè)字段的偏移量OFFSET需要對(duì)齊至NX

  • 規(guī)則2:在開(kāi)啟了壓縮指針的64位JVM中晓褪,Java類中的第一個(gè)字段的OFFSET需要對(duì)齊至4N堵漱,在關(guān)閉壓縮指針的情況下類中第一個(gè)字段的OFFSET需要對(duì)齊至8N

這里基本數(shù)組類型的實(shí)例數(shù)據(jù)區(qū)中是long型涣仿,在關(guān)閉指針壓縮的情況下勤庐,根據(jù)規(guī)則1和規(guī)則2需要對(duì)齊至8的倍數(shù),所以要在其與對(duì)象頭之間填充4個(gè)字節(jié)好港,達(dá)到內(nèi)存對(duì)齊的目的愉镰,起始地址變?yōu)?code>24。

8.2 引用類型數(shù)組的內(nèi)存布局

引用類型數(shù)組的內(nèi)存布局.png

上圖表示的是引用類型數(shù)組在內(nèi)存中的布局钧汹,引用類型數(shù)組在JVM中用objArrayOop結(jié)構(gòu)體表示丈探,基本類型數(shù)組類型元信息用ObjArrayKlass結(jié)構(gòu)體表示。

同樣在引用類型數(shù)組的對(duì)象頭中也會(huì)有一個(gè)4字節(jié)大小用來(lái)表示數(shù)組長(zhǎng)度的部分拔莱。

我們還是分別以開(kāi)啟指針壓縮和關(guān)閉指針壓縮兩種情況碗降,通過(guò)下面的例子來(lái)進(jìn)行說(shuō)明:

public class ReferenceArrayLayout {
    char a;
    int b;
    short c;
}

ReferenceArrayLayout[] referenceArrayLayout = new ReferenceArrayLayout[1];

開(kāi)啟指針壓縮 -XX:+UseCompressedOops

image.png

引用數(shù)組類型內(nèi)存布局與基礎(chǔ)數(shù)組類型內(nèi)存布局最大的不同在于它們的實(shí)例數(shù)據(jù)區(qū)。由于開(kāi)啟了壓縮指針塘秦,所以對(duì)象引用占用內(nèi)存大小為4個(gè)字節(jié)遗锣,而我們示例中引用數(shù)組只包含一個(gè)引用元素,所以這里實(shí)例數(shù)據(jù)區(qū)中只有4個(gè)字節(jié)嗤形。相同的到道理,如果示例中的引用數(shù)組包含的元素變?yōu)閮蓚€(gè)引用元素弧圆,那么實(shí)例數(shù)據(jù)區(qū)就會(huì)變?yōu)?個(gè)字節(jié)赋兵,以此類推......。

最后由于Java對(duì)象需要內(nèi)存對(duì)齊至8的倍數(shù)搔预,所以在該引用數(shù)組的實(shí)例數(shù)據(jù)區(qū)后填充了4個(gè)字節(jié)霹期。

關(guān)閉指針壓縮 -XX:-UseCompressedOops

image.png

當(dāng)關(guān)閉壓縮指針時(shí),對(duì)象引用占用內(nèi)存大小變?yōu)榱?code>8個(gè)字節(jié)拯田,所以引用數(shù)組類型的實(shí)例數(shù)據(jù)區(qū)占用了8個(gè)字節(jié)历造。

根據(jù)字段重排列規(guī)則2,在引用數(shù)組類型對(duì)象頭與實(shí)例數(shù)據(jù)區(qū)中間需要填充4個(gè)字節(jié)以保證內(nèi)存對(duì)齊的目的船庇。


總結(jié)

本文筆者詳細(xì)介紹了Java普通對(duì)象以及數(shù)組類型對(duì)象的內(nèi)存布局吭产,以及相關(guān)對(duì)象占用內(nèi)存大小的計(jì)算方法。

以及在對(duì)象內(nèi)存布局中的實(shí)例數(shù)據(jù)區(qū)字段重排列的三個(gè)重要規(guī)則鸭轮。以及后邊由字節(jié)的對(duì)齊填充引出來(lái)的false sharding問(wèn)題臣淤,還有Java8為了解決false sharding而引入的@Contented注解的原理及使用方式。

為了講清楚內(nèi)存對(duì)齊的底層原理窃爷,筆者還花了大量的篇幅講解了內(nèi)存的物理結(jié)構(gòu)以及CPU讀寫內(nèi)存的完整過(guò)程邑蒋。

最后又由內(nèi)存對(duì)齊引出了壓縮指針的工作原理姓蜂。由此我們知道進(jìn)行內(nèi)存對(duì)齊的四個(gè)原因:

  • CPU訪問(wèn)性能:當(dāng)CPU訪問(wèn)內(nèi)存對(duì)齊的地址時(shí),可以通過(guò)一個(gè)read transaction讀取一個(gè)字長(zhǎng)(word size)大小的數(shù)據(jù)出來(lái)医吊。否則就需要兩個(gè)read transaction钱慢。

  • 原子性: CPU可以原子地操作一個(gè)對(duì)齊的word size memory。

  • 盡可能利用CPU緩存:內(nèi)存對(duì)齊可以使對(duì)象或者字段盡可能的被分配到一個(gè)緩存行中卿堂,避免跨緩存行存儲(chǔ)束莫,導(dǎo)致CPU執(zhí)行效率減半。

  • 提升壓縮指針的內(nèi)存尋址空間: 對(duì)象與對(duì)象之間的內(nèi)存對(duì)齊御吞,可以使我們?cè)?4位系統(tǒng)中利用32位對(duì)象引用將內(nèi)存尋址空間提升至32G麦箍。既降低了對(duì)象引用的內(nèi)存占用,又提升了內(nèi)存尋址空間陶珠。

在本文中我們順帶還介紹了和內(nèi)存布局相關(guān)的幾個(gè)JVM參數(shù):-XX:+UseCompressedOops, -XX +CompactFields ,-XX:-RestrictContended ,-XX:ContendedPaddingWidth, -XX:ObjectAlignmentInBytes挟裂。

最后感謝大家能看到這里,我們下篇文章再見(jiàn)~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末揍诽,一起剝皮案震驚了整個(gè)濱河市诀蓉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌暑脆,老刑警劉巖渠啤,帶你破解...
    沈念sama閱讀 216,997評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異添吗,居然都是意外死亡沥曹,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門碟联,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)妓美,“玉大人,你說(shuō)我怎么就攤上這事鲤孵⊙菰酰” “怎么了惜辑?”我有些...
    開(kāi)封第一講書人閱讀 163,359評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我德谅,道長(zhǎng)熏瞄,這世上最難降的妖魔是什么拍埠? 我笑而不...
    開(kāi)封第一講書人閱讀 58,309評(píng)論 1 292
  • 正文 為了忘掉前任秸抚,我火速辦了婚禮,結(jié)果婚禮上漆际,老公的妹妹穿的比我還像新娘淆珊。我一直安慰自己,他們只是感情好奸汇,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,346評(píng)論 6 390
  • 文/花漫 我一把揭開(kāi)白布施符。 她就那樣靜靜地躺著往声,像睡著了一般。 火紅的嫁衣襯著肌膚如雪戳吝。 梳的紋絲不亂的頭發(fā)上浩销,一...
    開(kāi)封第一講書人閱讀 51,258評(píng)論 1 300
  • 那天,我揣著相機(jī)與錄音听哭,去河邊找鬼慢洋。 笑死,一個(gè)胖子當(dāng)著我的面吹牛陆盘,可吹牛的內(nèi)容都是我干的普筹。 我是一名探鬼主播,決...
    沈念sama閱讀 40,122評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼隘马,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼太防!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起酸员,我...
    開(kāi)封第一講書人閱讀 38,970評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤蜒车,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后幔嗦,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體酿愧,經(jīng)...
    沈念sama閱讀 45,403評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,596評(píng)論 3 334
  • 正文 我和宋清朗相戀三年邀泉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了嬉挡。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,769評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡汇恤,死狀恐怖棘伴,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情屁置,我是刑警寧澤,帶...
    沈念sama閱讀 35,464評(píng)論 5 344
  • 正文 年R本政府宣布仁连,位于F島的核電站蓝角,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏饭冬。R本人自食惡果不足惜使鹅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,075評(píng)論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望昌抠。 院中可真熱鬧患朱,春花似錦、人聲如沸炊苫。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,705評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至执虹,卻和暖如春拓挥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背袋励。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,848評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工侥啤, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人茬故。 一個(gè)月前我還...
    沈念sama閱讀 47,831評(píng)論 2 370
  • 正文 我出身青樓盖灸,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親磺芭。 傳聞我的和親對(duì)象是個(gè)殘疾皇子赁炎,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,678評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容