java一個對象佔用多少字節?

最近在讀《深入理解Java虛擬機》,對Java對象的內存佈局有了進一步的認識,於是腦子裏自然而然就有一個很普通的問題,就是一個Java對象到底佔用多大內存?

想弄清楚上面的問題,先補充一下基礎知識。

1、JAVA 對象佈局
在 HotSpot虛擬機中,對象在內存中的存儲的佈局可以分爲三塊區域:對象頭(Header),實例數據(Instance Data)和對齊填充(Padding)

1.1對象頭(Header):
Java中對象頭由 Markword + 類指針kclass(該指針指向該類型在方法區的元類型) 組成。
普通對象頭在32位系統上佔用8bytes,64位系統上佔用16bytes。64位機器上,數組對象的對象頭佔用24個字節,啓用壓縮之後佔用16個字節。

用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等。在64位的虛擬機(未開啓壓縮指針)爲64bit。
Markword:

類指針kclass:
kclass存儲的是該對象所屬的類在方法區的地址,所以是一個指針,默認Jvm對指針進行了壓縮,用4個字節存儲,如果不壓縮就是8個字節。 關於Compressed Oops的知識,大家可以自行查閱相關資料來加深理解。

如果對象是一個Java數組,那在對象頭中還必須有一塊用於記錄數組長度的數據,這塊佔用4個字節。因爲虛擬機可以通過普通Java對象的元數據信息確定Java對象的大小,但是從數組的元數據中無法確定數組的大小。
(並不是所有的虛擬機實現都必須在對象數據上保留類型指針,換句話說,查找對象的元數據並不一定要經過對象本身,可參考對象的訪問定位)

1.2實例數據(Instance Data)
實例數據部分就是成員變量的值,其中包含父類的成員變量和本類的成員變量。也就是說,除去靜態變量和常量值放在方法區,非靜態變量的值是隨着對象存儲在堆中的。
因爲修改靜態變量會反映到方法區中class的數據結構中,故而推測對象保存的是靜態變量和常量的引用。

1.3對齊填充(Padding)
用於確保對象的總長度爲8字節的整數倍。
HotSpot要求對象的總長度必須是8字節的整數倍。由於對象頭一定是8字節的整數倍,但實例數據部分的長度是任意的。因此需要對齊補充字段確保整個對象的總長度爲8的整數倍。

2、Java數據類型有哪些
基礎數據類型(primitive type)
引用類型 (reference type)
2.1基礎數據類型內存佔用如下
2.2引用類型 內存佔用如下:
引用類型跟基礎數據類型不一樣,除了對象本身之外,還存在一個指向它的引用(指針),指針佔用的內存在64位虛擬機上8個字節,如果開啓指針壓縮是4個字節,默認是開啓了的。

2.3字段重排序
爲了更高效的使用內存,實例數據字段將會重排序。排序的優先級爲: long = double > int = float > char = short > byte > boolean > object reference
如下所示的類

class FieldTest{
        byte a;
        int c;
        boolean d;
        long e;
        Object f;
    }
將會重排序爲(開啓CompressedOops選項):

   OFFSET  SIZE               TYPE DESCRIPTION            
         16     8               long FieldTest.e            
         24     4                int FieldTest.c            
         28     1               byte FieldTest.a            
         29     1            boolean FieldTest.d            
         30     2              (alignment/padding gap)
         32     8   java.lang.Object FieldTest.f
3、驗證
講完了上面的概念,我們可以去驗證一下。
3.1有一個Fruit類繼承了Object類,我們分別新建一個object和fruit,那他們分別佔用多大的內存呢?

class Fruit extends Object {
     private int size;
}

Object object = new Object();
Fruit f = new Fruit();
先來看object對象,通過上面的知識,它的Markword是8個字節,kclass是4個字節, 加起來是12個字節,加上4個字節的對齊填充,所以它佔用的空間是16個字節。
再來看fruit對象,同樣的,它的Markword是8個字節,kclass是4個字節,但是它還有個size成員變量,int類型佔4個字節,加起來剛好是16個字節,所以不需要對齊填充。

那該如何驗證我們的結論呢?畢竟我們還是相信眼見爲實!很幸運Jdk提供了一個工具jol-core可以讓我們來分析對象頭佔用內存信息。具體使用參考
jol的使用也很簡單:
打印頭信息

    public static void main(String[] args) {
        System.out.print(ClassLayout.parseClass(Fruit.class).toPrintable());
        System.out.print(ClassLayout.parseClass(Object.class).toPrintable());
    }
輸出結果

com.zzx.algorithm.tst.Fruit object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0    12        (object header)                           N/A
     12     4    int Fruit.size                                N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0    12        (object header)                           N/A
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
可以看到輸出結果都是16 bytes,跟我們前面的分析結果一致。

3.2 除了類類型和接口類型的對象,Java中還有數組類型的對象,數組類型的對象除了上面表述的字段外,還有4個字節存儲數組的長度(所以數組的最大長度是Integer.MAX)。所以一個數組對象佔用的內存是 8 + 4 + 4 = 16個字節,當然這裏不包括數組內成員的內存。
我們也運行驗證一下。

    public static void main(String[] args) {
        String[] strArray = new String[0];
        System.out.println(ClassLayout.parseClass(strArray.getClass()).toPrintable());
    }
