1. 程式人生 > >java對象詳解

java對象詳解

add splay view created 元素 繼承關系 外部 優化 csdn

  • java對象詳解
    • 內存布局
      • 普通對象布局
      • 數組的內存布局
      • 內部類的內存布局
    • 對象分解
      • 對象頭-mark word(8字節)
      • 實例數據
      • 對齊填充(可選)
    • java鎖分析

java對象詳解

HotSpot虛擬機中,對象在內存中存儲的布局可以分為對象頭,實例數據,對齊填充三個區域。本文所說環境均為HotSpot虛擬機。即輸入java -version返回的虛擬機版本:

java version "1.8.0_111"
Java(TM) SE Runtime Environment (build 1.8.0_111-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.111-b14, mixed mode)

內存布局

普通對象布局

  • 在jvm中,任何對象都是8個字節為粒度進行對齊的,這是對象內存布局的第一個規則。

如果調用new Object(),由於Object類並沒有其他沒有其他可存儲的成員,那麽僅僅使用堆中的8個字節來保存兩個字的頭部即可。
除了上面所說的8個字節的頭部(關於對象頭,在下面會有詳細解釋),類屬性緊隨其後。屬性通常根據其大小來排列。例如,整型(int)以4個字節為單位對齊,長整型(long)以8個字節為單位對齊。這裏是出於性能考慮而這麽設計的:通常情況下,如果數據以4字節為單位對齊,那麽從內存中讀4字節的數據並寫入到處理器的4字節寄存器是性價比更高的。

為了節省內存,Sun VM並沒有按照屬性聲明時的順序來進行內存布局。實際上,屬性在內存中按照下面的順序來組織:

    1. 雙精度型(doubles)和長整型(longs)

    1. 整型(ints)和浮點型(floats)

    1. 短整型(shorts)和字符型(chars)

    1. 布爾型(booleans)和字節型(bytes)

    1. 引用類型(references)

內存使用率會通過這個機制得到優化。例如,如下聲明一個類:

class MyClass {
       byte a;
       int c;
       boolean d;
       long e;
       Object f;          
}

如果JVM並沒有打亂屬性的聲明順序,其對象內存布局將會是下面這個樣子:

[HEADER:  8 bytes]  8
[a:       1 byte ]  9
[padding: 3 bytes] 12
[c:       4 bytes] 16
[d:       1 byte ] 17
[padding: 7 bytes] 24
[e:       8 bytes] 32
[f:       4 bytes] 36
[padding: 4 bytes] 40

此時,用於占位的14個字節是浪費的,這個對象一共使用了40個字節的內存空間。但是,如果用上面的規則對這些對象重新排序,其內存結果會變成下面這個樣子:

[HEADER:  8 bytes]  8
[e:       8 bytes] 16
[c:       4 bytes] 20
[a:       1 byte ] 21
[d:       1 byte ] 22
[padding: 2 bytes] 24
[f:       4 bytes] 28
[padding: 4 bytes] 32

這次,用於占位的只有6個字節,這個對象使用了32個字節的內存空間。

  • 規則2:類屬性按照如下優先級進行排列:長整型和雙精度類型;整型和浮點型;字符和短整型;字節類型和布爾類型,最後是引用類型。這些屬性都按照各自的單位對齊。
    現在我們知道如何計算一個繼承了Object的類的實例的內存大小了。下面這個例子用來做下練習: java.lang.Boolean。這是其內存布局:

[HEADER:  8 bytes]  8
[value:   1 byte ]  9
[padding: 7 bytes] 16

Boolean類的實例占用16個字節的內存!驚訝吧?(別忘了最後用來占位的7個字節)。

  • 規則3:不同類繼承關系中的成員不能混合排列。首先按照規則2處理父類中的成員,接著才是子類的成員。
    舉例如下:

class A {
   long a;
   int b;
   int c;
}
 
class B extends A {
   long d;
}

類B的實例在內存中的存儲如下:

[HEADER:  8 bytes]  8
[a:       8 bytes] 16
[b:       4 bytes] 20
[c:       4 bytes] 24
[d:       8 bytes] 32

如果父類中的成員的大小無法滿足4個字節這個基本單位,那麽下一條規則就會起作用:

  • 規則4:當父類中最後一個成員和子類第一個成員的間隔如果不夠4個字節的話,就必須擴展到4個字節的基本單位。

class A {
   byte a;
}
 
class B {
   byte b;
}

[HEADER:  8 bytes]  8
[a:       1 byte ]  9
[padding: 3 bytes] 12
[b:       1 byte ] 13
[padding: 3 bytes] 16

註意到成員a被擴充了3個字節以保證和成員b之間的間隔是4個字節。這個空間不能被類B使用,因此被浪費了。

  • 規則5:如果子類第一個成員是一個雙精度或者長整型,並且父類並沒有用完8個字節,JVM會破壞規則2,按照整形(int),短整型(short),字節型(byte),引用類型(reference)的順序,向未填滿的空間填充。

class A {
  byte a;
}
 
class B {
  long b;
  short c;  
  byte d;
}

[HEADER:  8 bytes]  8
[a:       1 byte ]  9
[padding: 3 bytes] 12
[c:       2 bytes] 14
[d:       1 byte ] 15
[padding: 1 byte ] 16
[b:       8 bytes] 24

在第12字節處,類A“結束”的地方,JVM沒有遵守規則2,而是在長整型之前插入一個短整型和一個字節型成員,這樣可以避免浪費3個字節。

數組的內存布局

數組有一個額外的頭部成員,用來存放“長度”變量。數組元素以及數組本身,跟其他常規對象同樣,都需要遵守8個字節的邊界規則。
下面是一個有3個元素的字節數組的內存布局:

