日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

深入理解Java内存架构

發布時間:2024/4/11 java 54 豆豆
生活随笔 收集整理的這篇文章主要介紹了 深入理解Java内存架构 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

hi,大家周末好,今天給大家帶來一篇硬核的技術文章,本文我們將從計算機組成原理的角度詳細闡述對象在JVM內存中是如何布局的,以及什么是內存對齊,如果我們頭比較鐵,就是不進行內存對齊會造成什么樣的后果,最后引出壓縮指針的原理和應用。同時我們還介紹了在高并發場景下,False Sharing產生的原因以及帶來的性能影響。

相信大家看完本文后,一定會收獲很多,話不多說,下面我們正式開始本文的內容~~

本文概要.png

在我們的日常工作中,有時候我們為了防止線上應用發生OOM,所以我們需要在開發的過程中計算一些核心對象在內存中的占用大小,目的是為了更好的了解我們的應用程序內存占用的一個大概情況。

進而根據我們服務器的內存資源限制以及預估的對象創建數量級計算出應用程序占用內存的高低水位線,如果內存占用量超過高水位線,那么就有可能有發生OOM的風險。

我們可以在程序中根據估算出的高低水位線,做一些防止OOM的處理邏輯或者發出告警。

那么核心問題是如何計算一個Java對象在內存中的占用大小呢??

在為大家解答這個問題之前,筆者先來介紹下Java對象在內存中的布局,也就是本文的主題。

1. Java對象的內存布局

Java對象的內存布局.png

如圖所示,Java對象在JVM中是用instanceOopDesc 結構表示而Java對象在JVM堆中的內存布局可以分為三部分:

1.1 對象頭(Header)

每個Java對象都包含一個對象頭,對象頭中包含了兩類信息:

  • MarkWord:在JVM中用markOopDesc 結構表示用于存儲對象自身運行時的數據。比如:hashcode,GC分代年齡,鎖狀態標志,線程持有的鎖,偏向線程Id,偏向時間戳等。在32位操作系統和64位操作系統中MarkWord分別占用4B和8B大小的內存。

  • 類型指針:JVM中的類型指針封裝在klassOopDesc 結構中,類型指針指向了InstanceKclass對象,Java類在JVM中是用InstanceKclass對象封裝的,里邊包含了Java類的元信息,比如:繼承結構,方法,靜態變量,構造函數等。

    • 在不開啟指針壓縮的情況下(-XX:-UseCompressedOops)。在32位操作系統和64位操作系統中類型指針分別占用4B和8B大小的內存。

    • 在開啟指針壓縮的情況下(-XX:+UseCompressedOops)。在32位操作系統和64位操作系統中類型指針分別占用4B和4B大小的內存。

  • 如果Java對象是一個數組類型的話,那么在數組對象的對象頭中還會包含一個4B大小的用于記錄數組長度的屬性。

由于在對象頭中用于記錄數組長度大小的屬性只占4B的內存,所以Java數組可以申請的最大長度為:2^32。

1.2 實例數據(Instance Data)

Java對象在內存中的實例數據區用來存儲Java類中定義的實例字段,包括所有父類中的實例字段。也就是說,雖然子類無法訪問父類的私有實例字段,或者子類的實例字段隱藏了父類的同名實例字段,但是子類的實例還是會為這些父類實例字段分配內存。

Java對象中的字段類型分為兩大類:

  • 基礎類型:Java類中實例字段定義的基礎類型在實例數據區的內存占用如下:

    • long | double占用8個字節。

    • int | float占用4個字節。

    • short | char占用2個字節。

    • byte | boolean占用1個字節。

  • 引用類型:Java類中實例字段的引用類型在實例數據區內存占用分為兩種情況:

    • 不開啟指針壓縮(-XX:-UseCompressedOops):在32位操作系統中引用類型的內存占用為4個字節。在64位操作系統中引用類型的內存占用為8個字節。

    • 開啟指針壓縮(-XX:+UseCompressedOops):在64為操作系統下,引用類型內存占用則變為為4個字節,32位操作系統中引用類型的內存占用繼續為4個字節。

為什么32位操作系統的引用類型占4個字節,而64位操作系統引用類型占8字節?

在Java中,引用類型所保存的是被引用對象的內存地址。在32位操作系統中內存地址是由32個bit表示,因此需要4個字節來記錄內存地址,能夠記錄的虛擬地址空間是2^32大小,也就是只能夠表示4G大小的內存。

而在64位操作系統中內存地址是由64個bit表示,因此需要8個字節來記錄內存地址,但在 64 位系統里只使用了低 48 位,所以它的虛擬地址空間是 2^48大小,能夠表示256T大小的內存,其中低 128T 的空間劃分為用戶空間,高 128T 劃分為內核空間,可以說是非常大了。

在我們從整體上介紹完Java對象在JVM中的內存布局之后,下面我們來看下Java對象中定義的這些實例字段在實例數據區是如何排列布局的:

2. 字段重排列

其實我們在編寫Java源代碼文件的時候定義的那些實例字段的順序會被JVM重新分配排列,這樣做的目的其實是為了內存對齊,那么什么是內存對齊,為什么要進行內存對齊,筆者會隨著文章深入的解讀為大家逐層揭曉答案~~

本小節中,筆者先來為大家介紹一下JVM字段重排列的規則:

