java 對象的內存布局和大小計算

創建java對象的方式

java對象的創建有多種,最簡單的是new XXClass,還可以通過反射,xx.clone(),反序列化等方法,在new以及反射創建對象的時候,會初始化實例字段。如果類沒有構造器,會默認添加構造器,并且編譯成<init>方法。默認生成的構造器里,如果父類有無參構造器, 會隱式遞歸調用父類的構造器.

public class TestClass {
  public void test() {
    TestClass t = new TestClass();
  }
}

生成的字節碼如下(用javap -v TestClass可以得到):

public TestClass();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return

public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class TestClass
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: return

可以看到,TestClass類添加了默認的構造器,生成了<init>方法,同時調用了Object類的<init>方法,也就是java.lang.Object類的構造方法.

jvm如何處理new指令

new指令會實例化一個對象, JVM如何處理new指令生成具體的對象并不是jvm規范的一部分,這里指的JVM實現指的是Hotspot的實現

image.png

類加載檢查
普通對象的創建過程:虛擬機遇到一條new指令時,首先將去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果沒有,那么必須先執行相應的類加載過程。

分配內存
分配內存時主要注意兩個問題:1.如何分配空間。2.修改指針時如何實現線程安全。

  1. 內存的分配存在兩種實現方式:
  • 指針碰撞:假設Java堆中內存是絕對規整的,所有用過的內存都放在一邊,空閑的內存放在另一邊,中間放著一個指針作為分界點的指示器,那所分配內存就僅僅是把那個指針向空閑空間那邊挪動一段與對象大小相等的距離,這種分配方式稱為“指針碰撞”(Bump the Pointer)。

  • 空閑列表:如果Java堆中的內存并不是規整的,已使用的內存和空閑的內存相互交錯,那就沒有辦法簡單地進行指針碰撞了,虛擬機就必須維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,并更新列表上的記錄,這種分配方式稱為“空閑列表”(Free List)。

  1. 線程安全

保證修改指針時線程安全也存在兩種實現方式:

  • 同步處理:對分配內存的空間動作進行同步處理(采用CAS配上失敗重試的方式保證跟新操作的原子性)
  • 本地線程分配緩沖:把內存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預先分配一小塊內存,叫本地線程分配緩沖(Thread Local Allocation Buffer,TLAB),可以通過-XX:UseTLAB來設置,哪個線程需要分配內存,就在那個線程的TLAB上分配,只有TLAB用完并分配新的TLAB時,才需要同步鎖定。

初始化和設置

內存分配完成后,虛擬機將分配到的內存初始化為零值(除對象頭外),接下來,虛擬機要對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。這些信息存放在對象的對象頭(Object Header)之中。
在上面工作都完成之后,從虛擬機的視角來看,一個新的對象已經產生了,但從Java程序的視角來看,,對象創建才剛剛開始——<init>方法還沒有執行,所有的字段都還為零。所以,一般來說執行new指令之后會接著執行<init>方法,把對象按照程序員的意愿進往初始化,這樣一個真正可用的對象才算完全產生出來。

對象的內存布局

對象的內存布局也不是jvm規范的一部分,屬于實現的細節,這里講的也是hotspot的實現.
hotspot設計了一個OOP-Klass Model,這里的 OOP 指的是 Ordinary Object Pointer (普通對象指針),它用來表示對象的實例信息,看起來像個指針實際上是藏在指針里的對象。而 Klass 則包含元數據和方法信息,用來描述Java類。之所以采用這個模型是因為HotSopt JVM的設計者不想讓每個對象中都含有一個vtable(虛函數表),所以就把對象模型拆成klass和oop,其中oop中不含有任何虛函數,而Klass就含有虛函數表,可以進行method dispatch。

Klass
Klass簡單的說是Java類在HotSpot中的c++對等體,用來描述Java類。那Klass是什么時候創建的呢?一般jvm在加載class文件時,會在方法區創建instanceKlass,表示其元數據,包括常量池、字段、方法等。

OOP
Klass是在class文件在加載過程中創建的,OOP則是在Java程序運行過程中new對象時創建的。一個OOP對象包含以下幾個部分:

  • instanceOopDesc,也叫對象頭

    • Mark Word,主要存儲對象運行時記錄信息,如hashcode, GC分代年齡,鎖狀態標志,線程ID,時間戳等。這些字段并不是固定的,而是不斷變化的,對象在不同的階段,mark word的值不一樣。 在64位的虛擬機上標記字段一般是8個字節,類型指針也是8個字節,總共就是16個字節. 可以使用-XX:UseCompressedOops來開啟壓縮指針, 以減少對象的內存使用量, 默認是開啟的. 而類型指針指向的是對象的元數據信息, 也就是對象所屬類的信息.

    • 元數據指針,即指向方法區的instanceKlass實例

  • 實例數據

  • 對齊填充。僅僅起到占位符的作用,并非必須。

