你寫的Java對象究竟佔多少內存?

memory

概述

Java 做爲一個面嚮對象語言,給咱們帶來了多態,繼承,封裝等特性,使得咱們能夠利用這些特性很輕鬆的就能構建出易於擴展,易於維護的代碼。做爲一個Javaer,每天搞「對象」,那你寫的對象究竟佔用了多少內存呢?咱們來看看你的「對象」是如何「敗家」的。html

本文環境:jdk1.8_64java

Java 對象頭內存模型

想要了解Java對象究竟佔用多少內存一定先要了解一個Java 對象的內存模型是怎麼樣的?因爲咱們的虛擬機是分爲32位和64位,那確定它們的模型也是有區別的,下面我列出列32位虛擬機和64位虛擬機下的Java對象頭內存模型。git

32位虛擬機
32位虛擬機

64位虛擬機
64位虛擬機

64位帶指針壓縮
64位帶指針壓縮

由於筆者的本地環境是jdk1.8,64位虛擬機,這裏我以64位虛擬機(開啓指針壓縮)來分析,由於默認狀況下,jdk1.8 在64位虛擬機默認開啓指針壓縮。程序員

Java 對象頭主要包括兩部分,第一部分就是 Mark Word,這也是 Java 鎖實現原理中重要的一環,另一部分是 Klass Wordgithub

Klass Word 這裏實際上是虛擬機設計的一個oop-klass model模型,這裏的OOP是指Ordinary Object Pointer(普通對象指針),看起來像個指針其實是藏在指針裏的對象。而 klass 則包含 元數據和方法信息,用來描述 Java 類。它在64位虛擬機開啓壓縮指針的環境下佔用 32bits 空間。bash

Mark Word 是咱們分析的重點,這裏也會設計到鎖的相關知識。Mark Word 在64位虛擬機環境下佔用 64bits 空間。整個Mark Word的分配有幾種狀況:maven

  1. 未鎖定(Normal): 哈希碼(identity_hashcode)佔用31bits,分代年齡(age)佔用4 bits,偏向模式(biased_lock)佔用1 bits,鎖標記(lock)佔用2 bits,剩餘26bits 未使用(也就是全爲0)
  2. 可偏向(Biased): 線程id 佔54bits,epoch 佔2 bits,分代年齡(age)佔用4 bits,偏向模式(biased_lock)佔用1 bits,鎖標記(lock)佔用2 bits,剩餘 1bit 未使用。
  3. 輕量鎖定(Lightweight Locked): 鎖指針佔用62bits,鎖標記(lock)佔用2 bits。
  4. 重量級鎖定(Heavyweight Locked):鎖指針佔用62bits,鎖標記(lock)佔用2 bits。
  5. GC 標記:標記位佔2bits,其他爲空(也就是填充0)

以上就是咱們對Java對象頭內存模型的解析,只要是Java對象,那麼就確定會包括對象頭,也就是說這部份內存佔用是避免不了的。因此,在筆者64位虛擬機,Jdk1.8(開啓了指針壓縮)的環境下,任何一個對象,啥也不作,只要聲明一個類,那麼它的內存佔用就至少是96bits,也就是至少12字節。ide

驗證模型

咱們來寫點代碼來驗證一下上述的內存模型,這裏推薦openjdk的jol工具,它能夠幫助你查看對象內存的佔用狀況。工具

首先添加maven依賴oop

<dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.10</version>
        </dependency>
複製代碼

咱們先來看看,若是隻是新建一個普通的類,什麼屬性也不添加,佔用的空間是多少?

/**
 * @description:
 * @author: luozhou
 * @create: 2020-02-26 10:00
 **/
public class NullObject {

}
複製代碼

按照咱們以前的Java對象內存模型分析,一個空對象,那就是隻有一個對象頭部,在指針壓縮的條件下會佔用 96 bits,也就是12bytes。

運行工具查看空間佔用

public static void main(String[] args) {
        System.out.println(ClassLayout.parseInstance(new NullObject()).toPrintable());
    }
複製代碼

上面這行代碼會解析你新建一個NullObject對象,佔用了多少內存。咱們執行看看結果如何:

內存佔用
內存佔用

這裏咱們發現結果顯示:Instance size:16 bytes,結果就是16字節,與咱們以前預測的12字節不同,爲何會這樣呢?咱們看到上圖中有3行 object header,每一個佔用4字節,因此頭部就是12字節,這裏和咱們的計算是一致的,最後一行是虛擬機填充的4字節,那爲何虛擬機要填充4個字節呢?

什麼是內存對齊

想要知道爲何虛擬機要填充4個字節,咱們須要瞭解什麼是內存對齊?

咱們程序員看內存是這樣的:

