一個Java對象到底占多大內存?

jopen 9年前發布 | 134K 次閱讀 Java對象 Java開發

原文出處: cnblogs-zhanjindong

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

在網上搜到了一篇博客講的非常好:http://yueyemaitian.iteye.com/blog/2033046,里面提供的這個類也非常實用:

import java.lang.instrument.Instrumentation;  
import java.lang.reflect.Array;  
import java.lang.reflect.Field;  
import java.lang.reflect.Modifier;  
import java.util.ArrayDeque;  
import java.util.Deque;  
import java.util.HashSet;  
import java.util.Set;  

/** 
 * 對象占用字節大小工具類 
 * 
 * @author tianmai.fh 
 * @date 2014-03-18 11:29 
 */  
public class SizeOfObject {  
    static Instrumentation inst;  

    public static void premain(String args, Instrumentation instP) {  
        inst = instP;  
    }  

    /** 
     * 直接計算當前對象占用空間大小,包括當前類及超類的基本類型實例字段大小、<br></br> 
     * 引用類型實例字段引用大小、實例基本類型數組總占用空間、實例引用類型數組引用本身占用空間大小;<br></br> 
     * 但是不包括超類繼承下來的和當前類聲明的實例引用字段的對象本身的大小、實例引用數組引用的對象本身的大小 <br></br> 
     * 
     * @param obj 
     * @return 
     */  
    public static long sizeOf(Object obj) {  
        return inst.getObjectSize(obj);  
    }  

    /** 
     * 遞歸計算當前對象占用空間總大小,包括當前類和超類的實例字段大小以及實例字段引用對象大小 
     * 
     * @param objP 
     * @return 
     * @throws IllegalAccessException 
     */  
    public static long fullSizeOf(Object objP) throws IllegalAccessException {  
        Set<Object> visited = new HashSet<Object>();  
        Deque<Object> toBeQueue = new ArrayDeque<Object>();  
        toBeQueue.add(objP);  
        long size = 0L;  
        while (toBeQueue.size() > 0) {  
            Object obj = toBeQueue.poll();  
            //sizeOf的時候已經計基本類型和引用的長度,包括數組  
            size += skipObject(visited, obj) ? 0L : sizeOf(obj);  
            Class<?> tmpObjClass = obj.getClass();  
            if (tmpObjClass.isArray()) {  
                //[I , [F 基本類型名字長度是2  
                if (tmpObjClass.getName().length() > 2) {  
                    for (int i = 0, len = Array.getLength(obj); i < len; i++) {  
                        Object tmp = Array.get(obj, i);  
                        if (tmp != null) {  
                            //非基本類型需要深度遍歷其對象  
                            toBeQueue.add(Array.get(obj, i));  
                        }  
                    }  
                }  
            } else {  
                while (tmpObjClass != null) {  
                    Field[] fields = tmpObjClass.getDeclaredFields();  
                    for (Field field : fields) {  
                        if (Modifier.isStatic(field.getModifiers())   //靜態不計  
                                || field.getType().isPrimitive()) {    //基本類型不重復計  
                            continue;  
                        }  

                        field.setAccessible(true);  
                        Object fieldValue = field.get(obj);  
                        if (fieldValue == null) {  
                            continue;  
                        }  
                        toBeQueue.add(fieldValue);  
                    }  
                    tmpObjClass = tmpObjClass.getSuperclass();  
                }  
            }  
        }  
        return size;  
    }  

    /** 
     * String.intern的對象不計;計算過的不計,也避免死循環 
     * 
     * @param visited 
     * @param obj 
     * @return 
     */  
    static boolean skipObject(Set<Object> visited, Object obj) {  
        if (obj instanceof String && obj == ((String) obj).intern()) {  
            return true;  
        }  
        return visited.contains(obj);  
    }  
}

大家可以用這個代碼邊看邊驗證,注意的是,運行這個程序需要通過javaagent注入Instrumentation,具體可以看原博客。我今天主要是總結下手動計算Java對象占用字節數的基本規則,做為基本的技能必須get√,希望能幫到和我一樣的Java菜鳥。

在介紹之前,簡單回顧下,Java對象的內存布局:對象頭(Header),實例數據(Instance Data)和對齊填充(Padding),詳細的可以看我的讀書筆記。另外:不同的環境結果可能有差異,我所在的環境是HotSpot虛擬機,64位Windwos。

