面試官超級喜歡問的MarkWord
前言
年底了,最近好幾天沒吃飯了,在微博吃瓜吃的飽飽的。
續上次被問到synchronized鎖后,面試官繼續刁難阿巴阿巴,進而深入到對象頭中相關的概念。
當場拿offer
面試官: 上次提到了synchronized鎖,那你知道synchronized鎖具體是怎么實現的嗎?
阿巴阿巴: 在JDK版本1.5及之前的版本synchronized主要靠的是Monitor對象來完成,同步代碼塊使用的是monitorenter和monitorexit指令,而synchronized修飾方法靠的是ACC_SYNCHRONIZED標識,這些都是進入到內核態進行加鎖的,然后將競爭鎖失敗的線程直接掛起,等待后面恢復。
阿巴阿巴: 在JDK1.6及之后的版本中,synchronized鎖得到了優化,引入了自適應自旋鎖、偏向鎖、輕量鎖,他們主要優化了鎖在一定條件下的性能。避免了一上來就加重量級鎖,等待鎖的其他線程只能乖乖掛起,對cpu性能影響特別大。
阿巴阿巴: 在hotspot虛擬機中,對象頭主要包括兩部分 MarkWord和Klass Pointer。
MarkWord 對象標記字段,默認存儲的是對象的HashCode,GC的分代年齡(2bit最大表示15)和鎖的標志信息等。對于32位的虛擬機MarkWord占32bit,對于64位的虛擬機MarkWord占用64字節。
Klass Pointer Class 對象的類型指針,它指向對象對應的Class對象的內存地址。大小占4字節(指針壓縮的情況下為4字節,未進行指針壓縮則占8字節)。32位虛擬機MarkWord分布
64位虛擬機MarkWord分布
圖片來源https://blog.csdn.net/weixin_40816843/article/details/120811181
查看虛擬機是多少位的可以使用:java -version
面試官: 我們怎么看對象頭里的MarkWord數據呢?
阿巴阿巴: 可以看到在openJDK中關于MarkWord的描述,首先可以在Github上找到Open Jdk的源碼
gitHub地址:https://github.com/openjdk/jdk
在IDE中打開并找到如下的位置
src/hotspot/share/oops/markWord.hpp
- // 查看虛擬機是多少位的可以使用:java -version
- // 32 bits:
- // --------
- // hash:25 ------------>| age:4 unused_gap:1 lock:2 (normal object)
- //
- // 64 bits:
- // --------
- // unused:25 hash:31 -->| unused_gap:1 age:4 unused_gap:1 lock:2 (normal object)
阿巴阿巴: 當然可以引入openjdk提供的jol-core,然后進行打印即可。
- // 在pom中引入
- <dependency>
- <groupId>org.openjdk.jol</groupId>
- <artifactId>jol-core</artifactId>
- <version>0.10</version>
- </dependency>
然后編寫如下代碼
- public static void main(String[] args) {
- Test t = new Test();
- System.out.println(ClassLayout.parseInstance(t).toPrintable());
- }
打印如下
markword在哪?Klass pointer在哪兒?
1處是MarkWord占用8Byte也就是64bit
2處是Klass Pointer占用了4Byte也就是32bit
klass pointer看起來是被壓縮了,怎么確定是被壓縮了呢?可以通過如下命令
面試官: 對于JDK1.6及以上版本,synchronized和MarkWord有啥關系嘛?
阿巴阿巴: 那關系可大了,可以看到在MarkWord中有2bit用來表示鎖的標志位,代表著經過優化的synchronized鎖不會直接上重量級鎖,而是由偏向鎖轉為輕量鎖,再由輕量鎖轉為重量級鎖,一步一步膨脹的過程。
下面是2bit的鎖標志位代表的含義
- // [ptr | 00] locked ptr points to real header on stack
- // [header | 01] unlocked regular object header
- // [ptr | 10] monitor inflated lock (header is wapped out)
- // [ptr | 11] marked used to mark an object
- // [0 ............ 0| 00] inflating inflation in progress
- 001 無鎖狀態 (第一位代表偏向標志,為0的時候表示不偏向,為1的時候表示偏向)
- 101 偏向鎖 且記錄線程ID
- 00 輕量鎖 指向棧中鎖記錄的指針
- 10 重量級鎖 重量級鎖的指針
- 11 GC標志
然后再找到上圖Value部分的數據,這兩位是鎖的標志位
面試官: 你剛不是說有一位是鎖的偏向標志嗎?在哪兒呢?
阿巴阿巴: 鎖的偏向標志就在鎖標志的前一位
阿巴阿巴: 程序啟動后4s就會加偏向鎖,只不過這個偏向鎖沒有偏向任何線程ID,也屬于無鎖狀態
阿巴阿巴: 當應用處于單線程環境中時,這時候上的是偏向鎖,在對象頭中偏向標示顯示為1,案例如下
- public static void main(String[] args) {
- Test t = new Test();
- new Thread(()->{
- synchronized (t) {
- System.out.println(ClassLayout.parseInstance(t).toPrintable());
- }
- }).start();
- }
打印出來的數據如下
阿巴阿巴: 讓程序處于2個線程交替進行競爭鎖
- public static void main(String[] args) throws InterruptedException {
- Test t = new Test();
- Thread thread = new Thread(()->{
- synchronized (t) {
- System.out.println(ClassLayout.parseInstance(t).toPrintable());
- }
- });
- thread.start();
- // 等待thread運行完
- thread.join();
- synchronized (t) {
- System.out.println(ClassLayout.parseInstance(t).toPrintable());
- }
- }
可以看到當main線程拿鎖時已經膨脹為輕量鎖了,鎖的2bit標志為變成00了
阿巴阿巴: 輕量鎖的時候,虛擬機會在當前線程的棧幀中建立一個鎖記錄的空間“Lock Record”,用于存儲鎖對象目前的MarkWord的拷貝,這一步采用CAS,如果成功了,那么與此同時,2bit的鎖標記位會從“01”轉變為“00”。這就是加輕量鎖的過程。
阿巴阿巴: 之所以引入偏向鎖,是為了解決在無多線程競爭環境下的輕量鎖,輕量鎖CAS多次的嘗試也是對性能的損耗。相對于輕量鎖而言,偏向鎖值只需要進行一次CAS,這次CAS是用來設置線程ID的,設置成功后就代表獲取鎖了。輕量鎖更適合于線程交替執行的場景,它們通過CAS自旋,避免了線程直接掛起以及掛起后的恢復過程,以此來降低CPU的損耗。
阿巴阿巴: 最后讓我們看看加上重量鎖后的MarkWord表現吧,先上代碼
- public static void main(String[] args) throws InterruptedException {
- Test t = new Test();
- Thread thread = new Thread(()->{
- synchronized (t) {
- System.out.println(ClassLayout.parseInstance(t).toPrintable());
- }
- });
- thread.start();
- // 等待thread運行完
- // thread.join(); 去掉該代碼
- synchronized (t) {
- System.out.println(ClassLayout.parseInstance(t).toPrintable());
- }
- }
控制臺打印如下,發現已經加上重量鎖了,鎖的2bit標志為變成10了。
阿巴阿巴: 當輕量級鎖升級成重量級鎖時,Mark Word的鎖標記位更新為10,Mark Word 將指向互斥量(重量級鎖)。
阿巴阿巴: 以上就是關于synchronized和MarkWord的關系啦。
面試官: 理解的不錯,明天來上班吧~
阿巴阿巴: 好的~