class Model
{
    public static int a = 1;
    public int b;
 
    public Model(int b) {
        this.b = b;
    }
}
 
public static void main(String[] args) {
    int c = 10;
    Model modelA = new Model(2);
    Model modelB = new Model(3);
}
image.png
oop-klass的jvm源碼分析

OOP的實現就是instanceOopDesc和arrayOopDesc,分別是普通對象實現和數組對象實現, 均繼承自上面的oopDesc,數組對象比普通對象多一個長度字段.

// oopDesc is the top baseclass for objects classes.  The {name}Desc classes describe
// the format of Java objects so the fields can be accessed from C++.
//這個類描述了java對象的格式
// oopDesc is abstract.
// (see oopHierarchy for complete oop class hierarchy)
//
// no virtual functions allowed  不允許虛函數
class oopDesc {
  friend class VMStructs;
 private:
  volatile markOop  _mark;  //Mark Word
  union _metadata {    //元數據指針
    wideKlassOop    _klass;
    narrowOop       _compressed_klass;
  } _metadata;
 
}

oopDesc類描述了java對象的格式。

oopDesc中包含兩個數據成員:_mark 和 _metadata。

_mark對象即為Mark World,存儲對象運行時記錄信息,如hashcode, GC分代年齡,鎖狀態標志,線程ID,時間戳等。
_metadata即為元數據指針,它是一個聯合體,其中_klass是普通指針,_compressed_klass是壓縮類指針,這兩個指針都指向instanceKlass對象。

instanceOopDesc繼承了oopDesc,它代表了java類的一個實例化對象。

// An instanceOop is an instance of a Java Class
// Evaluating "new HashTable()" will create an instanceOop.
 
class instanceOopDesc : public oopDesc {
 public:
  // aligned header size.
  static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; }
 
  // If compressed, the offset of the fields of the instance may not be aligned.
  static int base_offset_in_bytes() {
    return UseCompressedOops ?
             klass_gap_offset_in_bytes() :
             sizeof(instanceOopDesc);
  }
 
  static bool contains_field_offset(int offset, int nonstatic_field_size) {
    int base_in_bytes = base_offset_in_bytes();
    return (offset >= base_in_bytes &&
            (offset-base_in_bytes) < nonstatic_field_size * heapOopSize);
  }
};

instanceKlass是Java類的vm級別的表示。其中,ClassState描述了類加載的狀態:分配、加載、鏈接、初始化。instanceKlass的布局包括:聲明接口、字段、方法、常量池、源文件名等等。

// An instanceKlass is the VM level representation of a Java class.
// It contains all information needed for at class at execution runtime.
 
class instanceKlass: public Klass {
  friend class VMStructs;
 public:
 
  enum ClassState {
    unparsable_by_gc = 0,               // object is not yet parsable by gc. Value of _init_state at object allocation.
    allocated,                          // allocated (but not yet linked)
    loaded,                             // loaded and inserted in class hierarchy (but not linked yet)
    linked,                             // successfully linked/verified (but not initialized yet)
    being_initialized,                  // currently running class initializer
    fully_initialized,                  // initialized (successfull final state)
    initialization_error                // error happened during initialization
  };
 
//部分內容省略
protected:
  // Method array.  方法數組
  objArrayOop     _methods; 
  // Interface (klassOops) this class declares locally to implement.
  objArrayOop     _local_interfaces;  //該類聲明要實現的接口.
  // Instance and static variable information
  typeArrayOop    _fields; 
  // Constant pool for this class.
  constantPoolOop _constants;     //常量池
  // Class loader used to load this class, NULL if VM loader used.
  oop             _class_loader;  //類加載器
  typeArrayOop    _inner_classes;   //內部類
  Symbol*         _source_file_name;   //源文件名
 
}

markOop描述了java的對象頭格式。

// The markOop describes the header of an object.
//markOop描述了Java的對象頭
// Note that the mark is not a real oop but just a word.
// It is placed in the oop hierarchy for historical reasons.
//
// Bit-format of an object header (most significant first, big endian layout below):
//
//  32 bits:
//  --------
//             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
//             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
//             size:32 ------------------------------------------>| (CMS free block)
//             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
//  64 bits:
//  --------
//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
//  size:64 ----------------------------------------------------->| (CMS free block)
//
//  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)
//  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)
//  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
//  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
 
class markOopDesc: public oopDesc {
 private:
  // Conversion
  uintptr_t value() const { return (uintptr_t) this; }
 
