你創建的對象真正占了多少內存?作為程序員基本每天都在new 對象,那么new出來的對象真正占用了多少內存呢?你new出來的對象會不會導致OOM呢?不知道大家關注過沒。
上周寫代碼的時候遇到如下一個邏輯:一個10000 size的list,再創建一個list,把數據都寫進來,新的list占多少內存?
這個東西分析起來還是挺麻煩的,讓我們一步步來。
首先我們要分析都先創建了那些對象?
List<String> list = Lists.newArrayListWithCapacity(10000);
我們通過上段代碼創建了新List,並執行了數據處理,可以看出來主要是創建了10000容量的ArrayList。我們都知道ArrayList中存儲的都是對象的引用,所以這部分操作增加的內存就是這個新的ArrayList需要分配的內存。
再看下ArrayList的源碼會發下主要就下面兩個私有字段,以及AbstractList中的modCount字段
/** * The array buffer into which the elements of the ArrayList are stored. * The capacity of the ArrayList is the length of this array buffer. Any * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA * will be expanded to DEFAULT_CAPACITY when the first element is added. */ transient Object[] elementData; // non-private to simplify nested class access /** * The size of the ArrayList (the number of elements it contains). * * @serial */ private int size;
protected transient int modCount = 0;
也就是說ArrayList需要分配實例數據就是size跟elementData大小,由於elementData是數組,所以對ArrayList來說也只需要分配其引用即可。最核心的內存占用就是Objecgt[] elementData這個數組了,在初始化ArrayList時候如果指定其容量會執行如下代碼,其實就直接初始化了一個相應大小的數組。
public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } }
接着我們來分析如何計算一個對象的內存占用大小。
在HotSpot虛擬機中,對象在內存中存儲的布局可以分為3塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。
對象頭
HotSpot虛擬機的對象頭包括三部分信息:
-
第一部分markword,用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程ID、偏向時間戳等,這部分數據的長度在32位和64位的虛擬機(未開啟壓縮指針)中分別為32bit和64bit,官方稱它為“MarkWord”。
-
對象頭的另外一部分是klass,類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例.
- 數組類型長度
_mark | _kclass | array length | |
32bit | 4 | 4 | 4 |
64bit | 8 | 8 | 4 |
64bit+comp | 8 | 4 | 4 |
實例數據
實例數據部分是對象真正存儲的有效信息,也是在程序代碼中所定義的各種類型的字段內容。無論是從父類繼承下來的,還是在子類中定義的,都需要記錄起來。
jvm會根據如下順序進行字段排序,為了是節省padding次數
- doubles (8) and longs (8)
- ints (4) and floats (4)
- shorts (2) and chars (2)
- booleans (1) and bytes (1)
- references (4/8(64bit未開啟壓縮))
- repeat for sub-class fields
可以看出來相同寬度的總是被分配在一起,在滿足這個前提條件下,父類定義的變量會出現在子類之前,但如果開啟了CompactFields參數,子類中較窄的變量也可能會查到父類變量空隙中。
對齊填充
第三部分對齊填充並不是必然存在的,也沒有特別的含義,它僅僅起着占位符的作用。由於HotSpot VM的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說,就是對象的大小必須是8字節的整數倍。當對象實例數據部分沒有對齊時,就需要通過對齊填充來補全。
了解了這些信息以后我們就可以進行上面對象的計算了,我們是在64bit下並開啟了指針壓縮情況下進行計算。
首先計算10000長度的數組大小
head | data | padding | total |
8+4+4 | 4*10000 | 0 | 40016 |
再計算下ArrayList對象的大小
head | data | padding | total |
8+4+0 | 4+4+4 | 0 | 24 |
那總的內存占用也就是24+40016=40040
那么有沒有簡單的工具,我輸入給他一個對象他就幫我計算出內存大小呢?有的,主要有兩種方式一種是使用Instrumentation,但是用起來還挺麻煩,有興趣的同學可以自己研究下,這里主要介紹下jol的使用,jol是openjdk的一個工具包可以用來方便的計算對象或者類的內存占用大小,也提供maven GAV,可以自行去搜索引用。
那么下面使用下jol來驗證下我們的計算結果。
執行如下代碼:
List<String> list = Lists.newArrayListWithCapacity(10000); System.out.println(ClassLayout.parseInstance(list).toPrintable()); Field field = ArrayList.class.getDeclaredField("elementData"); field.setAccessible(true); Object array = field.get(list); System.out.println(ClassLayout.parseInstance(array).toPrintable());
輸出結果如下:
java.util.ArrayList object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 22 2f 00 f8 (00100010 00101111 00000000 11111000) (-134205662) 12 4 int AbstractList.modCount 0 16 4 int ArrayList.size 0 20 4 java.lang.Object[] ArrayList.elementData [null, null, .....] Instance size: 24 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total [Ljava.lang.Object; object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) f5 22 00 f8 (11110101 00100010 00000000 11111000) (-134208779) 12 4 (object header) 10 27 00 00 (00010000 00100111 00000000 00000000) (10000) 16 40000 java.lang.Object Object;.<elements> N/A Instance size: 40016 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
跟我們之前的計算的結果一致,如果不開啟指針壓縮,-XX:-UseCompressedOops那么結果如下:
java.util.ArrayList object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 50 a5 8e 21 (01010000 10100101 10001110 00100001) (562996560) 12 4 (object header) 02 00 00 00 (00000010 00000000 00000000 00000000) (2) 16 4 int AbstractList.modCount 0 20 4 (alignment/padding gap) 24 4 int ArrayList.size 0 28 4 (alignment/padding gap) 32 8 java.lang.Object[] ArrayList.elementData [null, null, ......] Instance size: 40 bytes Space losses: 8 bytes internal + 0 bytes external = 8 bytes total [Ljava.lang.Object; object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) f0 d1 8b 21 (11110000 11010001 10001011 00100001) (562811376) 12 4 (object header) 02 00 00 00 (00000010 00000000 00000000 00000000) (2) 16 4 (object header) 10 27 00 00 (00010000 00100111 00000000 00000000) (10000) 20 4 (alignment/padding gap) 24 80000 java.lang.Object Object;.<elements> N/A Instance size: 80024 bytes Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
那么你學會了如何計算一個對象的占用內存大小了嗎?