java對象結構以及對象大小計算示例(通俗易懂)

概念

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

具體如下圖

java 普通對象結構
在這裏插入圖片描述

java 數組對象結構
在這裏插入圖片描述

對象結構組成

對象頭

HotSpot虛擬機的對象頭包括兩部分信息:

  1. Mark Word
    第一部分Mark Word,用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等,這部分數據的長度在32位和64位的虛擬機(未開啓壓縮指針)中分別爲32bit和64bit
  2. 類型指針
    對象頭的另外一部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例.
  3. 數組長度(只有數組對象有)
    如果對象是一個數組, 那在對象頭中還必須有一塊數據用於記錄數組長度.

實例數據

​ 實例數據部分是對象真正存儲的有效信息,也是在程序代碼中所定義的各種類型的字段內容。無論是從父類繼承下來的,還是在子類中定義的,都需要記錄起來。

對象引用(reference)類型在64位機器上,關閉指針壓縮時佔用8bytes, 開啓時佔用4bytes。

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

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

包裝類型

包裝類(Boolean/Byte/Short/Character/Integer/Long/Double/Float)佔用內存的大小等於對象頭大小加上底層基礎數據類型的大小。

包裝類型的對象內存佔用情況如下:

Numberic Wrappers +useCompressedOops -useCompressedOops
Byte, Boolean 16 bytes 24 bytes
Short, Character 16 bytes 24 bytes
Integer, Float 16 bytes 24 bytes
Long, Double 24 bytes 24 bytes

對齊填充

​ 第三部分對齊填充並不是必然存在的,也沒有特別的含義,它僅僅起着佔位符的作用。由於HotSpot VM的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說,就是對象的大小必須是8字節的整數倍。而對象頭部分正好是8字節的倍數(1倍或者2倍),因此,當對象實例數據部分沒有對齊時,就需要通過對齊填充來補全。

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

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

jvm相關參數

上面用到的useCompressedOops這個參數,我們可以看看在命令行輸入:java -XX:+PrintCommandLineFlags -version 查看jvm默認參數如圖:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-pKMke0p9-1589680207000)(E:\技術帖子\筆記\基礎\圖\java對象結構\jvm參數.png)]

分別是 -XX:+UseCompressedOops 和 -XX:+UseCompressedClassPointers
這2個參數都是默認開啓(+代表開啓,-代表關閉)

UseCompressedOops:普通對象指針壓縮(oop是ordinary object pointer的縮寫),
UseCompressedClassPointers:類型指針壓縮。

例如:

Object o = new Object();
o指向new Object()的引用就是“普通對象指針”,
new Object()自身還需要指向Object類型的引用,也就是"類型指針"。

這2個壓縮參數可以有4種組合(++, --, + -, -+),但有1種組合是會拋出警告的:

-XX:+UseCompressedClassPointers -XX:-UseCompressedOops,不要使用這種參數組合,用這種參數啓動jvm時會拋出警告。

Java HotSpot(TM) 64-Bit Server TIM warning: UseCompressedClassPointers requires UseCompressOops

原因是jvm層面的hotspot源碼對jvm的參數組合做了限制,一看就懂:

// UseCompressedOops must be on for UseCompressedClassPointers to be on.
if (!UseCompressedOops){
   if (UseCompressedClassPointers){
   warning("UseCompressedClassPointers requires UseCompressOops");
}
  FLAG_SET_DEFAULT(UseCompressedClassPointers , false);
}

HotSpot對象模型

HotSpot中採用了OOP-Klass模型,它是描述Java對象實例的模型,它分爲兩部分:

  • 類被加載到內存時,就被封裝成了klass,klass包含類的元數據信息,像類的方法、常量池這些信息都是存在klass裏的,你可以認爲它是java裏面的java.lang.Class對象,記錄了類的全部信息;

  • OOP(Ordinary Object Pointer)指的是普通對象指針,它包含MarkWord 和元數據指針,MarkWord用來存儲當前指針指向的對象運行時的一些狀態數據;元數據指針則指向klass,用來告訴你當前指針指向的對象是什麼類型,也就是使用哪個類來創建出來的;

    那麼爲何要設計這樣一個一分爲二的對象模型呢?這是因爲HotSopt JVM的設計者不想讓每個對象中都含有一個vtable(虛函數表),所以就把對象模型拆成klass和oop,其中oop中不含有任何虛函數,而klass就含有虛函數表,可以進行method dispatch。

HotSpot中,OOP-Klass實現的代碼都在/hotspot/src/share/vm/oops/路徑下,oop的實現爲instanceOop 和 arrayOop,他們來描述對象頭,其中arrayOop對象用於描述數組類型。

以下就是oop.hhp文件中oopDesc的源碼,可以看到兩個變量_mark就是MarkWord,_metadata就是元數據指針,指向klass對象,這個指針壓縮的是32位,未壓縮的是64位;

volatile markOop _mark;  //標識運行時數據
  union _metadata {
    Klass*      _klass;
    narrowKlass _compressed_klass;
  } _metadata;  //klass指針

一個Java對象在內存中的佈局可以連續分成兩部分:instanceOop(繼承自oop.hpp)和實例數據;

如圖:
在這裏插入圖片描述

通過棧幀中的對象引用reference找到Java堆中的對象,再通過對象的instanceOop中的元數據指針klass來找到方法區中的instanceKlass,從而確定該對象的類型。

對象大小的計算

有以下幾點:

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

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

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

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

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

貼網上的一個比較實用的工具類:

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;  

/** 

​	*對象佔用字節大小工具類 

​    **/  
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);  
}  
}

最後舉三個例子:

首先需要創建一個mavean項目,引入包

<dependency>
  <groupId>org.openjdk.jol</groupId>
  <artifactId>jol-core</artifactId>
  <version>0.9</version>
</dependency>

1.需要補齊的對象

代碼

public class User {
    long sex;
    Long mobile;
    String name;

    public static void main(String[] args) {
        System.out.println(ClassLayout.parseInstance(new User()).toPrintable());
    }
}

輸出如圖
在這裏插入圖片描述
2.不需要padding補齊的對象

代碼:

public class User {

    String name;
    Long mobile;
    int sex;

    public static void main(String[] args) {
        System.out.println(ClassLayout.parseInstance(new User()).toPrintable());
    }
}

輸出如圖
在這裏插入圖片描述
3.空對象,所佔字節數

代碼:

public class User {

    public static void main(String[] args) {
        System.out.println(ClassLayout.parseInstance(new User()).toPrintable());
    }
}

輸出如圖
在這裏插入圖片描述
4.數組對象結構

代碼:

public class ArrayTest {

    public static void main(String[] args) {
        System.out.println(ClassLayout.parseInstance(new Integer[7]).toPrintable());
        System.out.println(ClassLayout.parseInstance(new Integer[8]).toPrintable());
        System.out.println(ClassLayout.parseInstance(new int[7]).toPrintabl![在這裏插入圖片描述](https://img-blog.csdnimg.cn/20200517102321457.png)e());
    }
}

輸出如圖
在這裏插入圖片描述
如果大家對java架構相關感興趣,可以關注下面公衆號,會持續更新java基礎面試題, netty, spring boot,spring cloud等系列文章,一系列乾貨隨時送達, 超神之路從此展開, BTAJ不再是夢想!

架構殿堂

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