上圖表示一個坑一個蘿蔔的內存讀取方式。但實際上 CPU 並不會以一個一個字節去讀取和寫入內存。相反 CPU 讀取內存是一塊一塊讀取的,塊的大小能夠爲 二、四、六、八、16 字節等大小。塊大小咱們稱其爲內存訪問粒度。以下圖:

假設一個32位平臺的 CPU,那它就會以4字節爲粒度去讀取內存塊。那爲何須要內存對齊呢?主要有兩個緣由:

  • 平臺(移植性)緣由:不是全部的硬件平臺都可以訪問任意地址上的任意數據。例如:特定的硬件平臺只容許在特定地址獲取特定類型的數據,不然會致使異常狀況。
  • 性能緣由:若訪問未對齊的內存,將會致使 CPU 進行兩次內存訪問,而且要花費額外的時鐘週期來處理對齊及運算。而自己就對齊的內存僅須要一次訪問就能夠完成讀取動做。

我用圖例來講明 CPU 訪問非內存對齊的過程:

在上圖中,假設CPU 是一次讀取4字節,在這個連續的8字節的內存空間中,若是個人數據沒有對齊,存儲的內存塊在地址1,2,3,4中,那CPU的讀取就會須要進行兩次讀取,另外還有額外的計算操做:

  1. CPU 首次讀取未對齊地址的第一個內存塊,讀取 0-3 字節。並移除不須要的字節 0。
  2. CPU 再次讀取未對齊地址的第二個內存塊,讀取 4-7 字節。並移除不須要的字節 五、六、7 字節。
  3. 合併 1-4 字節的數據。
  4. 合併後放入寄存器。

因此,沒有進行內存對齊就會致使CPU進行額外的讀取操做,而且須要額外的計算。若是作了內存對齊,CPU能夠直接從地址0開始讀取,一次就讀取到想要的數據,不須要進行額外讀取操做和運算操做,節省了運行時間。咱們用了空間換時間,這就是爲何咱們須要內存對齊。

回到Java空對象填充了4個字節的問題,由於原字節頭是12字節,64位機器下,內存對齊的話就是128位,也就是16字節,因此咱們還須要填充4個字節。

非空對象佔用內存計算

咱們知道了一個空對象是佔用16字節,那麼一個非空對象究竟佔用多少字節呢?咱們仍是寫一個普通類來驗證下:

public class TestNotNull {
    private NullObject nullObject=new NullObject();
    private int a;
}
複製代碼

這個演示類中引入了別的對象,咱們知道int類型是佔用4個字節,NullObject對象佔用16字節,對象頭佔12字節,還有一個很重要的狀況 NullObject在當前這個類中是一個引用,因此不會存真正的對象,而只存引用地址,引用地址佔4字節,因此總共就是12+4+4=20字節,內存對齊後就是24字節。咱們來驗證下是否是這個結果:

public static void main(String[] args) {
        //打印對象內存佔用
        System.out.println(ClassLayout.parseInstance(new TestNotNull()).toPrintable());
        System.out.println("=========================");
        //輸出對象相關全部內存佔用
        System.out.println(GraphLayout.parseInstance(new TestNotNull()).toPrintable());
        System.out.println("=========================");
        //輸出內存佔用統計
        System.out.println(GraphLayout.parseInstance(new TestNotNull()).toFootprint());
    }
複製代碼

結果以下:

咱們能夠看到TestNotNull的類佔用空間是24字節,其中頭部佔用12字節,變量aint類型,佔用4字節,變量nullObject是引用,佔用了4字節,最後填充了4個字節,總共是24個字節,與咱們以前的預測一致。可是,由於咱們實例化了NullObject,這個對象一會存在於內存中,因此咱們還須要加上這個對象的內存佔用16字節,那總共就是24bytes+16bytes=40bytes。咱們圖中最後的統計打印結果也是40字節,因此咱們的分析正確。

這也是如何分析一個對象真正的佔用多少內存的思路,根據這個思路加上openJDK的jol工具就能夠基本的掌握本身寫的「對象」究竟敗家了你多少內存。

總結

本文我主要講述瞭如何分析一個Java對象究竟佔用多少內存空間,主要總結點以下:

  1. Java對象頭部內存模型在32位虛擬機和64位虛擬機是不同的,64位虛擬機又分爲開啓指針壓縮和不開啓指針壓縮兩種對象頭模型,因此總共有3種對象頭模型。
  2. 內存對齊主要是由於平臺的緣由和性能的緣由,本文主要解析的是性能方面的緣由。
  3. 空對象的內存佔用計算注意要計算內存對齊,非空對象的內存計算注意加上引用內存佔用和原實例對象的空間佔用。

參考

1.cr.openjdk.java.net/~lfoltan/bu…

2.gist.github.com/arturmkrtch…

3.weekly-geekly.github.io/articles/44…

4.developer.ibm.com/articles/pa…

相關文章
相關標籤/搜索