 public:
  // Constants
  enum { age_bits                 = 4,
         lock_bits                = 2,
         biased_lock_bits         = 1,
         max_hash_bits            = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
         hash_bits                = max_hash_bits > 31 ? 31 : max_hash_bits,
         cms_bits                 = LP64_ONLY(1) NOT_LP64(0),
         epoch_bits               = 2
  };
 
  // The biased locking code currently requires that the age bits be
  // contiguous to the lock bits.
  enum { lock_shift               = 0,
         biased_lock_shift        = lock_bits,
         age_shift                = lock_bits + biased_lock_bits,
         cms_shift                = age_shift + age_bits,
         hash_shift               = cms_shift + cms_bits,
         epoch_shift              = hash_shift
  };
//部分內容省略
}

instanceOopDesc對象的創建過程

allocate_instance方法
instanceOopDesc對象通過instanceKlass::allocate_instance進行創建,實現過程如下:
1、has_finalizer判斷當前類是否包含不為空的finalize方法;
2、size_helper確定創建當前對象需要分配多大內存;
3、CollectedHeap::obj_allocate從堆中申請指定大小的內存,并創建instanceOopDesc對象

instanceOop instanceKlass::allocate_instance(TRAPS) {
  assert(!oop_is_instanceMirror(), "wrong allocation path");
  bool has_finalizer_flag = has_finalizer(); // Query before possible GC
  int size = size_helper();  // Query before forming handle.
 
  KlassHandle h_k(THREAD, as_klassOop());
 
  instanceOop i;
 
  i = (instanceOop)CollectedHeap::obj_allocate(h_k, size, CHECK_NULL);
  if (has_finalizer_flag && !RegisterFinalizersAtInit) {
    i = register_finalizer(i, CHECK_NULL);
  }
  return i;
}

CollectedHeap::obj_allocate從堆中申請指定大小的內存,并創建instanceOopDesc對象,實現如下:

oop CollectedHeap::obj_allocate(KlassHandle klass, int size, TRAPS) {
  debug_only(check_for_valid_allocation_state());
  assert(!Universe::heap()->is_gc_active(), "Allocation during gc not allowed");
  assert(size >= 0, "int won't convert to size_t");
  HeapWord* obj = common_mem_allocate_init(klass, size, CHECK_NULL);
  post_allocation_setup_obj(klass, obj);
  NOT_PRODUCT(Universe::heap()->check_for_bad_heap_word_value(obj, size));
  return (oop)obj;
}

common_mem_allocate_noinit方法,該方法的實現如下:
1、如果開啟了TLAB優化,從tlab分配內存并返回(TLAB全稱ThreadLocalAllocBuffer,是線程的一塊私有內存);
2、如果第一步不執行,調用Universe::heap()->mem_allocate方法在堆上分配內存并返回;