JVM重新分配字段的排列順序受-XX:FieldsAllocationStyle參數的影響,默認值為1,實例字段的重新分配策略遵循以下規則:

  • 如果一個字段占用X個字節,那么這個字段的偏移量OFFSET需要對齊至NX

  • 偏移量是指字段的內存地址與Java對象的起始內存地址之間的差值。比如long類型的字段,它內存占用8個字節,那么它的OFFSET應該是8的倍數8N。不足8N的需要填充字節。

  • 在開啟了壓縮指針的64位JVM中,Java類中的第一個字段的OFFSET需要對齊至4N,在關閉壓縮指針的情況下類中第一個字段的OFFSET需要對齊至8N。

  • JVM默認分配字段的順序為:long / double,int / float,short / char,byte / boolean,oops(Ordianry Object Point 引用類型指針),并且父類中定義的實例變量會出現在子類實例變量之前。當設置JVM參數-XX +CompactFields 時(默認),占用內存小于long / double 的字段會允許被插入到對象中第一個 long / double字段之前的間隙中,以避免不必要的內存填充。

  • CompactFields選項參數在JDK14中以被標記為過期了,并在將來的版本中很可能被刪除。詳細細節可查看issue:https://bugs.openjdk.java.net/browse/JDK-8228750

    上邊的三條字段重排列規則非常非常重要,但是讀起來比較繞腦,很抽象不容易理解,筆者把它們先列出來的目的是為了讓大家先有一個朦朦朧朧的感性認識,下面筆者舉一個具體的例子來為大家詳細說明下,在閱讀這個例子的過程中也方便大家深刻的理解這三條重要的字段重排列規則。

    假設現在我們有這樣一個類定義

    public?class?Parent?{long?l;int?i; }public?class?Child?extends?Parent?{long?l;int?i; }
    • 根據上面介紹的規則3我們知道父類中的變量是出現在子類變量之前的,并且字段分配順序應該是long型字段l,應該在int型字段i之前。

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

    • 根據規則1我們知道long型字段在實例數據區的OFFSET需要對齊至8N,而int型字段的OFFSET需要對齊至4N。

    • 根據規則2我們知道如果開啟壓縮指針-XX:+UseCompressedOops,Child對象的第一個字段的OFFSET需要對齊至4N,關閉壓縮指針時-XX:-UseCompressedOops,Child對象的第一個字段的OFFSET需要對齊至8N。

    由于JVM參數UseCompressedOops 和CompactFields 的存在,導致Child對象在實例數據區字段的排列順序分為四種情況,下面我們結合前邊提煉出的這三點規則來看下字段排列順序在這四種情況下的表現。

    2.1 -XX:+UseCompressedOops ?-XX -CompactFields 開啟壓縮指針,關閉字段壓縮

    image.png
    • 偏移量OFFSET = 8的位置存放的是類型指針,由于開啟了壓縮指針所以占用4個字節。對象頭總共占用12個字節:MarkWord(8字節) + 類型指針(4字節)。

    • 根據規則3:父類Parent中的字段是要出現在子類Child的字段之前的并且long型字段在int型字段之前。

    • 根據規則2:在開啟壓縮指針的情況下,Child對象中的第一個字段需要對齊至4N。這里Parent.l字段的OFFSET可以是12也可以是16。

    • 根據規則1:long型字段在實例數據區的OFFSET需要對齊至8N,所以這里Parent.l字段的OFFSET只能是16,因此OFFSET = 12的位置就需要被填充。Child.l字段只能在OFFSET = 32處存儲,不能夠使用OFFSET = 28位置,因為28的位置不是8的倍數無法對齊8N,因此OFFSET = 28的位置被填充了4個字節。

    規則1也規定了int型字段的OFFSET需要對齊至4N,所以Parent.i與Child.i分別存儲以OFFSET = 24和OFFSET = 40的位置。

    因為JVM中的內存對齊除了存在于字段與字段之間還存在于對象與對象之間,Java對象之間的內存地址需要對齊至8N

    所以Child對象的末尾處被填充了4個字節,對象大小由開始的44字節被填充到48字節。

    2.2 ?-XX:+UseCompressedOops ?-XX +CompactFields 開啟壓縮指針,開啟字段壓縮

    image.png
    • 在第一種情況的分析基礎上,我們開啟了-XX +CompactFields壓縮字段,所以導致int型的Parent.i字段可以插入到OFFSET = 12的位置處,以避免不必要的字節填充。

    • 根據規則2:Child對象的第一個字段需要對齊至4N,這里我們看到int型的Parent.i字段是符合這個規則的。

    • 根據規則1:Child對象的所有long型字段都對齊至8N,所有的int型字段都對齊至4N。

    最終得到Child對象大小為36字節,由于Java對象與對象之間的內存地址需要對齊至8N,所以最后Child對象的末尾又被填充了4個字節最終變為40字節。

    這里我們可以看到在開啟字段壓縮-XX +CompactFields的情況下,Child對象的大小由48字節變成了40字節。

    2.3 -XX:-UseCompressedOops ?-XX -CompactFields 關閉壓縮指針,關閉字段壓縮

    image.png

    首先在關閉壓縮指針-UseCompressedOops的情況下,對象頭中的類型指針占用字節變成了8字節。導致對象頭的大小在這種情況下變為了16字節。

    • 根據規則1:long型的變量OFFSET需要對齊至8N。根據規則2:在關閉壓縮指針的情況下,Child對象的第一個字段Parent.l需要對齊至8N。所以這里的Parent.l字段的OFFSET ?= 16。

    • 由于long型的變量OFFSET需要對齊至8N,所以Child.l字段的OFFSET 需要是32,因此OFFSET = 28的位置被填充了4個字節。

    這樣計算出來的Child對象大小為44字節,但是考慮到Java對象與對象的內存地址需要對齊至8N,于是又在對象末尾處填充了4個字節,最終Child對象的內存占用為48字節。

    2.4 ?-XX:-UseCompressedOops ?-XX +CompactFields 關閉壓縮指針,開啟字段壓縮

    在第三種情況的分析基礎上,我們來看下第四種情況的字段排列情況:

    image.png

    由于在關閉指針壓縮的情況下類型指針的大小變為了8個字節,所以導致Child對象中第一個字段Parent.l前邊并沒有空隙,剛好對齊8N,并不需要int型變量的插入。所以即使開啟了字段壓縮-XX +CompactFields,字段的總體排列順序還是不變的。

    默認情況下指針壓縮-XX:+UseCompressedOops以及字段壓縮-XX +CompactFields都是開啟的

    3. 對齊填充(Padding)

    在前一小節關于實例數據區字段重排列的介紹中為了內存對齊而導致的字節填充不僅會出現在字段與字段之間,還會出現在對象與對象之間。

    前邊我們介紹了字段重排列需要遵循的三個重要規則,其中規則1,規則2定義了字段與字段之間的內存對齊規則。規則3定義的是對象字段之間的排列規則。

    為了內存對齊的需要,對象頭與字段之間,以及字段與字段之間需要填充一些不必要的字節。

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

    image.png

    而以上提到的四種情況都會在對象實例數據區的后邊在填充4字節大小的空間,原因是除了需要滿足字段與字段之間的內存對齊之外,還需要滿足對象與對象之間的內存對齊。

    Java 虛擬機堆中對象之間的內存地址需要對齊至8N(8的倍數),如果一個對象占用內存不到8N個字節,那么就必須在對象后填充一些不必要的字節對齊至8N個字節。

    虛擬機中內存對齊的選項為-XX:ObjectAlignmentInBytes,默認為8。也就是說對象與對象之間的內存地址需要對齊至多少倍,是由這個JVM參數控制的。

    我們還是以上邊第一種情況為例說明:圖中對象實際占用是44個字節,但是不是8的倍數,那么就需要再填充4個字節,內存對齊至48個字節。

    以上這些為了內存對齊的目的而在字段與字段之間,對象與對象之間填充的不必要字節,我們就稱之為對齊填充(Padding)。

    4. 對齊填充的應用

    在我們知道了對齊填充的概念之后,大家可能好奇了,為啥我們要進行對齊填充,是要解決什么問題嗎?

    那么就讓我們帶著這個問題,來接著聽筆者往下聊~~

    4.1 解決偽共享問題帶來的對齊填充

    除了以上介紹的兩種對齊填充的場景(字段與字段之間,對象與對象之間),在JAVA中還有一種對齊填充的場景,那就是通過對齊填充的方式來解決False Sharing(偽共享)的問題。

    在介紹False Sharing(偽共享)之前,筆者先來介紹下CPU讀取內存中數據的方式。

    4.1.1 CPU緩存

    根據摩爾定律:芯片中的晶體管數量每隔18個月就會翻一番。導致CPU的性能和處理速度變得越來越快,而提升CPU的運行速度比提升內存的運行速度要容易和便宜的多,所以就導致了CPU與內存之間的速度差距越來越大。

    為了彌補CPU與內存之間巨大的速度差異,提高CPU的處理效率和吞吐,于是人們引入了L1,L2,L3高速緩存集成到CPU中。當然還有L0也就是寄存器,寄存器離CPU最近,訪問速度也最快,基本沒有時延。

    CPU緩存結構.png

    一個CPU里面包含多個核心,我們在購買電腦的時候經常會看到這樣的處理器配置,比如4核8線程。意思是這個CPU包含4個物理核心8個邏輯核心。4個物理核心表示在同一時間可以允許4個線程并行執行,8個邏輯核心表示處理器利用超線程的技術將一個物理核心模擬出了兩個邏輯核心,一個物理核心在同一時間只會執行一個線程,而超線程芯片可以做到線程之間快速切換,當一個線程在訪問內存的空隙,超線程芯片可以馬上切換去執行另外一個線程。因為切換速度非常快,所以在效果上看到是8個線程在同時執行。

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

    從圖中我們可以看到L1Cache是離CPU核心最近的高速緩存,緊接著就是L2Cache,L3Cache,內存。

    離CPU核心越近的緩存訪問速度也越快,造價也就越高,當然容量也就越小。

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

    而L3Cache是整個CPU所有物理核心共享的。

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

    L1Cache

    L1Cache離CPU是最近的,它的訪問速度最快,容量也最小。

    從圖中我們看到L1Cache分為兩個部分,分別是:Data Cache和Instruction Cache。它們一個是存儲數據的,一個是存儲代碼指令的。

    我們可以通過cd /sys/devices/system/cpu/來查看linux機器上的CPU信息。

    image.png

    在/sys/devices/system/cpu/目錄里,我們可以看到CPU的核心數,當然這里指的是邏輯核心

    筆者機器上的處理器并沒有使用超線程技術所以這里其實是4個物理核心。

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

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

    image.png

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

    image.png
    • level:表示該cache信息屬于哪一級,1表示L1Cache。

    • type:表示屬于L1Cache的DataCache。

    • size:表示DataCache的大小為32K。

    • shared_cpu_list:之前我們提到L1Cache和L2Cache是CPU物理核所私有的,而由物理核模擬出來的邏輯核是共享L1Cache和L2Cache的,/sys/devices/system/cpu/目錄下描述的信息是邏輯核。shared_cpu_list描述的正是哪些邏輯核共享這個物理核。

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

    image.png

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

    L2Cache

    L2Cache的信息存儲在index2目錄下:

    image.png

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

    L3Cache

    L3Cache的信息存儲在index3目錄下:

    image.png

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

    當然這些數值在不同的CPU配置上會是不同的,但是總體上來說L1Cache的量級是幾十KB,L2Cache的量級是幾百KB,L3Cache的量級是幾MB。

    4.1.2 CPU緩存行

    前邊我們介紹了CPU的高速緩存結構,引入高速緩存的目的在于消除CPU與內存之間的速度差距,根據程序的局部性原理我們知道,CPU的高速緩存肯定是用來存放熱點數據的。

    程序局部性原理表現為:時間局部性和空間局部性。時間局部性是指如果程序中的某條指令一旦執行,則不久之后該指令可能再次被執行;如果某塊數據被訪問,則不久之后該數據可能再次被訪問。空間局部性是指一旦程序訪問了某個存儲單元,則不久之后,其附近的存儲單元也將被訪問。

    那么在高速緩存中存取數據的基本單位又是什么呢??

    事實上熱點數據在CPU高速緩存中的存取并不是我們想象中的以單獨的變量或者單獨的指針為單位存取的。

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

    image.png

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

    這也就意味著每次CPU從內存中獲取數據或者寫入數據的大小為64個字節,即使你只讀一個bit,CPU也會從內存中加載64字節數據進來。同樣的道理,CPU從高速緩存中同步數據到內存也是按照64字節的單位來進行。

    比如你訪問一個long型數組,當CPU去加載數組中第一個元素時也會同時將后邊的7個元素一起加載進緩存中。這樣一來就加快了遍歷數組的效率。

    long類型在Java中占用8個字節,一個緩存行可以存放8個long型變量。

    事實上,你可以非常快速的遍歷在連續的內存塊中分配的任意數據結構,如果你的數據結構中的項在內存中不是彼此相鄰的(比如:鏈表),這樣就無法利用CPU緩存的優勢。由于數據在內存中不是連續存放的,所以在這些數據結構中的每一個項都可能會出現緩存行未命中(程序局部性原理)的情況。

    還記得我們在?《Reactor在Netty中的實現(創建篇)》中介紹Selector的創建時提到,Netty利用數組實現的自定義SelectedSelectionKeySet類型替換掉了JDK利用HashSet類型實現的sun.nio.ch.SelectorImpl#selectedKeys。目的就是利用CPU緩存的優勢來提高IO活躍的SelectionKeys集合的遍歷性能

    4.2 False Sharing(偽共享)

    我們先來看一個這樣的例子,筆者定義了一個示例類FalseSharding,類中有兩個long型的volatile字段a,b。

    public?class?FalseSharding?{volatile?long?a;volatile?long?b;}

    字段a,b之間邏輯上是獨立的,它們之間一點關系也沒有,分別用來存儲不同的數據,數據之間也沒有關聯。

    FalseSharding類中字段之間的內存布局如下:

    image.png

    FalseSharding類中的字段a,b在內存中是相鄰存儲,分別占用8個字節。

    如果恰好字段a,b被CPU讀進了同一個緩存行,而此時有兩個線程,線程a用來修改字段a,同時線程b用來讀取字段b。

    falsesharding1.png

    在這種場景下,會對線程b的讀取操作造成什么影響呢

    我們知道聲明了volatile關鍵字的變量可以在多線程處理環境下,確保內存的可見性。計算機硬件層會保證對被volatile關鍵字修飾的共享變量進行寫操作后的內存可見性,而這種內存可見性是由Lock前綴指令以及緩存一致性協議(MESI控制協議)共同保證的。

    • Lock前綴指令可以使修改線程所在的處理器中的相應緩存行數據被修改后立馬刷新回內存中,并同時鎖定所有處理器核心中緩存了該修改變量的緩存行,防止多個處理器核心并發修改同一緩存行。

    • 緩存一致性協議主要是用來維護多個處理器核心之間的CPU緩存一致性以及與內存數據的一致性。每個處理器會在總線上嗅探其他處理器準備寫入的內存地址,如果這個內存地址在自己的處理器中被緩存的話,就會將自己處理器中對應的緩存行置為無效,下次需要讀取的該緩存行中的數據的時候,就需要訪問內存獲取。

    基于以上volatile關鍵字原則,我們首先來看第一種影響

    falsesharding2.png
    • 當線程a在處理器core0中對字段a進行修改時,Lock前綴指令會將所有處理器中緩存了字段a的對應緩存行進行鎖定,這樣就會導致線程b在處理器core1中無法讀取和修改自己緩存行的字段b

    • 處理器core0將修改后的字段a所在的緩存行刷新回內存中。

    從圖中我們可以看到此時字段a的值在處理器core0的緩存行中以及在內存中已經發生變化了。但是處理器core1中字段a的值還沒有變化,并且core1中字段a所在的緩存行處于鎖定狀態,無法讀取也無法寫入字段b。

    從上述過程中我們可以看出即使字段a,b之間邏輯上是獨立的,它們之間一點關系也沒有,但是線程a對字段a的修改,導致了線程b無法讀取字段b。

    第二種影響

    faslesharding3.png

    當處理器core0將字段a所在的緩存行刷新回內存的時候,處理器core1會在總線上嗅探到字段a的內存地址正在被其他處理器修改,所以將自己的緩存行置為失效。當線程b在處理器core1中讀取字段b的值時,發現緩存行已被置為失效,core1需要重新從內存中讀取字段b的值即使字段b沒有發生任何變化。

    從以上兩種影響我們看到字段a與字段b實際上并不存在共享,它們之間也沒有相互關聯關系,理論上線程a對字段a的任何操作,都不應該影響線程b對字段b的讀取或者寫入。

    但事實上線程a對字段a的修改導致了字段b在core1中的緩存行被鎖定(Lock前綴指令),進而使得線程b無法讀取字段b。

    線程a所在處理器core0將字段a所在緩存行同步刷新回內存后,導致字段b在core1中的緩存行被置為失效(緩存一致性協議),進而導致線程b需要重新回到內存讀取字段b的值無法利用CPU緩存的優勢。

    由于字段a和字段b在同一個緩存行中,導致了字段a和字段b事實上的共享(原本是不應該被共享的)。這種現象就叫做False Sharing(偽共享)。

    在高并發的場景下,這種偽共享的問題,會對程序性能造成非常大的影響。

    如果線程a對字段a進行修改,與此同時線程b對字段b也進行修改,這種情況對性能的影響更大,因為這會導致core0和core1中相應的緩存行相互失效。

    4.3 False Sharing的解決方案

    既然導致False Sharing出現的原因是字段a和字段b在同一個緩存行導致的,那么我們就要想辦法讓字段a和字段b不在一個緩存行中。

    那么我們怎么做才能夠使得字段a和字段b一定不會被分配到同一個緩存行中呢?

    這時候,本小節的主題字節填充就派上用場了~~

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

    比如我們將一開始的實例代碼修改成這個這樣子,就可以保證字段a和字段b各自獨占一個緩存行了。

    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;}

    修改后的對象在內存中布局如下:

    image.png

    我們看到為了解決False Sharing問題,我們將原本占用32字節的FalseSharding示例對象硬生生的填充到了200字節。這對內存的消耗是非常可觀的。通常為了極致的性能,我們會在一些高并發框架或者JDK的源碼中看到False Sharing的解決場景。因為在高并發場景中,任何微小的性能損失比如False Sharing,都會被無限放大。

    但解決False Sharing的同時又會帶來巨大的內存消耗,所以即使在高并發框架比如disrupter或者JDK中也只是針對那些在多線程場景下被頻繁寫入的共享變量

    這里筆者想強調的是在我們日常工作中,我們不能因為自己手里拿著錘子,就滿眼都是釘子,看到任何釘子都想上去錘兩下。

    image.png

    我們要清晰的分辨出一個問題會帶來哪些影響和損失,這些影響和損失在我們當前業務階段是否可以接受?是否是瓶頸?同時我們也要清晰的了解要解決這些問題我們所要付出的代價。一定要綜合評估,講究一個投入產出比。某些問題雖然是問題,但是在某些階段和場景下并不需要我們投入解決。而有些問題則對于我們當前業務發展階段是瓶頸,我們不得不去解決。我們在架構設計或者程序設計中,方案一定要簡單,合適。并預估一些提前量留有一定的演化空間。

    4.3.1 @Contended注解

    在Java8中引入了一個新注解@Contended,用于解決False Sharing的問題,同時這個注解也會影響到Java對象中的字段排列。

    在上一小節的內容介紹中,我們通過手段填充字段的方式解決了False Sharing的問題,但是這里也有一個問題,因為我們在手動填充字段的時候還需要考慮CPU緩存行的大小,因為雖然現在所有主流的處理器緩存行大小均為64字節,但是也還是有處理器的緩存行大小為32字節,有的甚至是128字節。我們需要考慮很多硬件的限制因素。

    Java8中通過引入@Contended注解幫我們解決了這個問題,我們不在需要去手動填充字段了。下面我們就來看下@Contended注解是如何幫助我們來解決這個問題的~~

    上小節介紹的手動填充字節是在共享變量前后填充64字節大小的空間,這樣只能確保程序在緩存行大小為32字節或者64字節的CPU下獨占緩存行。但是如果CPU的緩存行大小為128字節,這樣依然存在False Sharing的問題。

    引入@Contended注解可以使我們忽略底層硬件設備的差異性,做到Java語言的初衷:平臺無關性。

    @Contended注解默認只是在JDK內部起作用,如果我們的程序代碼中需要使用到@Contended注解,那么需要開啟JVM參數-XX:-RestrictContended才會生效。

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

    @Contended注解可以標注在類上也可以標注在類中的字段上,被@Contended標注的對象會獨占緩存行,不會和任何變量或者對象共享緩存行。

    • @Contended標注在類上表示該類對象中的實例數據整體需要獨占緩存行。不能與其他實例數據共享緩存行。

    • @Contended標注在類中的字段上表示該字段需要獨占緩存行。

    • 除此之外@Contended還提供了分組的概念,注解中的value屬性表示contention group 。屬于統一分組下的變量,它們在內存中是連續存放的,可以允許共享緩存行。不同分組之間不允許共享緩存行。

    下面我們來分別看下@Contended注解在這三種使用場景下是怎樣影響字段之間的排列的。

    @Contended標注在類上
    @Contended public?class?FalseSharding?{volatile?long?a;volatile?long?b;volatile?int?c;volatile?int?d; }

    當@Contended標注在FalseSharding示例類上時,表示FalseSharding示例對象中的整個實例數據區需要獨占緩存行,不能與其他對象或者變量共享緩存行。

    這種情況下的內存布局:

    image.png

    如圖中所示,FalseSharding示例類被標注了@Contended之后,JVM會在FalseSharding示例對象的實例數據區前后填充128個字節,保證實例數據區內的字段之間內存是連續的,并且保證整個實例數據區獨占緩存行,不會與實例數據區之外的數據共享緩存行。

    細心的朋友可能已經發現了問題,我們之前不是提到緩存行的大小為64字節嗎?為什么這里會填充128字節呢

    而且之前介紹的手動填充也是填充的64字節,為什么@Contended注解會采用兩倍的緩存行大小來填充呢?

    其實這里的原因有兩個:

  • 首先第一個原因,我們之前也已經提到過了,目前大部分主流的CPU緩存行是64字節,但是也有部分CPU緩存行是32字節或者128字節,如果只填充64字節的話,在緩存行大小為32字節和64字節的CPU中是可以做到獨占緩存行從而避免FalseSharding的,但在緩存行大小為128字節的CPU中還是會出現FalseSharding問題,這里Java采用了悲觀的一種做法,默認都是填充128字節,雖然對于大部分情況下比較浪費,但是屏蔽了底層硬件的差異。

  • 不過@Contended注解填充字節的大小我們可以通過JVM參數-XX:ContendedPaddingWidth指定,有效值范圍0 - 8192,默認為128。

    • 第二個原因其實是最為核心的一個原因,主要是為了防止CPU Adjacent Sector Prefetch(CPU相鄰扇區預取)特性所帶來的FalseSharding問題。

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

    CPU Adjacent Sector Prefetch是Intel處理器特有的BIOS功能特性,默認是enabled。主要作用就是利用程序局部性原理,當CPU從內存中請求數據,并讀取當前請求數據所在緩存行時,會進一步預取與當前緩存行相鄰的下一個緩存行,這樣當我們的程序在順序處理數據時,會提高CPU處理效率。這一點也體現了程序局部性原理中的空間局部性特征

    當CPU Adjacent Sector Prefetch特性被disabled禁用時,CPU就只會獲取當前請求數據所在的緩存行,不會預取下一個緩存行。

    所以在當CPU Adjacent Sector Prefetch啟用(enabled)的時候,CPU其實同時處理的是兩個緩存行,在這種情況下,就需要填充兩倍緩存行大小(128字節)來避免CPU Adjacent Sector Prefetch所帶來的的FalseSharding問題。

    @Contended標注在字段上
    public?class?FalseSharding?{@Contendedvolatile?long?a;@Contendedvolatile?long?b;volatile?int?c;volatile?long?d; }image.png

    這次我們將 @Contended注解標注在了FalseSharding示例類中的字段a和字段b上,這樣帶來的效果是字段a和字段b各自獨占緩存行。從內存布局上看,字段a和字段b前后分別被填充了128個字節,來確保字段a和字段b不與任何數據共享緩存行。

    而沒有被@Contended注解標注字段c和字段d則在內存中連續存儲,可以共享緩存行。

    @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放在另一個content group下。

    這樣處在同一分組group1下的字段a與字段b在內存中是連續存儲的,可以共享緩存行。

    同理處在同一分組group2下的字段c與字段d在內存中也是連續存儲的,也允許共享緩存行。

    但是分組之間是不能共享緩存行的,所以在字段分組的前后各填充128字節,來保證分組之間的變量不能共享緩存行。

    5. 內存對齊

    通過以上內容我們了解到Java對象中的實例數據區字段需要進行內存對齊而導致在JVM中會被重排列以及通過填充緩存行避免false sharding的目的所帶來的字節對齊填充。

    我們也了解到內存對齊不僅發生在對象與對象之間,也發生在對象中的字段之間。

    那么在本小節中筆者將為大家介紹什么是內存對齊,在本節的內容開始之前筆者先來拋出兩個問題:

    • 為什么要進行內存對齊?如果就是頭比較鐵,就是不內存對齊,會產生什么樣的后果?

    • Java 虛擬機堆中對象的起始地址為什么需要對齊至 8的倍數?為什么不對齊至4的倍數或16的倍數或32的倍數呢?

    帶著這兩個問題,下面我們正式開始本節的內容~~~

    5.1 內存結構

    我們平時所稱的內存也叫隨機訪問存儲器(random-access memory)也叫RAM。而RAM分為兩類:

    • 一類是靜態RAM(SRAM),這類SRAM用于前邊介紹的CPU高速緩存L1Cache,L2Cache,L3Cache。其特點是訪問速度快,訪問速度為1 - 30個時鐘周期,但是容量小,造價高。

    • 另一類則是動態RAM(DRAM),這類DRAM用于我們常說的主存上,其特點的是訪問速度慢(相對高速緩存),訪問速度為50 - 200個時鐘周期,但是容量大,造價便宜些(相對高速緩存)。

    內存由一個一個的存儲器模塊(memory module)組成,它們插在主板的擴展槽上。常見的存儲器模塊通常以64位為單位(8個字節)傳輸數據到存儲控制器上或者從存儲控制器傳出數據。

    image.png

    如圖所示內存條上黑色的元器件就是存儲器模塊(memory module)。多個存儲器模塊連接到存儲控制器上,就聚合成了主存。

    內存結構.png

    而前邊介紹到的DRAM芯片就包裝在存儲器模塊中,每個存儲器模塊中包含8個DRAM芯片,依次編號為0 - 7。

    存儲器模塊.png

    而每一個DRAM芯片的存儲結構是一個二維矩陣,二維矩陣中存儲的元素我們稱為超單元(supercell),每個supercell大小為一個字節(8 bit)。每個supercell都由一個坐標地址(i,j)。

    i表示二維矩陣中的行地址,在計算機中行地址稱為RAS(row access strobe,行訪問選通脈沖)。j表示二維矩陣中的列地址,在計算機中列地址稱為CAS(column access strobe,列訪問選通脈沖)。

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

    DRAM結構.png

    DRAM芯片中的信息通過引腳流入流出DRAM芯片。每個引腳攜帶1 bit的信號。

    圖中DRAM芯片包含了兩個地址引腳(addr),因為我們要通過RAS,CAS來定位要獲取的supercell。還有8個數據引腳(data),因為DRAM芯片的IO單位為一個字節(8 bit),所以需要8個data引腳從DRAM芯片傳入傳出數據。

    注意這里只是為了解釋地址引腳和數據引腳的概念,實際硬件中的引腳數量是不一定的。

    5.2 DRAM芯片的訪問

    我們現在就以讀取上圖中坐標地址為(2,2)的supercell為例,來說明訪問DRAM芯片的過程。

    DRAM芯片訪問.png
  • 首先存儲控制器將行地址RAS = 2通過地址引腳發送給DRAM芯片。

  • DRAM芯片根據RAS = 2將二維矩陣中的第二行的全部內容拷貝到內部行緩沖區中。

  • 接下來存儲控制器會通過地址引腳發送CAS = 2到DRAM芯片中。

  • DRAM芯片從內部行緩沖區中根據CAS = 2拷貝出第二列的supercell并通過數據引腳發送給存儲控制器。

  • DRAM芯片的IO單位為一個supercell,也就是一個字節(8 bit)。

    5.3 CPU如何讀寫主存

    前邊我們介紹了內存的物理結構,以及如何訪問內存中的DRAM芯片獲取supercell中存儲的數據(一個字節)。

    本小節我們來介紹下CPU是如何訪問內存的。

    CPU與內存之間的總線結構.png

    其中關于CPU芯片的內部結構我們在介紹false sharding的時候已經詳細的介紹過了,這里我們主要聚焦在CPU與內存之間的總線架構上。

    5.3.1 總線結構

    CPU與內存之間的數據交互是通過總線(bus)完成的,而數據在總線上的傳送是通過一系列的步驟完成的,這些步驟稱為總線事務(bus transaction)。

    其中數據從內存傳送到CPU稱之為讀事務(read transaction),數據從CPU傳送到內存稱之為寫事務(write transaction)。

    總線上傳輸的信號包括:地址信號,數據信號,控制信號。其中控制總線上傳輸的控制信號可以同步事務,并能夠標識出當前正在被執行的事務信息:

    • 當前這個事務是到內存的?還是到磁盤的?或者是到其他IO設備的?

    • 這個事務是讀還是寫?

    • 總線上傳輸的地址信號(內存地址),還是數據信號(數據)?。

    還記得我們前邊講到的MESI緩存一致性協議嗎?當core0修改字段a的值時,其他CPU核心會在總線上嗅探字段a的內存地址,如果嗅探到總線上出現字段a的內存地址,說明有人在修改字段a,這樣其他CPU核心就會失效自己緩存字段a所在的cache line。

    如上圖所示,其中系統總線是連接CPU與IO bridge的,存儲總線是來連接IO bridge和主存的。

    IO bridge負責將系統總線上的電子信號轉換成存儲總線上的電子信號。IO bridge也會將系統總線和存儲總線連接到IO總線(磁盤等IO設備)上。這里我們看到IO bridge其實起的作用就是轉換不同總線上的電子信號。

    5.3.2 CPU從內存讀取數據過程

    假設CPU現在要將內存地址為A的內容加載到寄存器中進行運算。

    CPU讀取內存.png

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

  • CPU將內存地址A放到系統總線上。隨后IO bridge將信號傳遞到存儲總線上。

  • 主存感受到存儲總線上的地址信號并通過存儲控制器將存儲總線上的內存地址A讀取出來。

  • 存儲控制器通過內存地址A定位到具體的存儲器模塊,從DRAM芯片中取出內存地址A對應的數據X。

  • 存儲控制器將讀取到的數據X放到存儲總線上,隨后IO bridge將存儲總線上的數據信號轉換為系統總線上的數據信號,然后繼續沿著系統總線傳遞。

  • CPU芯片感受到系統總線上的數據信號,將數據從系統總線上讀取出來并拷貝到寄存器中。

  • 以上就是CPU讀取內存數據到寄存器中的完整過程。

    但是其中還涉及到一個重要的過程,這里我們還是需要攤開來介紹一下,那就是存儲控制器如何通過內存地址A從主存中讀取出對應的數據X的?

    接下來我們結合前邊介紹的內存結構以及從DRAM芯片讀取數據的過程,來總體介紹下如何從主存中讀取數據。

    5.3.3 如何根據內存地址從主存中讀取數據

    前邊介紹到,當主存中的存儲控制器感受到了存儲總線上的地址信號時,會將內存地址從存儲總線上讀取出來。

    隨后會通過內存地址定位到具體的存儲器模塊。還記得內存結構中的存儲器模塊嗎??

    內存結構.png

    而每個存儲器模塊中包含了8個DRAM芯片,編號從0 - 7。

    存儲器模塊.png

    存儲控制器會將內存地址轉換為DRAM芯片中supercell在二維矩陣中的坐標地址(RAS,CAS)。并將這個坐標地址發送給對應的存儲器模塊。隨后存儲器模塊會將RAS和CAS廣播到存儲器模塊中的所有DRAM芯片。依次通過(RAS,CAS)從DRAM0到DRAM7讀取到相應的supercell。

    DRAM芯片訪問.png

    我們知道一個supercell存儲了8 bit數據,這里我們從DRAM0到DRAM7 依次讀取到了8個supercell也就是8個字節,然后將這8個字節返回給存儲控制器,由存儲控制器將數據放到存儲總線上。

    CPU總是以word size為單位從內存中讀取數據,在64位處理器中的word size為8個字節。64位的內存也只能每次吞吐8個字節。

    CPU每次會向內存讀寫一個cache line大小的數據(64個字節),但是內存一次只能吞吐8個字節。

    所以在內存地址對應的存儲器模塊中,DRAM0芯片存儲第一個低位字節(supercell),DRAM1芯片存儲第二個字節,......依次類推DRAM7芯片存儲最后一個高位字節。

    內存一次讀取和寫入的單位是8個字節。而且在程序員眼里連續的內存地址實際上在物理上是不連續的。因為這連續的8個字節其實是存儲于不同的DRAM芯片上的。每個DRAM芯片存儲一個字節(supercell)。

    讀取存儲器模塊數據.png

    5.3.4 CPU向內存寫入數據過程

    我們現在假設CPU要將寄存器中的數據X寫到內存地址A中。同樣的道理,CPU芯片中的總線接口會向總線發起寫事務(write transaction)。寫事務步驟如下:

  • CPU將要寫入的內存地址A放入系統總線上。

  • 通過IO bridge的信號轉換,將內存地址A傳遞到存儲總線上。

  • 存儲控制器感受到存儲總線上的地址信號,將內存地址A從存儲總線上讀取出來,并等待數據的到達。

  • CPU將寄存器中的數據拷貝到系統總線上,通過IO bridge的信號轉換,將數據傳遞到存儲總線上。

  • 存儲控制器感受到存儲總線上的數據信號,將數據從存儲總線上讀取出來。

  • 存儲控制器通過內存地址A定位到具體的存儲器模塊,最后將數據寫入存儲器模塊中的8個DRAM芯片中。

  • 6. 為什么要內存對齊

    我們在了解了內存結構以及CPU讀寫內存的過程之后,現在我們回過頭來討論下本小節開頭的問題:為什么要內存對齊?

    下面筆者從三個方面來介紹下要進行內存對齊的原因:

    速度

    CPU讀取數據的單位是根據word size來的,在64位處理器中word size = 8字節,所以CPU向內存讀寫數據的單位為8字節。

    在64位內存中,內存IO單位為8個字節,我們前邊也提到內存結構中的存儲器模塊通常以64位為單位(8個字節)傳輸數據到存儲控制器上或者從存儲控制器傳出數據。因為每次內存IO讀取數據都是從數據所在具體的存儲器模塊中包含的這8個DRAM芯片中以相同的(RAM,CAS)依次讀取一個字節,然后在存儲控制器中聚合成8個字節返回給CPU。

    讀取存儲器模塊數據.png

    由于存儲器模塊中這種由8個DRAM芯片組成的物理存儲結構的限制,內存讀取數據只能是按照地址順序8個字節的依次讀取----8個字節8個字節地來讀取數據。

    內存IO單位.png
    • 假設我們現在讀取0x0000 - 0x0007這段連續內存地址上的8個字節。由于內存讀取是按照8個字節為單位依次順序讀取的,而我們要讀取的這段內存地址的起始地址是0(8的倍數),所以0x0000 - 0x0007中每個地址的坐標都是相同的(RAS,CAS)。所以他可以在8個DRAM芯片中通過相同的(RAS,CAS)一次性讀取出來。

    • 如果我們現在讀取0x0008 - 0x0015這段連續內存上的8個字節也是一樣的,因為內存段起始地址為8(8的倍數),所以這段內存上的每個內存地址在DREAM芯片中的坐標地址(RAS,CAS)也是相同的,我們也可以一次性讀取出來。

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

    • 但如果我們現在讀取0x0007 - 0x0014這段連續內存上的8個字節情況就不一樣了,由于起始地址0x0007在DRAM芯片中的(RAS,CAS)與后邊地址0x0008 - 0x0014的(RAS,CAS)不相同,所以CPU只能先從0x0000 - 0x0007讀取8個字節出來先放入結果寄存器中并左移7個字節(目的是只獲取0x0007),然后CPU在從0x0008 - 0x0015讀取8個字節出來放入臨時寄存器中并右移1個字節(目的是獲取0x0008 - 0x0014)最后與結果寄存器或運算。最終得到0x0007 - 0x0014地址段上的8個字節。

    從以上分析過程來看,當CPU訪問內存對齊的地址時,比如0x0000和0x0008這兩個起始地址都是對齊至8的倍數。CPU可以通過一次read transaction讀取出來。

    但是當CPU訪問內存沒有對齊的地址時,比如0x0007這個起始地址就沒有對齊至8的倍數。CPU就需要兩次read transaction才能將數據讀取出來。

    還記得筆者在小節開頭提出的問題嗎 ??"Java 虛擬機堆中對象的起始地址為什么需要對齊至 8的倍數?為什么不對齊至4的倍數或16的倍數或32的倍數呢?" 現在你能回答了嗎???

    原子性

    CPU可以原子地操作一個對齊的word size memory。64位處理器中word size = 8字節。

    盡量分配在一個緩存行中

    前邊在介紹false sharding的時候我們提到目前主流處理器中的cache line大小為64字節,堆中對象的起始地址通過內存對齊至8的倍數,可以讓對象盡可能的分配到一個緩存行中。一個內存起始地址未對齊的對象可能會跨緩存行存儲,這樣會導致CPU的執行效率慢2倍

    其中對象中字段內存對齊的其中一個重要原因也是讓字段只出現在同一 CPU 的緩存行中。如果字段不是對齊的,那么就有可能出現跨緩存行的字段。也就是說,該字段的讀取可能需要替換兩個緩存行,而該字段的存儲也會同時污染兩個緩存行。這兩種情況對程序的執行效率而言都是不利的。

    另外在《2. 字段重排列》這一小節介紹的三種字段對齊規則,是保證在字段內存對齊的基礎上使得實例數據區占用內存盡可能的小

    7. 壓縮指針

    在介紹完關于內存對齊的相關內容之后,我們來介紹下前邊經常提到的壓縮指針。可以通過JVM參數XX:+UseCompressedOops開啟,當然默認是開啟的。

    在本小節內容開啟之前,我們先來討論一個問題,那就是為什么要使用壓縮指針??

    假設我們現在正在準備將32位系統切換到64位系統,起初我們可能會期望系統性能會立馬得到提升,但現實情況可能并不是這樣的。

    在JVM中導致性能下降的最主要原因就是64位系統中的對象引用。在前邊我們也提到過,64位系統中對象的引用以及類型指針占用64 bit也就是8個字節。

    這就導致了在64位系統中的對象引用占用的內存空間是32位系統中的兩倍大小,因此間接的導致了在64位系統中更多的內存消耗以及更頻繁的GC發生,GC占用的CPU時間越多,那么我們的應用程序占用CPU的時間就越少。

    另外一個就是對象的引用變大了,那么CPU可緩存的對象相對就少了,增加了對內存的訪問。綜合以上幾點從而導致了系統性能的下降。

    從另一方面來說,在64位系統中內存的尋址空間為2^48 = 256T,在現實情況中我們真的需要這么大的尋址空間嗎??好像也沒必要吧~~

    于是我們就有了新的想法:那么我們是否應該切換回32位系統呢?

    如果我們切換回32位系統,我們怎么解決在32位系統中擁有超過4G的內存尋址空間呢?因為現在4G的內存大小對于現在的應用來說明顯是不夠的。

    我想以上的這些問題,也是當初JVM的開發者需要面對和解決的,當然他們也交出了非常完美的答卷,那就是使用壓縮指針可以在64位系統中利用32位的對象引用獲得超過4G的內存尋址空間

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

    還記得之前我們在介紹對齊填充和內存對齊小節中提到的,在Java虛擬機堆中對象的起始地址必須對齊至8的倍數嗎?

    由于堆中對象的起始地址均是對齊至8的倍數,所以對象引用在開啟壓縮指針情況下的32位二進制的后三位始終是0(因為它們始終可以被8整除)。

    既然JVM已經知道了這些對象的內存地址后三位始終是0,那么這些無意義的0就沒必要在堆中繼續存儲。相反,我們可以利用存儲0的這3位bit存儲一些有意義的信息,這樣我們就多出3位bit的尋址空間。

    這樣在存儲的時候,JVM還是按照32位來存儲,只不過后三位原本用來存儲0的bit現在被我們用來存放有意義的地址空間信息。

    當尋址的時候,JVM將這32位的對象引用左移3位(后三位補0)。這就導致了在開啟壓縮指針的情況下,我們原本32位的內存尋址空間一下變成了35位。可尋址的內存空間變為2^32 * 2^3 = 32G。

    壓縮指針.png

    這樣一來,JVM雖然額外的執行了一些位運算但是極大的提高了尋址空間,并且將對象引用占用內存大小降低了一半,節省了大量空間。況且這些位運算對于CPU來說是非常容易且輕量的操作

    通過壓縮指針的原理我挖掘到了內存對齊的另一個重要原因就是通過內存對齊至8的倍數,我們可以在64位系統中使用壓縮指針通過32位的對象引用將尋址空間提升至32G.

    從Java7開始,當maximum heap size小于32G的時候,壓縮指針是默認開啟的。但是當maximum heap size大于32G的時候,壓縮指針就會關閉。

    那么我們如何在壓縮指針開啟的情況下進一步擴大尋址空間呢???

    7.2 如何進一步擴大尋址空間

    前邊提到我們在Java虛擬機堆中對象起始地址均需要對其至8的倍數,不過這個數值我們可以通過JVM參數-XX:ObjectAlignmentInBytes 來改變(默認值為8)。當然這個數值的必須是2的次冪,數值范圍需要在8 - 256之間。

    正是因為對象地址對齊至8的倍數,才會多出3位bit讓我們存儲額外的地址信息,進而將4G的尋址空間提升至32G。

    同樣的道理,如果我們將ObjectAlignmentInBytes的數值設置為16呢?

    對象地址均對齊至16的倍數,那么就會多出4位bit讓我們存儲額外的地址信息。尋址空間變為2^32 * 2^4 = 64G。

    通過以上規律,我們就能知道,在64位系統中開啟壓縮指針的情況,尋址范圍的計算公式:4G * ObjectAlignmentInBytes = 尋址范圍。

    但是筆者并不建議大家貿然這樣做,因為增大了ObjectAlignmentInBytes雖然能擴大尋址范圍,但是這同時也可能增加了對象之間的字節填充,導致壓縮指針沒有達到原本節省空間的效果。

    8. 數組對象的內存布局

    前邊大量的篇幅我們都是在討論Java普通對象在內存中的布局情況,最后這一小節我們再來說下Java中的數組對象在內存中是如何布局的。

    8.1 基本類型數組的內存布局

    基本類型數組內存布局.png

    上圖表示的是基本類型數組在內存中的布局,基本類型數組在JVM中用typeArrayOop結構體表示,基本類型數組類型元信息用TypeArrayKlass 結構體表示。

    數組的內存布局大體上和普通對象的內存布局差不多,唯一不同的是在數組類型對象頭中多出了4個字節用來表示數組長度的部分。

    我們還是分別以開啟指針壓縮和關閉指針壓縮兩種情況,通過下面的例子來進行說明:

    long[]?longArrayLayout?=?new?long[1];

    開啟指針壓縮 -XX:+UseCompressedOops

    image.png

    我們看到紅框部分即為數組類型對象頭中多出來一個4字節大小用來表示數組長度的部分。

    因為我們示例中的long型數組只有一個元素,所以實例數據區的大小只有8字節。如果我們示例中的long型數組變為兩個元素,那么實例數據區的大小就會變為16字節,以此類推................。

    關閉指針壓縮 ?-XX:-UseCompressedOops

    image.png

    當關閉了指針壓縮時,對象頭中的MarkWord還是占用8個字節,但是類型指針從4個字節變為了8個字節。數組長度屬性還是不變保持4個字節。

    這里我們發現是實例數據區與對象頭之間發生了對齊填充。大家還記得這是為什么嗎??

    我們前邊在字段重排列小節介紹了三種字段排列規則在這里繼續適用:

    • 規則1:如果一個字段占用X個字節,那么這個字段的偏移量OFFSET需要對齊至NX。

    • 規則2:在開啟了壓縮指針的64位JVM中,Java類中的第一個字段的OFFSET需要對齊至4N,在關閉壓縮指針的情況下類中第一個字段的OFFSET需要對齊至8N。

    這里基本數組類型的實例數據區中是long型,在關閉指針壓縮的情況下,根據規則1和規則2需要對齊至8的倍數,所以要在其與對象頭之間填充4個字節,達到內存對齊的目的,起始地址變為24。

    8.2 引用類型數組的內存布局

    引用類型數組的內存布局.png

    上圖表示的是引用類型數組在內存中的布局,引用類型數組在JVM中用objArrayOop結構體表示,基本類型數組類型元信息用ObjArrayKlass 結構體表示。

    同樣在引用類型數組的對象頭中也會有一個4字節大小用來表示數組長度的部分。

    我們還是分別以開啟指針壓縮和關閉指針壓縮兩種情況,通過下面的例子來進行說明:

    public?class?ReferenceArrayLayout?{char?a;int?b;short?c; }ReferenceArrayLayout[]?referenceArrayLayout?=?new?ReferenceArrayLayout[1];

    開啟指針壓縮 -XX:+UseCompressedOops

    image.png

    引用數組類型內存布局與基礎數組類型內存布局最大的不同在于它們的實例數據區。由于開啟了壓縮指針,所以對象引用占用內存大小為4個字節,而我們示例中引用數組只包含一個引用元素,所以這里實例數據區中只有4個字節。相同的到道理,如果示例中的引用數組包含的元素變為兩個引用元素,那么實例數據區就會變為8個字節,以此類推......。

    最后由于Java對象需要內存對齊至8的倍數,所以在該引用數組的實例數據區后填充了4個字節。

    關閉指針壓縮 -XX:-UseCompressedOops

    image.png

    當關閉壓縮指針時,對象引用占用內存大小變為了8個字節,所以引用數組類型的實例數據區占用了8個字節。

    根據字段重排列規則2,在引用數組類型對象頭與實例數據區中間需要填充4個字節以保證內存對齊的目的。


    總結

    本文筆者詳細介紹了Java普通對象以及數組類型對象的內存布局,以及相關對象占用內存大小的計算方法。

    以及在對象內存布局中的實例數據區字段重排列的三個重要規則。以及后邊由字節的對齊填充引出來的false sharding問題,還有Java8為了解決false sharding而引入的@Contented注解的原理及使用方式。

    為了講清楚內存對齊的底層原理,筆者還花了大量的篇幅講解了內存的物理結構以及CPU讀寫內存的完整過程。

    最后又由內存對齊引出了壓縮指針的工作原理。由此我們知道進行內存對齊的四個原因:

    • CPU訪問性能:當CPU訪問內存對齊的地址時,可以通過一個read transaction讀取一個字長(word size)大小的數據出來。否則就需要兩個read transaction。

    • 原子性:CPU可以原子地操作一個對齊的word size memory。

    • 盡可能利用CPU緩存:內存對齊可以使對象或者字段盡可能的被分配到一個緩存行中,避免跨緩存行存儲,導致CPU執行效率減半。

    • 提升壓縮指針的內存尋址空間: 對象與對象之間的內存對齊,可以使我們在64位系統中利用32位對象引用將內存尋址空間提升至32G。既降低了對象引用的內存占用,又提升了內存尋址空間。

    在本文中我們順帶還介紹了和內存布局相關的幾個JVM參數:-XX:+UseCompressedOops, -XX +CompactFields ,-XX:-RestrictContended ,-XX:ContendedPaddingWidth, -XX:ObjectAlignmentInBytes。

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

    - END -


    看完一鍵三連在看轉發點贊

    是對文章最大的贊賞,極客重生感謝你

    推薦閱讀

    定個目標|建立自己的技術知識體系


    大廠后臺開發基本功修煉路線和經典資料


    一文搞懂JAVA與GO垃圾回收

    JVM底層原理解析


    你好,這里是極客重生,我是阿榮,大家都叫我榮哥,從華為->外企->到互聯網大廠,目前是大廠資深工程師,多次獲得五星員工,多年職場經驗,技術扎實,專業后端開發和后臺架構設計,熱愛底層技術,豐富的實戰經驗,分享技術的本質原理,希望幫助更多人蛻變重生,拿BAT大廠offer,培養高級工程師能力,成為技術專家,實現高薪夢想,期待你的關注!點擊藍字查看我的成長之路

    校招/社招/簡歷/面試技巧/大廠技術棧分析/后端開發進階/優秀開源項目/直播分享/技術視野/實戰高手等,?極客星球希望成為最有技術價值星球,盡最大努力為星球的同學提供技術和成長幫助!詳情查看->極客星球

    ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 求點贊,在看,分享三連

    總結

    以上是生活随笔為你收集整理的深入理解Java内存架构的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

    色是在线视频 | 超碰国产97 | 91视频国产高清 | 激情网站免费观看 | 久久99国产精品久久 | 日韩欧美一二三 | 天天操天天射天天爽 | 久99精品| 天堂av免费| 五月天色综合 | 亚洲综合色婷婷 | 99精品视频在线观看 | 免费观看国产视频 | 在线免费观看国产黄色 | 蜜桃av人人夜夜澡人人爽 | 久久99国产精品久久99 | 久久久网页 | 日日夜夜免费精品视频 | 黄色影院在线免费观看 | 色婷婷综合激情 | 国产精品白浆 | 97精品国自产拍在线观看 | 日韩网站在线免费观看 | 成人av免费在线 | 精品久久久久久国产偷窥 | 天天干天天操天天爱 | 国产一区在线观看视频 | 特级毛片aaa | 一本一本久久a久久精品牛牛影视 | 国产热re99久久6国产精品 | 日本久久久精品视频 | 国内免费的中文字幕 | 黄a在线| 成人在线免费看视频 | 天天射天天干天天 | 成人全视频免费观看在线看 | www.亚洲视频.com | 亚洲精品国内 | 欧美精选一区二区三区 | 97成人精品 | 久久av影院 | 中国一级片视频 | 狠狠网站 | 国产在线精品一区二区不卡了 | 久久国语 | 久久人人爽人人爽人人片av软件 | 手机看片99| 亚洲无在线| 一区精品在线 | 成人中文字幕在线 | 亚洲一区二区三区在线看 | 久久国产一区二区 | www色综合 | 国产粉嫩在线 | 在线观看视频你懂 | 国产精品久久久精品 | 午夜在线观看影院 | 欧美黄在线 | 欧美久久久一区二区三区 | 欧美精品久久久久久久久久丰满 | 黄色成人在线 | 久久91网| 国产小视频你懂的在线 | 亚洲欧美激情精品一区二区 | 免费亚洲黄色 | 国产美女精品人人做人人爽 | 亚洲成人精品影院 | 九色在线 | 久久综合久久综合这里只有精品 | 久久手机精品视频 | 黄色av电影 | 日韩视频在线不卡 | 久久视频这里有精品 | 天天躁天天躁天天躁婷 | 日批视频在线播放 | 亚洲精品美女久久久久网站 | 亚洲三级影院 | 伊人首页 | 一级黄色av | 色婷婷激婷婷情综天天 | 欧美激情在线网站 | 麻豆精品在线视频 | 99久久久久免费精品国产 | 丁香六月婷婷开心婷婷网 | 久久69精品久久久久久久电影好 | 亚洲成av人影片在线观看 | 草久在线 | 久久国产电影院 | 亚洲成人av电影 | 久草视频在线资源 | 欧美日韩精品在线播放 | av网址最新 | 中文字幕在线观看国产 | 中文久草| 久久精品老司机 | 国产精品久久伊人 | 日日碰狠狠添天天爽超碰97久久 | 91九色成人| 激情丁香综合五月 | 色在线观看网站 | 国产传媒中文字幕 | 久久这里 | 色搞搞 | 午夜精品福利在线 | 五月婷婷开心中文字幕 | 国产精品一区二区三区四 | 三级av在线免费观看 | av不卡中文 | 国产五月色婷婷六月丁香视频 | 欧美91视频 | 91视频88av | 国产一线天在线观看 | 久久成人精品电影 | 免费视频xnxx com | 一区二区视频在线播放 | 欧美一区二区三区激情视频 | 操久在线| 婷婷激情五月综合 | 亚洲精品午夜aaa久久久 | 免费看十八岁美女 | 日本爱爱免费视频 | 少妇搡bbbb搡bbb搡忠贞 | 激情五月婷婷激情 | 丁香资源影视免费观看 | 99精品免费久久久久久久久日本 | 午夜12点 | 久久激情电影 | 国产99久久九九精品免费 | 91av免费观看 | 成人av免费播放 | 亚洲国产成人精品电影在线观看 | 中文字幕91| 色综合久久精品 | 色com网 | 亚洲午夜av电影 | 夜夜躁天天躁很躁波 | 午夜精品久久久久久久99 | 中文字幕在线观看一区 | 开心激情综合网 | 欧美成人精品欧美一级乱黄 | 最近最新mv字幕免费观看 | 在线视频 你懂得 | 在线免费观看黄色大片 | 国产a级精品 | 久久高清国产视频 | 激情小说 五月 | 综合激情 | 久草在线视频在线 | japanesefreesexvideo高潮 | 丰满少妇麻豆av | 天天操天天操天天 | 久久草草热国产精品直播 | 五月天堂网 | 精品91| 中文字幕色播 | 久久国产精品视频 | 十八岁以下禁止观看的1000个网站 | 国产中出在线观看 | 婷婷综合成人 | 国产精品久久久久久久久久久杏吧 | 国产视频不卡一区 | 天天精品视频 | 激情综合网五月激情 | 日韩中文字幕在线 | 一区二区中文字幕在线播放 | 美女久久视频 | 国产午夜激情视频 | 国产专区视频在线观看 | 国产一区二区免费 | 精品视频在线看 | 深夜免费福利在线 | 夜夜骑天天操 | 日韩专区在线播放 | 久草在线看片 | 国产一区二区影院 | 国产精品福利视频 | 国产精品igao视频网网址 | 国产成人久久 | 人人爽人人爽人人爽人人爽 | 色视频网站免费观看 | 成人在线播放视频 | 国产视频精品久久 | 日韩中文在线电影 | 丰满少妇在线观看资源站 | av资源中文字幕 | 伊人国产在线观看 | 色婷婷六月天 | 国产高清黄 | 国产a精品 | 天天色天天艹 | 日韩中文字幕亚洲一区二区va在线 | 久久字幕网 | 综合色站导航 | 欧美日韩国产mv | 婷婷色网视频在线播放 | 六月色| 91精品一区国产高清在线gif | 在线国产福利 | 欧美成人精品欧美一级乱黄 | 日韩丝袜在线观看 | 亚洲成人午夜av | 天天操,夜夜操 | 国产精品无av码在线观看 | 成人精品久久久 | 国产亚洲成av片在线观看 | 91视频免费网址 | 97视频播放 | 91在线文字幕 | 91精品国产综合久久久久久久 | 久草电影在线 | 日韩mv欧美mv国产精品 | 日日爽视频 | 午夜三级影院 | 色就色,综合激情 | 高清日韩一区二区 | 国产精品嫩草影院99网站 | 久久97久久 | 射九九| 久操中文字幕在线观看 | 国产精品久久久久久久久久久不卡 | 久久视频免费在线观看 | 中文在线免费一区三区 | 久久96国产精品久久99漫画 | 一区二区亚洲精品 | 久久黄视频 | 在线中文视频 | 日韩三级视频在线观看 | 午夜精品一区二区三区视频免费看 | 亚洲国产97在线精品一区 | 国产日韩精品在线观看 | av在线最新 | 久草视频在线免费看 | 午夜精品久久久久久久久久久久 | 亚洲 综合 国产 精品 | 最近高清中文字幕 | 日韩精品欧美专区 | 国产在线不卡 | 91精品网站 | 亚洲毛片一区二区三区 | 国产视频亚洲 | 99re国产视频| 天天操网站 | 亚洲经典在线 | 亚洲激精日韩激精欧美精品 | 天天射射天天 | 天天天操操操 | 久久免费黄色 | 国产99久久精品一区二区永久免费 | www.人人草 | 久久久久网站 | 成人h在线| 91色偷偷| aa一级片| 欧美在线视频一区二区 | 国产123区在线观看 国产精品麻豆91 | 91在线最新 | а天堂中文最新一区二区三区 | 一区二区三区中文字幕在线观看 | 日韩专区一区二区 | 麻豆视频在线免费 | 日本爽妇网 | 激情综合国产 | 久久97精品| 中文字幕精品一区二区精品 | 麻豆91在线观看 | av免费观看网站 | 91精品在线免费观看视频 | 天天操夜| 亚洲欧美国产精品18p | 久久久精品欧美一区二区免费 | 中文字幕传媒 | 中文字幕中文字幕在线中文字幕三区 | 夜夜操天天操 | 深爱激情开心 | 日韩电影一区二区三区 | 四虎永久免费网站 | 国产精品6999成人免费视频 | 亚洲综合婷婷 | 国产福利一区二区三区视频 | 91亚洲精品久久久中文字幕 | 天天玩天天操天天射 | 中文字幕免费一区二区 | 九月婷婷人人澡人人添人人爽 | 高清美女视频 | 成人国产网站 | 久久综合久久综合九色 | 中文字幕在线观看一区二区三区 | 国产 在线观看 | 国产视频在线观看一区二区 | 亚洲资源片 | 国产玖玖在线 | 91桃色视频 | 99热九九这里只有精品10 | 青春草免费在线视频 | 狠狠插天天干 | 五月婷婷国产 | 在线观看成人小视频 | 中文字幕在线播放视频 | 91精品免费视频 | 99久久久久国产精品免费 | 天堂在线一区二区三区 | 天天曰天天| 国产精品久久一区二区三区不卡 | 九九免费在线看完整版 | 色婷五月| 成人a视频在线观看 | 久久精品福利视频 | 免费三级黄色片 | 91久草视频 | 日韩av五月天| 成人a在线观看高清电影 | 免费网站观看www在线观看 | 日韩精品一卡 | 日韩精品无 | 国产精品理论在线观看 | 久操伊人 | 免费福利视频导航 | 免费久草视频 | 五月天久久久久 | 精品一区二区在线观看 | 99在线免费视频 | 99精品欧美一区二区三区黑人哦 | 日韩三级视频在线看 | 五月天综合婷婷 | av免费看电影 | 97精品在线视频 | 亚洲欧美成人 | 国产一区二区在线观看视频 | 亚洲综合色激情五月 | 国产色视频一区二区三区qq号 | 亚洲精品字幕在线 | 美女久久久久久久 | 99re亚洲国产精品 | 最近更新的中文字幕 | 综合激情| 最新高清无码专区 | 成人小视频在线免费观看 | 国产香蕉在线 | 精品在线一区二区 | 亚洲片在线资源 | 日本精品一区二区三区在线播放视频 | 一区二区 不卡 | 开心色婷婷 | 日本精品久久久久影院 | 奇米影视777四色米奇影院 | 日韩欧美精品在线 | 中文在线| 国产在线播放一区二区 | 久久福利综合 | 丁香激情综合国产 | 99精品视频免费观看 | 91av手机在线观看 | 久久久久久久久久免费 | 十八岁以下禁止观看的1000个网站 | 久久久www免费电影网 | 亚洲美女精品区人人人人 | 精品九九九 | 中文字幕在线观看完整版电影 | 日本精品久久久久影院 | 久久久久久久久久久久久久免费看 | 国产成人三级在线观看 | 久久理论片 | 久久久噜噜噜久久久 | 97人人艹 | 亚洲精品一区二区三区在线观看 | 亚洲欧美在线视频免费 | av成人免费在线 | 成人理论在线观看 | 婷婷在线观看视频 | 亚洲精品一区二区三区高潮 | 久久官网 | 天天干,狠狠干 | 免费成人在线观看视频 | 天天操天天干天天操天天干 | 三级av黄色 | 麻豆国产网站入口 | 日韩色爱 | 久精品视频在线观看 | 97超碰在 | 成人91视频| 精品专区一区二区 | 91在线免费观看国产 | 精品一区二区在线免费观看 | 黄色av成人在线 | 日韩中字在线 | 人人玩人人添人人澡97 | 人人爽人人看 | 九九久久久久久久久激情 | 久久深夜| 黄网站污 | 欧美精品亚洲二区 | 中文字幕黄色网址 | 99精品国产99久久久久久福利 | 亚洲成年人在线播放 | 国产高清久久久久 | 久久久久国产成人精品亚洲午夜 | 久久精品久久精品久久 | 最近最新中文字幕视频 | 亚洲欧美精品一区二区 | 777xxx欧美 | 久久久人| 麻豆免费视频观看 | 久久久久久久久久伊人 | www.黄色片.com| 在线播放视频一区 | 999视频网站| 91免费网址 | 免费观看黄 | 亚欧日韩成人h片 | 国产在线视频资源 | 中文字幕中文字幕在线中文字幕三区 | 精品主播网红福利资源观看 | 蜜臀av网址| 天无日天天操天天干 | 激情视频免费在线 | 黄色免费电影网站 | 中文字幕日韩高清 | 久久午夜剧场 | 免费看在线看www777 | 国产精品99精品久久免费 | 狠狠色丁香婷婷综合久久片 | 色婷婷免费 | 视频一区二区国产 | 午夜精品一区二区三区可下载 | 99视频偷窥在线精品国自产拍 | 国产精品久久毛片 | 免费看成年人 | 日韩毛片在线播放 | 国产精品视频免费观看 | 日韩精品视频免费专区在线播放 | 五月天视频网站 | 亚洲最大免费成人网 | 五月天久久狠狠 | 超碰人人99| 碰超在线 | 免费在线观看成人小视频 | www色,com| 国产一级免费在线 | 久久免费视频精品 | 欧美日韩高清在线 | 亚洲精品美女视频 | 国产最新网站 | 97免费在线视频 | 日韩在线播放av | 国产一二区精品 | 久久久 精品 | www.五月天激情 | 五月婷婷中文 | 精品久久久久一区二区国产 | 91av视频在线免费观看 | 亚洲成aⅴ人在线观看 | 在线观看日韩 | 亚洲国产精品va在线看黑人 | 婷婷av电影| 91麻豆精品国产91久久久无限制版 | 超黄视频网站 | avwww在线观看 | 一区二区在线电影 | 一本一本久久a久久精品牛牛影视 | 久久夜夜夜 | 欧美激情精品一区 | 久久久精品国产一区二区电影四季 | 久久久这里有精品 | 天天色播 | 成人黄视频 | 中文字幕免费久久 | 亚洲理论视频 | 国产精品一区二区在线看 | 69国产精品视频免费观看 | 国产精品99久久久精品 | 九九久久国产精品 | 久久国产精品免费视频 | 国产三级香港三韩国三级 | 免费网站观看www在线观看 | 国产精品夜夜夜一区二区三区尤 | 精品国产免费久久 | 久久综合久久伊人 | 久草视频看看 | 九草视频在线 | 九九热视频在线播放 | 国产精品日韩欧美一区二区 | 久久精品3 | 99综合电影在线视频 | 91在线资源| 99久热在线精品视频 | 欧美午夜理伦三级在线观看 | 欧美a级一区二区 | 999久久国产精品免费观看网站 | 最新国产精品拍自在线播放 | 久草久草在线观看 | 亚洲一二三区精品 | 成人免费看电影 | 色99中文字幕 | 狠狠干夜夜爽 | 色综合色综合久久综合频道88 | 日韩v在线 | 亚洲精品久久久久中文字幕m男 | 国产在线97 | 少妇av片 | 成人九九视频 | 欧美视频日韩 | 久久国产精品久久久 | 日韩精品不卡在线 | 香蕉视频网站在线观看 | 国产高清在线观看av | 91看毛片| 91精品国产三级a在线观看 | 久久精品99北条麻妃 | 91精品国产91热久久久做人人 | 岛国精品一区二区 | 美女国产在线 | 五月婷婷在线观看视频 | 97在线视频免费 | 亚洲 成人 一区 | 欧美日产在线观看 | 色激情五月 | 在线精品视频免费播放 | 欧美一二三区在线观看 | 欧美性春潮 | 偷拍精品一区二区三区 | 中文字幕av有码 | 在线观看欧美成人 | 国产专区在线播放 | 精品99久久 | 久久在线观看 | 成人永久免费 | 天天干天天干天天射 | av成人动漫 | 欧美日韩一区二区视频在线观看 | 色网址99 | 亚洲人成人天堂h久久 | 亚洲最新av网站 | 人成免费网站 | av理论电影| 亚洲午夜久久久久 | 亚洲高清视频在线观看 | h视频在线看| 在线观看精品一区 | 欧美一级电影 | 人人要人人澡人人爽人人dvd | 97国产在线观看 | 久草在线免费电影 | 久久人人爽人人人人片 | 日韩特级毛片 | 99在线精品观看 | 国产精品资源 | 日韩天天干 | 国产精品岛国久久久久久久久红粉 | 黄色高清视频在线观看 | 日韩在线电影一区二区 | 九九热av| 香蕉手机在线 | 在线观看黄色免费视频 | 99re视频在线观看 | 麻豆国产精品永久免费视频 | 日韩aa视频| 久久这里只有精品视频99 | 狠狠色丁香婷婷综合欧美 | 五月婷婷综合激情网 | 亚洲乱码在线 | 成人av视屏 | 国产激情久久久 | 国产色影院 | 欧美激情精品久久久久久变态 | 国产精品福利无圣光在线一区 | 久精品视频免费观看2 | 亚洲三级影院 | 99精品国产一区二区三区麻豆 | 国产精品第一视频 | 久久久久久看片 | 久久综合福利 | 黄色国产精品 | 成人免费视频网 | 国产99re | 精品一区二区三区四区在线 | 99福利影院 | 激情小说 五月 | 三级性生活视频 | 免费在线看成人av | 精品一区精品二区 | 黄色1级毛片 | 天天碰天天操视频 | 99热高清| www好男人 | 香蕉91视频 | 欧美久久久久久久久中文字幕 | 九九九热精品免费视频观看网站 | www日日| 久久午夜视频 | 五月天伊人 | 女人18片毛片90分钟 | 99色在线观看 | 久久狠狠一本精品综合网 | 久久精品国产一区二区电影 | 91成人精品一区在线播放69 | 国产男男gay做爰 | 91尤物国产尤物福利在线播放 | 黄色av一区 | 91三级视频| 国产成人综合精品 | 精品国产乱码久久久久久1区二区 | 国内一级片在线观看 | 天天干,天天射,天天操,天天摸 | 国产精品永久久久久久久久久 | 在线观看av网 | 亚洲精品自在在线观看 | 日本中文字幕在线播放 | 91片黄在线观 | 国产999精品久久久久久 | 91精品在线视频 | 视频二区在线 | 九七人人干 | 4438全国亚洲精品在线观看视频 | 亚洲永久精品在线观看 | 91视频最新网址 | 成人午夜网址 | 欧美色888| 国产精品久久久久永久免费看 | 午夜久久久久久久久久影院 | 在线日韩三级 | 在线观看av小说 | 99re国产 | 欧美在线视频日韩 | 久在线 | 91成人精品一区在线播放69 | 欧美日韩精品网站 | 久热久草 | 国产精品99久久久久人中文网介绍 | 蜜臀av性久久久久蜜臀aⅴ流畅 | 国产一区二区三区免费视频 | 天天做日日爱夜夜爽 | 成人av在线直播 | 日韩精品不卡 | 黄色精品网站 | 亚洲性少妇性猛交wwww乱大交 | 五月婷婷久草 | 97成人在线观看视频 | 亚洲国产日韩在线 | 国产91av视频在线观看 | 黄色软件在线观看 | 国产99久久久国产精品免费看 | 国产黄色精品在线观看 | 国产高清99 | 24小时日本在线www免费的 | 久久久久久久久艹 | 成人一区二区三区中文字幕 | 99热精品在线观看 | 欧美午夜a| av在线com| 久久婷婷一区二区三区 | 亚洲,国产成人av | 精品国产区 | 日韩精品一区二区在线观看视频 | av在线com | 久久综合久久八八 | 国产香蕉久久精品综合网 | 欧美日一级片 | 国产精品网站一区二区三区 | 九色视频网址 | 私人av| 麻豆传媒视频在线 | 极品美女被弄高潮视频网站 | 国内偷拍精品视频 | 成人黄大片视频在线观看 | www夜夜 | 911国产在线观看 | 97在线观视频免费观看 | 婷婷激情影院 | 日韩aa视频 | 一级大片在线观看 | 国产精品区在线观看 | 亚洲国产片 | 欧美日韩免费网站 | 精品少妇一区二区三区在线 | 国产一区视频在线播放 | 久久在视频 | 日韩福利在线观看 | 欧美国产日韩激情 | 夜夜夜夜爽 | 国产中文字幕大全 | 99热手机在线 | 五月天视频网 | 成年人免费在线看 | 天天草综合 | 在线高清一区 | 一区免费视频 | 玖玖玖国产精品 | 色综合在 | 亚洲经典中文字幕 | 国产精品99久久久 | 黄色精品视频 | 99精品久久久久久久久久综合 | 人人干干人人 | 日韩电影在线观看一区二区三区 | 国产黄色精品在线观看 | 国产精品原创av片国产免费 | 综合色婷婷 | 精品福利在线视频 | 狠狠狠色丁香婷婷综合久久88 | 黄色免费网站 | 国产成人性色生活片 | 国产视| 国产看片免费 | 日韩大片免费在线观看 | 九九精品在线观看 | 中文字幕av影院 | 深夜免费小视频 | 狠狠狠色狠狠色综合 | 亚洲成色| 欧美男同视频网站 | 久久精品久久精品久久精品 | 天天五月天色 | 精品91 | 亚洲 中文 在线 精品 | 66av99精品福利视频在线 | 天天性天天草 | 久久成人麻豆午夜电影 | 去干成人网 | 色网站国产精品 | 亚洲成人999 | 91九色蝌蚪| 人人爽人人片 | 亚洲国产精品va在线看黑人动漫 | 91日韩在线播放 | 久久av免费电影 | 国产短视频在线播放 | 狠狠撸电影| 日韩精品免费在线观看视频 | 人人澡人人爽 | 日韩黄色免费在线观看 | av免费电影网站 | 国产亚洲精品久久久久久 | 在线免费观看成人 | 久久久久久久久爱 | 成人影视片 | 精品国产一区二区三区日日嗨 | 中文字幕在线成人 | 精品国产自 | 久操视频在线免费看 | 国产超碰在线 | 国产精品视频久久 | 五月婷在线观看 | 国产h在线观看 | 一本到视频在线观看 | 最近中文国产在线视频 | 99精品欧美一区二区三区黑人哦 | 丁香婷五月 | 97免费在线视频 | 中文字幕在 | 人人干狠狠操 | 黄色片网站大全 | 天天色草| 亚洲人人射 | 久久精品国产精品 | 国产伦精品一区二区三区在线 | 91视频 - 88av | 91一区二区三区久久久久国产乱 | 日韩在线免费电影 | 日韩精品久久久 | 国产日产av | 最近乱久中文字幕 | 超碰久热 | 麻豆国产精品永久免费视频 | 国产成人香蕉 | av在线电影网站 | 97超碰人人澡人人爱 | 天天射天天射天天射 | 国产伦精品一区二区三区高清 | 天天干天天操天天入 | 精品中文字幕在线播放 | 超碰人人做 | 久久久精品在线观看 | 免费在线观看视频a | 婷婷去俺也去六月色 | 色av男人的天堂免费在线 | 欧美一级xxxx | 在线视频电影 | 天堂av在线免费 | 久久国产经典 | 国产精品久久久久久久免费 | 99久久久国产精品免费99 | 激情久久久久久久久久久久久久久久 | 丁香av| 精品久久久久_ | 91色在线观看视频 | 精品成人a区在线观看 | 国产九九九精品视频 | 日韩网站在线观看 | 国产精品欧美一区二区 | 亚洲黄色小说网 | 精品视频久久 | www.成人久久 | 色视频在线观看 | 久久精品www人人爽人人 | 99自拍视频在线观看 | 中文字幕久久精品亚洲乱码 | 国产在线一线 | 国产欧美精品一区二区三区四区 | 亚洲色图 校园春色 | 国产91在线 | 美洲 | 91在线播放视频 | 久久久久女人精品毛片 | 国产亚洲综合精品 | 在线视频你懂 | 亚洲精品五月天 | 久久天 | 日韩电影在线视频 | 色老板在线| 天天操天天草 | 91视频麻豆 | 中文字幕区 | 97成人精品区在线播放 | 黄色亚洲精品 | 免费看一及片 | 精品国模一区二区三区 | 麻豆国产视频下载 | 美女网站色| 久久免费99| 中字幕视频在线永久在线观看免费 | 天天草天天色 | av一本久道久久波多野结衣 | 99久久99久久精品国产片果冰 | 久久九九精品久久 | 免费看国产黄色 | 亚洲欧美在线观看视频 | 97涩涩视频 | 91九色在线观看视频 | 日韩免费三级 | 肉色欧美久久久久久久免费看 | 992tv在线成人免费观看 | 成 人 免费 黄 色 视频 | 日韩av电影中文字幕在线观看 | 色狠狠综合 | 天天亚洲 | 激情婷婷在线 | 国产免费成人 | 亚洲视频免费在线看 | 日韩综合在线观看 | 91精品国产乱码在线观看 | 国产日韩精品视频 | 久久电影日韩 | 狠狠干干 | 午夜精品一区二区三区在线播放 | 精品美女久久久久久免费 | 国产一区二区成人 | 国产成人黄色片 | 手机看片国产日韩 | 免费在线观看黄色网 | 免费h精品视频在线播放 | 天天做天天爱天天爽综合网 | 免费精品在线观看 | 99国内精品久久久久久久 | 五月综合激情婷婷 | 欧美一二三四在线 | 欧美无极色 | 亚洲日本色 | 色a综合 | 亚洲精品国产高清 | 手机看片午夜 | 亚洲精品乱码久久久久久久久久 | 中文字幕久久精品 | 天天操天天干天天干 | 亚洲综合在线观看视频 | 久久资源在线 | 一区二区三区久久 | 国产免费一区二区三区网站免费 | 黄色电影在线免费观看 | 久久精品中文字幕少妇 | 九九热久久久 | 久草在线视频免费资源观看 | 精品视频免费播放 | 最新99热 | 爱射综合| 日日碰狠狠添天天爽超碰97久久 | 成人免费在线观看av | 免费在线观看黄 | 日韩v欧美v日本v亚洲v国产v | 免费进去里的视频 | 精品国产电影一区 | 天天干天天摸 | 激情五月色播五月 | 免费在线黄 | 久久久久久久久久网站 | 中文字幕精品一区二区三区电影 | 久久视频在线观看中文字幕 | 日韩欧美视频免费在线观看 | 韩国av免费观看 | 97在线视频网站 | 国产一区电影在线观看 | 一区二区三区在线电影 | 国产在线精品一区二区三区 | 精品少妇一区二区三区在线 | 亚av在线 | 99久久精品免费看国产四区 | 高潮毛片无遮挡高清免费 | 性色va | 在线a视频 | 在线亚洲精品 | 日本在线免费看 | 天堂麻豆 | 免费在线激情电影 | 婷婷色中文字幕 | 国产三级香港三韩国三级 | 日韩在线第一 | 91在线日韩 | 久草91视频| 91大神免费视频 | 五月婷婷香蕉 | 在线观看91精品国产网站 | a天堂中文在线 | 国产亚洲激情视频在线 | 91亚洲网 | 涩涩爱夜夜爱 | 97av精品| 色综合久 | 久草在线在线精品观看 | 国产在线中文字幕 | 日韩系列 | 日韩精品一卡 | zzijzzij亚洲成熟少妇 | 国内精品视频一区二区三区八戒 | 9999国产精品 | 在线亚洲午夜片av大片 | 日韩欧美精品在线视频 | 日批网站免费观看 | 福利久久久 | 亚洲撸撸| 91自拍成人 | 三级黄在线 | 五月婷婷伊人网 | 奇米网444| 99久久激情视频 | 国产成人性色生活片 | 黄色资源在线 | 手机看片中文字幕 | 日日夜夜天天 | 99久热在线精品视频 | 久久精彩免费视频 | 久久er99热精品一区二区 | 中文字幕 国产 一区 | 国产视频 亚洲精品 | 亚洲欧美国产精品va在线观看 | 日本视频高清 | 在线黄色毛片 | 2022久久国产露脸精品国产 | 又黄又爽又色无遮挡免费 | 91插插插免费视频 | 五月婷婷丁香激情 | 伊人射 | 国产成人精品一区二区三区网站观看 | 99电影| 国产精品ⅴa有声小说 | 国产字幕在线观看 | 亚洲欧洲精品一区二区精品久久久 | 久久久久久99精品 | 午夜精品久久久久久久99 | 亚洲成 人精品 | 国产精品综合久久久久 | 色婷婷在线视频 | av片中文字幕| 在线播放 日韩专区 | 国产中文字幕第一页 | 五月婷婷中文网 | 国产高清在线永久 | 超碰999 | 五月婷婷亚洲 | 中文字幕在线电影 | 超碰资源在线 | 一区二区三区在线不卡 | 欧美精品v国产精品v日韩精品 | 国产精品久久久久久久久免费 | 激情久久久久 | 日本精品一区二区三区在线播放视频 | 久久草av| 狠狠狠色狠狠色综合 | 区一区二区三在线观看 | 免费日韩一区 | 黄色大片免费播放 | 久草干 | 久久综合九色欧美综合狠狠 | 四虎成人av| 国产精品一区二区三区99 | 成人免费网视频 | 亚洲va欧美va| 久久视频一区 | 国产高清一区二区 | www国产精品com | 久久精品—区二区三区 | 色资源网在线观看 | 六月色婷 | 日本久久电影网 | 日本中文字幕一二区观 | 干综合网| 国产精品电影一区 | 日韩性xxxx | 中文字幕乱码日本亚洲一区二区 | 国产精品免费视频久久久 | 国产一区二区精品久久91 |