java物件在記憶體中如何分佈 | java上鎖原來就是記憶體佔位,so easy

語言: CN / TW / HK

theme: channing-cyan

前言

  • 本章節作為java鎖章節的開山之作,他的地位絕對是重中之重。java中建立物件通過關鍵字new直接建立。但是一個物件在記憶體中佔多少位元組,每塊位元組是什麼作用,這些相信大家很少關注。如果想學好java多執行緒我覺得了解物件記憶體分佈是有必要的。
  • 為了視覺化分析執行下的Java物件的記憶體情況,本文使用的是openjdk提供的記憶體分析工具;並且我們分析前提是64位hotspot虛擬機器環境下

<dependency>      <groupId>org.openjdk.jol</groupId>      <artifactId>jol-core</artifactId>      <version>0.9</version>  </dependency>

物件分佈

  • 首先我們得明確一個Java物件在記憶體中是什麼樣的結構。這裡有人可能會說能有啥結構。就是記憶體資料二進位制儲存唄。這樣的回答好像說了又好像沒說。
  • 這裡我們通過一個demo案例來具體講解。首先我們系統中存在這麼一個物件

@Data  public class User {      private int age;  }

  • 一個User物件僅有一個Int型別的屬性。他在記憶體中不僅僅是表述int型別的資料。圖中應為kclass point

image-20211207105206289.png

  • 對於一個普通的Java物件就是上述的資料結構儲存在記憶體中的。在64位系統中markword站8位元組;klass point在不開啟指標壓縮的前提下佔8位元組,否則佔位4位元組;這裡的內容就是我們物件中的屬性,因為Java中屬性都是有型別的而每種型別佔位也是不一樣的,比如上面我們int型別是佔位4位元組;至於補齊位元組是啥意思呢?在64位中要求一個物件在記憶體中所佔位元組必須是8的倍數。

image-20211207110314447.png

  • 按照我們補齊位元組的公式,能夠計算出針對User這個物件在記憶體中補齊位元組是0 ,因為他本身佔16位元組是8 的倍數。至於為什麼是8的倍數呢?一個物件在64為系統中佔用位元組數必須是8的倍數。因為8B=64b;針對位元組單位我整理如下表格

| 1B | 8b | | --- | ------ | | 1KB | 1024B | | 1MB | 1024KB | | 1GB | 1024MB |

  • 而我們所說的位元組就是B , 計算機最小單元是位就是b。
  • 這裡我們通過jol來看下

image-20211207111129194.png

  • 通過JOL我們也可以測測Null物件佔多少位元組。大家都知道是4位元組這裡就不測試了
  • 然後我們給User物件增加一個double屬性。在看看記憶體分佈

image-20211207111655882.png

  • 上面我們通過jol能夠檢視到物件在記憶體中分佈情況。但是Java中還存在一種資料型別陣列 。陣列在記憶體中分佈情況稍微不一樣。

image-20211207112145287.png

  • 在物件頭中除了markword和klass point以外還會儲存陣列長度。這裡能夠看到是4位元組的。換句話說Java物件中陣列長度最大是2^32(理論上)。
  • 我們知道陣列中索引是int型別,而java中int型別最大值是(2^31)-1 。所以陣列長度根本用不到2^32這個量級。另外針對陣列長度JVM作出了限制最大是(2^31)-2 ;
  • 試想一下如果我們陣列中儲存的是基本單位最小長度就是4位元組。2^31*4計算下來約等於16GB 。 在想想我們平時給JVM分配多大記憶體空間吧。所以陣列長度限制在那個量級上足夠我們使用。如果真的到達那個量級了也不是我們需要考慮的事情。首先虛擬機器那關就掛了

markword

  • markword是物件記憶體模型中印出來的概念。由上我們得治在64位系統中markword佔8位元組。這8位元組可是物件很重要的屬性。包括物件HashCode、GC次數、鎖標記等資訊都是儲存在markword中的。

image-20211207135807574.png

  • 從左到右依次是高位到低位的順序。但是在我們的JOL輸出的模型中是按照記憶體的順序進行輸出的,所以低位的反而是先輸出。在加上我們是按位元組為單位輸出的。所以先輸出低位位元組。

image-20211207140055041.png

  • 比如說上面JOL輸出記憶體模型。第一個輸出的是01是十六進位制表示式。而01對應的是8位。這8位對應的上圖中末8位
  • 01後面的是00 , 他是第二個輸出的位元組,他對應的是倒數後16位到倒數後8位

image-20211207140936753.png

  • 一個普通物件markword就是上圖所示。因為他既沒有GC資訊也沒有產生hashcode ,更沒有被鎖住,所以記憶體是0000000000000001 。 注意這是高位到低位顯示。下面我們試著呼叫hashcode試試

image-20211207143925519.png

  • 很是奇怪,在hashcode對應位並沒有儲存響應的hashcode , 上面已經打印出來hashcode為400c11fa 。 這是為什麼呢?最終發現是又因為我的User類用的是lombok註解。修改下就可以了

image-20211207144108863.png

  • 另外我將user物件進行上鎖,這時候我們在看看他的markword吧

image-20211207144238473.png

  • 通過觀察最後三位,我們很清楚知道當前物件已經上鎖且是輕量級鎖。關於物件鎖的生命週期我們後面詳細說說。什麼時間是偏向鎖、如何轉成輕量級鎖、最終是重量級鎖、還有什麼叫自旋鎖、無鎖到底是不是鎖等等問題我們下章見。

指標壓縮

  • 上面物件記憶體分佈中我們提到物件頭這個概念。物件頭=markword+klass point +array length;
  • 但是klass point所佔位元組是不固定的。如果開啟了指標壓縮那麼他就小點為4位元組;否則就是8位元組
  • 為什麼存在指標壓縮?這就牽涉到32位系統和64位系統了

在32位到64位的轉變中,我們能夠直觀的感受到記憶體容量的變化。在一個32位的系統中,記憶體地址的寬度就是32位,這就意味著,我們最大能獲取的記憶體空間是2^32(也就是4G)位元組。這個容量明顯不夠用!在一個64位的機器中,理論上,我們能獲取到的記憶體容量是2^64位元組,接下來,我們就談談compressed oops能幫我們做什麼

image-20211207115857599

  • 通過上圖我們能夠得出幾點結論

    1. 64位系統指標變大那麼相同記憶體下存放的指標數量就變少了,同時儲存的普通物件就會變少。很容易就觸發了GC,可以理解因為記憶體被指標佔用了
    2. 容器儲存的指標數量變少了,就導致對用被引用的範圍變小了。即CPU快取命中率變低了
  • 針對上面存在的問題,64系統出現了指標壓縮的這個概念;在JVM中我們可以通過-XX:-UseCompressedOops來設定指標壓縮關閉。

  • 開啟(-XX:+UseCompressedOops) 可以壓縮指標。 關閉(-XX:-UseCompressedOops) 可以關閉壓縮指標

  • 如果GC堆大小在 4G以下,直接砍掉高32位,避免了編碼解碼過程;
  • 如果GC堆大小在 4G以上32G以下,則啟用 UseCompressedOop;
  • 如果GC堆大小 大於32G,壓指失效,使用原來的64位(所以說伺服器記憶體太大不好......)。
「其他文章」