輸出結果:

[Ljava.lang.String; object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0    16                    (object header)                           N/A
     16     0   java.lang.String String;.<elements>                        N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
輸出結果object header的長度也是16,跟我們分析的一致。
3.3 接下來看對象的實例數據部分:
爲了方便說明,我們新建一個Apple類繼承上面的Fruit類

public class Apple extends Fruit {
    private int size;
    private String name;
    private Apple brother;
    private long create_time;
    
}
// 打印Apple的對象分佈信息

System.out.println(ClassLayout.parseClass(Apple.class).toPrintable());
1
// 輸出結果

com.zzx.algorithm.tst.Apple object internals:
 OFFSET  SIZE                          TYPE DESCRIPTION                               VALUE
      0    12                               (object header)                           N/A
     12     4                           int Fruit.size                                N/A
     16     8                          long Apple.create_time                         N/A
     24     4                           int Apple.size                                N/A
     28     4              java.lang.String Apple.name                                N/A
     32     4   com.zzx.algorithm.tst.Apple Apple.brother                             N/A
     36     4                               (loss due to the next object alignment)
Instance size: 40 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
可以看到Apple的對象頭12個字節,然後分別是從Fruit類繼承來的size屬性(雖然Fruit的size是private的,還是會被繼承,與Apple自身的size共存),還有自己定義的4個屬性,基礎數據類型直接分配,對象類型都是存的指針佔4個字節(默認都是開啓了指針壓縮),最終是40個字節,所以我們new一個Apple對象,直接就會佔用堆棧中40個字節的內存,清楚對象的內存分配,讓我們在寫代碼時心中有數,應當時刻有內存優化的意識!
這裏又引出了一個小知識點,上面其實已經標註出來了。

父類的私有成員變量是否會被子類繼承?
答案當然是肯定的,我們上面分析的Apple類,父類Fruit有一個private類型的size成員變量,Apple自身也有一個size成員變量,它們能夠共存。注意劃重點了,類的成員變量的私有訪問控制符private,只是編譯器層面的限制,在實際內存中不論是私有的,還是公開的,都按規則存放在一起,對虛擬機來說並沒有什麼分別!

4、方法內部new的對象是在堆上還是棧上?
我們常規的認識是對象的分配是在堆上,棧上會有個引用指向該對象(即存儲它的地址),到底是不是呢,我們來做個試驗!
我們在循環內創建一億個Apple對象,並記錄循環的執行時間,前面已經算過1個Apple對象佔用40個字節,總共需要4GB的空間。

public static void main(String[] args) {
     long startTime = System.currentTimeMillis();
     for (int i = 0; i < 100000000; i++) {
         newApple();
     }
     System.out.println("take time:" + (System.currentTimeMillis() - startTime) + "ms");
}

public static void newApple() {
     new Apple();
}
我們給JVM添加上-XX:+PrintGC運行配置,讓編譯器執行過程中輸出GC的log日誌
// 運行結果,沒有輸出任何gc的日誌

take time:6ms
1
1億個對象,6ms就分配完成,而且沒有任何GC,顯然如果對象在堆上分配的話是不可能的,其實上面的實例代碼,Apple對象全部都是在棧上分配的,這裏要提出一個概念指針逃逸,newApple方法中新建的對象Apple並沒有在外部被使用,所以它被優化爲在棧上分配,我們知道方法執行完成後該棧幀就會被清空,所以也就不會有GC。
我們可以設置虛擬機的運行參數來測試一下。
// 虛擬機關閉指針逃逸分析

-XX:-DoEscapeAnalysis
1
// 虛擬機關閉標量替換

-XX:-EliminateAllocations
1
在VM options裏面添加上面二個參數,再運行一次

[GC (Allocation Failure)  236984K->440K(459776K), 0.0003751 secs]
[GC (Allocation Failure)  284600K->440K(516608K), 0.0004272 secs]
[GC (Allocation Failure)  341432K->440K(585216K), 0.0004835 secs]
[GC (Allocation Failure)  410040K->440K(667136K), 0.0004655 secs]
[GC (Allocation Failure)  491960K->440K(645632K), 0.0003837 secs]
[GC (Allocation Failure)  470456K->440K(625152K), 0.0003598 secs]

take time:5347ms
可以看到有很多GC的日誌,而且運行的時間也比之前長了很多,因爲這時候Apple對象的分配在堆上,而堆是所有線程共享的,所以分配的時候肯定有同步機制,而且觸發了大量的gc,所以效率低很多。
總結一下: 虛擬機指針逃逸分析是默認開啓的,對象不會逃逸的時候優先在棧上分配,否則在堆上分配。
到這裏,關於“一個對象佔多少內存?”這個問題,已經能回答的相當全面了。
————————————————
版權聲明:本文爲CSDN博主「小左01」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/zzx410527/article/details/93646925

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章