下面進入正文:

對象頭

對象頭在32位系統上占用8bytes,64位系統上占用16bytes。

實例數據

原生類型(primitive type)的內存占用如下:

Primitive Type Memory Required(bytes)
boolean 1
byte 1
short 2
char 2
int 4
float 4
long 8
double 8

reference類型在32位系統上每個占用4bytes, 在64位系統上每個占用8bytes。

對齊填充

HotSpot的對齊方式為8字節對齊:

(對象頭 + 實例數據 + padding) % 8等于0且0 <= padding < 8

指針壓縮

對象占用的內存大小收到VM參數UseCompressedOops的影響。

1)對對象頭的影響

開啟(-XX:+UseCompressedOops)對象頭大小為12bytes(64位機器)。

static class A {
        int a;
    }

A對象占用內存情況:

關閉指針壓縮: 16+4=20不是8的倍數,所以+padding/4=24

開啟指針壓縮: 12+4=16已經是8的倍數了,不需要再padding。

2) 對reference類型的影響

64位機器上reference類型占用8個字節,開啟指針壓縮后占用4個字節。

static class B2 {
        int b2a;
        Integer b2b;
}

B2對象占用內存情況:

關閉指針壓縮: 16+4+8=28不是8的倍數,所以+padding/4=32

開啟指針壓縮: 12+4+4=20不是8的倍數,所以+padding/4=24

數組對象

64位機器上,數組對象的對象頭占用24個字節,啟用壓縮之后占用16個字節。之所以比普通對象占用內存多是因為需要額外的空間存儲數組的長度。

先考慮下new Integer[0]占用的內存大小,長度為0,即是對象頭的大小:

未開啟壓縮:24bytes

開啟壓縮后:16bytes

接著計算new Integer[1],new Integer[2],new Integer[3]和new Integer[4]就很容易了:

未開啟壓縮:

開啟壓縮:

拿new Integer[3]來具體解釋下:

未開啟壓縮:24(對象頭)+8*3=48,不需要padding;

開啟壓縮:16(對象頭)+3*4=28,+padding/4=32,其他依次類推。

自定義類的數組也是一樣的,比如:

static class B3 {
        int a;
        Integer b;
    }

new B3[3]占用的內存大小:

未開啟壓縮:48

開啟壓縮后:32

復合對象

計算復合對象占用內存的大小其實就是運用上面幾條規則,只是麻煩點。

1)對象本身的大小

直接計算當前對象占用空間大小,包括當前類及超類的基本類型實例字段大小、引用類型實例字段引用大小、實例基本類型數組總占用空間、實例引用類型數組引用本身占用空間大小; 但是不包括超類繼承下來的和當前類聲明的實例引用字段的對象本身的大小、實例引用數組引用的對象本身的大小。

static class B {
        int a;
        int b;
    }
static class C {
        int ba;
        B[] as = new B[3];

        C() {
            for (int i = 0; i < as.length; i++) {
                as[i] = new B();
            }
        }
    }

未開啟壓縮:16(對象頭)+4(ba)+8(as引用的大小)+padding/4=32

開啟壓縮:12+4+4+padding/4=24

2)當前對象占用的空間總大小

遞歸計算當前對象占用空間總大小,包括當前類和超類的實例字段大小以及實例字段引用對象大小。

遞歸計算復合對象占用的內存的時候需要注意的是:對齊填充是以每個對象為單位進行的,看下面這個圖就很容易明白。

現在我們來手動計算下C對象占用的全部內存是多少,主要是三部分構成:C對象本身的大小+數組對象的大小+B對象的大小。

未開啟壓縮:

(16 + 4 + 8+4(padding)) + (24+ 8*3) +(16+8)*3 = 152bytes

開啟壓縮:

(12 + 4 + 4 +4(padding)) + (16 + 4*3 +4(數組對象padding)) + (12+8+4(B對象padding))*3= 128bytes

大家有興趣的可以試試。

實際工作中真正需要手動計算對象大小的場景應該很少,但是個人覺得做為基礎知識每個Java開發人員都應該了解,另外:對自己寫的代碼大概占用多少內存,內存中是怎么布局的應該有一個直覺性的認識。

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!