[HEADER:  12 bytes] 12
[[0]:      1 byte ] 13
[[1]:      1 byte ] 14
[[2]:      1 byte ] 15
[padding:  1 byte ] 16

下面是一個有3個元素的長整型數字的內存布局:

[HEADER:  12 bytes] 12
[padding:  4 bytes] 16
[[0]:      8 bytes] 24
[[1]:      8 bytes] 32
[[2]:      8 bytes] 40

內部類的內存布局

非靜態內部類(Non-static inner classes)有一個額外的“隱藏”成員,這個成員是一個指向外部類的引用變量。這個成員是一個普通引用,因此遵守引用內存布局的規則。內部類因此有4個字節的額外開銷。

以上引用:http://www.importnew.com/1305.html

對象分解

對象頭-mark word(8字節)

對象頭主要包含兩部分信息,第一部分用於存儲對象自身運行時數據,如哈希碼,GC分代年齡(可以查看上一篇關於java內存回收分析的文章),鎖狀態標誌,線程持有鎖,偏向線程ID,偏向時間戳等。
如果對象是數組類型,則虛擬機用3個字寬存儲對象頭,如果是非數組類型,則用2個字寬存儲對象頭。下圖是一個32位虛擬機mark部分占用內存分布情況

技術分享
j1.jpeg

此圖來源:http://blog.csdn.net/zhoufanyang_china/article/details/54601311

另一部分是klass類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例,具體結構參考下圖。

技術分享
javao.png

在32位系統下,存放Class指針的空間大小是4字節,MarkWord是4字節,對象頭為8字節。

在64位系統下,存放Class指針的空間大小是8字節,MarkWord是8字節,對象頭為16字節。

64位開啟指針壓縮的情況下,存放Class指針的空間大小是4字節,MarkWord是8字節,對象頭為12字節。

數組長度4字節+數組對象頭8字節(對象引用4字節(未開啟指針壓縮的64位為8字節)+數組markword為4字節(64位未開啟指針壓縮的為8字節))+對齊4=16字節。

靜態屬性不算在對象大小內。

實例數據

對象實際數據,大下為實際數據的大小

對齊填充(可選)

按8字節對齊,參照上面內存布局部分

java鎖分析

synchronized到底鎖的是對象還是代碼片段?
例:

package com.startclan.thread;

/**
 * Created by wongloong on 17-5-20.
 */
public class TestSync {
    public synchronized void test() {

        System.out.println("test1 start");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("test1 end");
    }

    public synchronized void test2() {
        System.out.println("test2 start");
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("test2 end");
    }

}


package com.startclan.thread;

/**
 * Created by wongloong on 17-5-20.
 */
public class TestSyncStatic {
    public static synchronized void test() {
        System.out.println("test1 start");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("test1 end");
    }

    public static synchronized void test2() {
        System.out.println("test2 start");
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("test2 end");
    }

}


package com.startclan;

import com.startclan.thread.TestSync;
import com.startclan.thread.TestSyncStatic;
import org.junit.Test;

/**
 * Created by wongloong on 17-5-18.
 */
public class TestWithThread {

    @Test
    public void testThread1() throws Exception {
        final TestSync t1 = new TestSync();
        /**
         * 測試synchronized同步非static代碼塊
         * 此處會先執行test方法然後執行test2方法,說明synchronized在同步非static方法時,
         * 只能同步同一對象的同一實例進行同步
         */
        new Thread(new Runnable() {
            @Override
            public void run() {
                t1.test();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                t1.test2();
            }
        }).start();
        Thread.sleep(4000);
    }

    @Test
    public void testThread2() throws Exception {
        final TestSync t1 = new TestSync();
        final TestSync t2 = new TestSync();

        /**
         * 測試synchronized同步非static代碼塊
         * t1 t2不同對象,
         * 不能同步方法
         */
        new Thread(new Runnable() {
            @Override
            public void run() {
                t1.test();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                t2.test2();
            }
        }).start();

        Thread.sleep(4000);
    }

    @Test
    public void testThread3() throws Exception {
        final TestSyncStatic tss1 = new TestSyncStatic();
        final TestSyncStatic tss2 = new TestSyncStatic();

        /**
         * 測試synchronized 同步 static代碼塊
         * 由於method1和method2都屬於靜態同步方法,
         * 所以調用的時候需要獲取同一個類上monitor(每個類只對應一個class對象),
         * 所以也只能順序的執行。
         */
        new Thread(new Runnable() {
            @Override
            public void run() {
                tss1.test();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                tss2.test2();
            }
        }).start();
        Thread.sleep(4000);
    }
}

此時輸出結果為:

------------------------1-------------------------
test1 start
test1 end
test2 start
test2 end
-----------------------2-------------------------
test1 start
test2 start
test2 end
test1 end
-----------------------3-------------------------
test1 start
test1 end
test2 start
test2 end

結論:

  • synchronized(this)以及非static的synchronized方法,只能防止多個線程同時執行同一個實例的同步代碼段(在第一段測試代碼中,分別new了三個Mythread類,所以並不會執行同步)

  • synchronized(xx.class)及static的synchronized方法,可以防止多個線程同時執行同一個對象的多個實例同步的代碼段

  • synchronize原理
    每一個對象頭信息都包含一個鎖定狀態,可以看上面的mark word的圖解。當線程進入對象中,嘗試獲取鎖的所有權,如果為鎖的值為0,則該線程進入,並設置為1,該線程為鎖的擁有者。如果線程已經占用該鎖,只是重新進入,並且鎖值+1.當線程退出時則-1.如果其他線程訪問這個對象實例,則改線程堵塞。直到鎖值為0的時候,在重新嘗試取得鎖的所有權。

java對象詳解