HeapWord* CollectedHeap::common_mem_allocate_noinit(KlassHandle klass, size_t size, TRAPS) {
 
  // Clear unhandled oops for memory allocation.  Memory allocation might
  // not take out a lock if from tlab, so clear here.
  CHECK_UNHANDLED_OOPS_ONLY(THREAD->clear_unhandled_oops();)
 
  if (HAS_PENDING_EXCEPTION) {
    NOT_PRODUCT(guarantee(false, "Should not allocate with exception pending"));
    return NULL;  // caller does a CHECK_0 too
  }
 
  HeapWord* result = NULL;
  if (UseTLAB) {  //如果開啟了TLAB優化
    result = allocate_from_tlab(klass, THREAD, size);
    if (result != NULL) {
      assert(!HAS_PENDING_EXCEPTION,
             "Unexpected exception, will result in uninitialized storage");
      return result;
    }
  }
  bool gc_overhead_limit_was_exceeded = false;
  result = Universe::heap()->mem_allocate(size,
                                          &gc_overhead_limit_was_exceeded);
  if (result != NULL) {
    NOT_PRODUCT(Universe::heap()->
      check_for_non_bad_heap_word_value(result, size));
    assert(!HAS_PENDING_EXCEPTION,
           "Unexpected exception, will result in uninitialized storage");
    THREAD->incr_allocated_bytes(size * HeapWordSize);
 
    AllocTracer::send_allocation_outside_tlab_event(klass, size * HeapWordSize);
 
    return result;
  }

mem_allocate方法,假設使用G1垃圾收集器,該方法實現如下:
g1CollectedHeap.cpp

HeapWord*
G1CollectedHeap::mem_allocate(size_t word_size,
                              bool*  gc_overhead_limit_was_exceeded) {
  assert_heap_not_locked_and_not_at_safepoint();
 
  // Loop until the allocation is satisfied, or unsatisfied after GC.
  for (int try_count = 1; /* we'll return */; try_count += 1) {
    unsigned int gc_count_before;
 
    HeapWord* result = NULL;
    if (!isHumongous(word_size)) {
      result = attempt_allocation(word_size, &gc_count_before);
    } else {
      result = attempt_allocation_humongous(word_size, &gc_count_before);
    }
    if (result != NULL) {
      return result;
    }
 
    // Create the garbage collection operation...
    VM_G1CollectForAllocation op(gc_count_before, word_size);
    // ...and get the VM thread to execute it.
    VMThread::execute(&op);
 
    if (op.prologue_succeeded() && op.pause_succeeded()) {
      // If the operation was successful we'll return the result even
      // if it is NULL. If the allocation attempt failed immediately
      // after a Full GC, it's unlikely we'll be able to allocate now.
      HeapWord* result = op.result();
      if (result != NULL && !isHumongous(word_size)) {
        // Allocations that take place on VM operations do not do any
        // card dirtying and we have to do it here. We only have to do
        // this for non-humongous allocations, though.
        dirty_young_block(result, word_size);
      }
      return result;
    } else {
      assert(op.result() == NULL,
             "the result should be NULL if the VM op did not succeed");
    }
 
    // Give a warning if we seem to be looping forever.
    if ((QueuedAllocationWarningCount > 0) &&
        (try_count % QueuedAllocationWarningCount == 0)) {
      warning("G1CollectedHeap::mem_allocate retries %d times", try_count);
    }
  }
 
  ShouldNotReachHere();
  return NULL;
}

實例數據

成員變量在對象中的布局

各字段的分配策略為longs/doubles、ints、shorts/chars、bytes/boolean、oops(ordinary object pointers),相同寬度的字段總是被分配到一起,便于之后取數據。父類定義的變量會出現在子類定義的變量的前面

Hotspot采用的方法是直接指針訪問對象, 如圖:

image.png

Padding

對齊填充是最常見的優化手段,CPU一次尋址一般是2的倍數,所以一般會按照2的倍數來對齊提高CPU效率.這個似乎沒什么好講的.
此外,JVM上對齊填充也方便gc, JVM能直接計算出對象的大小, 就能快速定位到對象的起始終止地址.

對象大小的計算

JVM的數據類型分為基本數據類型和引用數據類型.

基本數據類型有:

long/double: 8字節, 長整型和雙精度浮點型
int/float: 4字節, 整數和浮點數
char,short: 2字節,字符型和短整型
byte: 1字節, 整數

在JDK8, 64位HotSpot上, 引用數據類型都是直接指針, 如果開了壓縮指針,就是4字節,沒開就是8字節

用原生數據類型就是為了提高性能的.后來為了滿足一切皆對象的概念和泛型系統,出了一堆包裝類, 造成裝箱和拆箱的一堆性能問題不說, 還浪費內存.
比如一個int原生類型才4字節,而Integer包裝類對象頭就至少12字節了.

前面講過一個對象包含3部分數據:

對象頭(Object Header)
實例數據(Instance Data)
對齊填充(Padding)

對象頭前面說過, 在64位的虛擬機上開了壓縮指針就是12字節,沒開就是16字節.
實例數據的大小依據數據類型的大小來計算, 注意要子類的對象大小要把父類的實例數據大小也計算進去.

對齊填充是按照對象里最寬的數據類型的大小來對齊的, 比如最大的是long8字節, 那么就是按照8的倍數來對齊.

demo

public class ObjectByteTest {

    private double a;
    private int b;
    private String c;


    static void print(String message) {
        System.out.println(message);
        System.out.println("-------------------------");
    }

    public static void main(String[] args) {
        ObjectByteTest obj = new ObjectByteTest();

        //查看對象內部信息
        print(ClassLayout.parseInstance(obj).toPrintable());

        //查看對象外部信息
        print(GraphLayout.parseInstance(obj).toPrintable());

        //獲取對象總大小
        print("size : " + GraphLayout.parseInstance(obj).totalSize());
    }

}

按照理論,開啟壓縮指針后,對象頭占12字節, 實例數據最長的pm25是8個字節, int是4字節, String是引用類型,占4字節, 按照8字節對齊.
總共是12+8+4+4=28字節,按照8字節對齊是32字節,要4個字節的對齊填充.

這里使用JOL是openjdk提供的用來驗證JVM的內存布局方案的工具. 添加pom依賴即可

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

得到的結果(JVM默認開啟壓縮指針):

jvm.ObjectByteTest 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)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4                int ObjectByteTest.b                          0
     16     8             double ObjectByteTest.a                          0.0
     24     4   java.lang.String ObjectByteTest.c                          null
     28     4                    (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以看到,Instance size=32字節, 和我們的計算一致.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。