java物件在記憶體中如何分佈 | java上鎖原來就是記憶體佔位,so easy
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
- 對於一個普通的Java物件就是上述的資料結構儲存在記憶體中的。在64位系統中markword站8位元組;klass point在不開啟指標壓縮的前提下佔8位元組,否則佔位4位元組;這裡的內容就是我們物件中的屬性,因為Java中屬性都是有型別的而每種型別佔位也是不一樣的,比如上面我們int型別是佔位4位元組;至於補齊位元組是啥意思呢?在64位中要求一個物件在記憶體中所佔位元組必須是8的倍數。
- 按照我們補齊位元組的公式,能夠計算出針對User這個物件在記憶體中補齊位元組是0 ,因為他本身佔16位元組是8 的倍數。至於為什麼是8的倍數呢?一個物件在64為系統中佔用位元組數必須是8的倍數。因為8B=64b;針對位元組單位我整理如下表格
| 1B | 8b | | --- | ------ | | 1KB | 1024B | | 1MB | 1024KB | | 1GB | 1024MB |
- 而我們所說的位元組就是B , 計算機最小單元是位就是b。
- 這裡我們通過jol來看下
- 通過JOL我們也可以測測Null物件佔多少位元組。大家都知道是4位元組這裡就不測試了
- 然後我們給User物件增加一個double屬性。在看看記憶體分佈
- 上面我們通過jol能夠檢視到物件在記憶體中分佈情況。但是Java中還存在一種資料型別陣列 。陣列在記憶體中分佈情況稍微不一樣。
- 在物件頭中除了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中的。
- 從左到右依次是高位到低位的順序。但是在我們的JOL輸出的模型中是按照記憶體的順序進行輸出的,所以低位的反而是先輸出。在加上我們是按位元組為單位輸出的。所以先輸出低位位元組。
- 比如說上面JOL輸出記憶體模型。第一個輸出的是01是十六進位制表示式。而01對應的是8位。這8位對應的上圖中末8位
- 01後面的是00 , 他是第二個輸出的位元組,他對應的是倒數後16位到倒數後8位
- 一個普通物件markword就是上圖所示。因為他既沒有GC資訊也沒有產生hashcode ,更沒有被鎖住,所以記憶體是
0000000000000001
。 注意這是高位到低位顯示。下面我們試著呼叫hashcode試試
- 很是奇怪,在hashcode對應位並沒有儲存響應的hashcode , 上面已經打印出來hashcode為
400c11fa
。 這是為什麼呢?最終發現是又因為我的User類用的是lombok註解。修改下就可以了
- 另外我將user物件進行上鎖,這時候我們在看看他的markword吧
- 通過觀察最後三位,我們很清楚知道當前物件已經上鎖且是輕量級鎖。關於物件鎖的生命週期我們後面詳細說說。什麼時間是偏向鎖、如何轉成輕量級鎖、最終是重量級鎖、還有什麼叫自旋鎖、無鎖到底是不是鎖等等問題我們下章見。
指標壓縮
- 上面物件記憶體分佈中我們提到物件頭這個概念。物件頭=
markword+klass point +array length
; - 但是
klass point
所佔位元組是不固定的。如果開啟了指標壓縮那麼他就小點為4位元組;否則就是8位元組 - 為什麼存在指標壓縮?這就牽涉到32位系統和64位系統了
在32位到64位的轉變中,我們能夠直觀的感受到記憶體容量的變化。在一個32位的系統中,記憶體地址的寬度就是32位,這就意味著,我們最大能獲取的記憶體空間是2^32(也就是4G)位元組。這個容量明顯不夠用!在一個64位的機器中,理論上,我們能獲取到的記憶體容量是2^64位元組,接下來,我們就談談compressed oops能幫我們做什麼
-
通過上圖我們能夠得出幾點結論
- 64位系統指標變大那麼相同記憶體下存放的指標數量就變少了,同時儲存的普通物件就會變少。很容易就觸發了GC,可以理解因為記憶體被指標佔用了
- 容器儲存的指標數量變少了,就導致對用被引用的範圍變小了。即CPU快取命中率變低了
-
針對上面存在的問題,64系統出現了指標壓縮的這個概念;在JVM中我們可以通過
-XX:-UseCompressedOops
來設定指標壓縮關閉。 -
開啟(-XX:+UseCompressedOops) 可以壓縮指標。 關閉(-XX:-UseCompressedOops) 可以關閉壓縮指標
- 如果GC堆大小在 4G以下,直接砍掉高32位,避免了編碼解碼過程;
- 如果GC堆大小在 4G以上32G以下,則啟用 UseCompressedOop;
- 如果GC堆大小 大於32G,壓指失效,使用原來的64位(所以說伺服器記憶體太大不好......)。
- 我正在參與掘金技術社群創作者簽約計劃招募活動,點選連結報名投稿。
「其他文章」
- 避免回表,引入索引下推|提高索引命中率 | 提前下班啦
- TDengine 時序性資料庫為什麼海量資料下不卡頓呢
- 神奇的XPath,快速完成前端及XML的元素定位,茫茫大海不迷路
- springboot通用分支處理---還在硬編碼特殊處理邏輯?超級管理員不應該被區別對待
- Spring事務太強大了,相容資料庫同時給我們提供多種組合應對業務需求
- java物件在記憶體中如何分佈 | java上鎖原來就是記憶體佔位,so easy
- linux三劍客之編輯器sed出廠
- linux三劍客awk教你如何裁剪結果集
- 執行緒池7個引數拿捏死死的,完爆面試官
- 執行緒池存在的意義
- 多年程式設計師總結下來的懶人必備指令碼之進度條⚠️製作
- java中的static關鍵字說清楚還得靠JVM
- 設計模式存在哪些關聯關係,六種關係傻傻分不清--- UML圖示詳解
- 每次需求評審產品總是讓我提高程式碼複用,說白了就是合成複用原則
- 越級上報不可行,各司其職才是王道---迪米特法則
- 偏向鎖/輕量鎖/重級鎖鎖鎖更健康,上鎖解鎖到底是怎麼完成實現的,我來告訴你
- 狸貓換太子里氏替換原則;不要一味的進行抽象否則最後你無法hold你的物件
- 設計模式是我擺脫碼畜的唯一出路---依賴倒轉原則
- 學好數理化,寫遍所有程式碼都不怕,我用數學分類討論的思想解決
- synchronized已經不在臃腫了,放下對他的成見之初識輕量級鎖