Java

Организация Java объектов в памяти

В данной статье мы рассмотрим, как устроены Java объекты в памяти.

Стандарт Java не указывает, какой должна быть организация памяти объектов, поэтому мы рассмотрим эти аспекты на примере open-source HotSpot JVM.

Java Object

Структура, описывающая Java Object, который неявно является родительским классом для всех объектов в Java, состоит из Mark Word и Klass Word.

Mark word — это структура, которая выступает в роли хэдеров объекта. JVM хранит здесь:

  • Identity Hash Code — кэш-код, возвращаемый методом System.identityHashCode(Object x) или дефолтным методом hashCode() объекта, если не был переопределен.
  • Biased lock pattern — требуется для реализации Biased Locking, т.е. чтобы «привязать» (bias) лок к треду.
  • Лок — каждый объект в Java может быть использован в качестве монитора (intrinsic lock), и реализовано это путем хранения лока в хэдере объекта
  • Метаданные для GC — время жизни объекта, а точнее, счетчик пережитых сборок мусора. После определенного значения пережитых сборок объект перемещается в Old Generation.

Klass Word — это указатель на область памяти в Metaspace, где хранится информация о классе: имя класса, его поля, информация о родительском классе и так далее. Другими словами, это java.lang.Class, который мы можем получить вызовом getClass() на объекте.

Все это мы можем подсмотреть из исходников HotSpot JVM, находящихся в свободном доступе на GitHub. Базовая структура Java объекта описана в файле hotspot/share/oops/oop.hpp:

class oopDesc {
  friend class VMStructs;
  friend class JVMCIVMStructs;
 private:
  volatile markWord _mark;
  union _metadata {
    Klass*      _klass;
    narrowKlass _compressed_klass;
  } _metadata;
....
}

Итак, отсюда мы видим, что структура oopDesc содержит поле типа markWord и union _metadata, хранящую указатель на Klass. Давайте взглянем на структуру markWord поближе. Она описана в файле hotspot/share/oops/markWord.hpp:

class markWord {
 private:
  uintptr_t _value;
....
}

Мы видим, что состояние структуры — всего одно поле типа uintptr_t, а не несколько различных полей для хранения хэдеров. Это интересно, но не удивительно: нам необходимо сделать структуру объекта, являющегося родительским классом для всех классов в Java, как можно легче. Поэтому все необходимые данные извлекаются и устанавливается в хэдере с помощью техники известной как Bit Masking: мы просто устанавливаем нужные биты в поле _value.

На самом же деле структура oopDesc является родительской для еще двух структур: для инстансов классов и для массивов объектов.

Это мы можем узнать из файла hotspot/share/oops/oopsHierarchy.hpp:

// OBJECT hierarchy
// This hierarchy is a representation hierarchy, i.e. if A is a superclass
// of B, A's representation is a prefix of B's representation.

typedef class oopDesc*                    oop;
typedef class   instanceOopDesc*            instanceOop;
typedef class   arrayOopDesc*               arrayOop;
typedef class     objArrayOopDesc*            objArrayOop;
typedef class     typeArrayOopDesc*           typeArrayOop;

Нас интересуют классы instanceOopDesc и arrayOopDesc. Первый класс используется для всех инстансов классов, кроме Object, например вызов new HashMap() вернет нам instanceOopDesc. А вторая структура используется для массивов объектов — так, вызов new Integer[N] вернет нам arrayOopDesc.

Взглянем на объявление первого класса, содержащееся в файле hotspot/share/oops/instanceOop.hpp:

// 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() {
    if (UseNewFieldLayout) {
      return (UseCompressedClassPointers) ?
              klass_gap_offset_in_bytes() :
              sizeof(instanceOopDesc);
    } else {
      // The old layout could not deal with compressed oops being off and compressed
      // class pointers being off.
      return (UseCompressedOops && UseCompressedClassPointers) ?
              klass_gap_offset_in_bytes() :
              sizeof(instanceOopDesc);
    }
  }
};

Мы видим, что этот класс только добавляет новые методы, которые нас не очень интересуют.

Посмотрим же на второй класс arrayOopDesc, объявление которого содержится в файле hotspot/share/oops/arrayOop.hpp:

// arrayOopDesc is the abstract baseclass for all arrays.  It doesn't
// declare pure virtual to enforce this because that would allocate a vtbl
// in each instance, which we don't want.

// The layout of array Oops is:
//
//  markWord
//  Klass*    // 32 bits if compressed but declared 64 in LP64.
//  length    // shares klass memory or allocated after declared fields.

class arrayOopDesc : public oopDesc {
  friend class VMStructs;
  friend class arrayOopDescTest;

  // Interpreter/Compiler offsets

  // Header size computation.
  // The header is considered the oop part of this type plus the length.
  // Returns the aligned header_size_in_bytes.  This is not equivalent to
  // sizeof(arrayOopDesc) which should not appear in the code.
  static int header_size_in_bytes() {
...

Итак, документация говорит нам, что структура массива объектов содержит все поля из oopDesc и в дополнение длину массива (length). Однако, это поле не объявлено явно.

Метод length_offset_in_bytes(), возвращающий offset, с которого в участке памяти этой структуры начинается значение длины, сообщает нам как оно аллоцируется:

 public:
  // The _length field is not declared in C++.  It is allocated after the
  // declared nonstatic fields in arrayOopDesc if not compressed, otherwise
  // it occupies the second half of the _klass field in oopDesc.
  static int length_offset_in_bytes() {
    return UseCompressedClassPointers ? klass_gap_offset_in_bytes() :
                               sizeof(arrayOopDesc);
  }

Мы можем также увидеть, что длина вычисляется путем интерпретирования участка памяти:

  // Accessors for instance variable which is not a C++ declared nonstatic
  // field.
  int length() const {
    return *(int*)(((intptr_t)this) + length_offset_in_bytes());
  }

Анализ размера объектов

Итак, мы разобрали исходники и узнали, что и как содержится в структуре объекта. Теперь посчитаем, сколько это все стоит по памяти. Давайте сначала попытаемся проанализировать, основываясь на ранее полученных наблюдениях, а затем проверить на практике.

Итак, класс oopDesc:

Поле volatile markWord _mark — это структура, состоящая из единственного поля поля типа uintptr_t. Это поле занимает 32 бита в 32-битной системе (4 байта) и 64 бита (8 байт) в 64-битной системе. Почему HotSpot JVM понадобился разный размер этого поля для 32 битной и 64 битной систем? Дело в том, что если произошел biased locking, то организация данных в markWord меняется и в него кладется указатель на JavaThread, а указатели зависят от битности.

Мы можем это увидеть из документации к классу markWord:

// The markWord describes the header of an object.
//
// 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)
//
//  64 bits:
//  --------
//  unused:25 hash:31 -->| unused_gap:1   age:4    biased_lock:1 lock:2 (normal object)
//  JavaThread*:54 epoch:2 unused_gap:1   age:4    biased_lock:1 lock:2 (biased object)

Второе поле — это union (объединение) из 2-х полей:

  union _metadata {
    Klass*      _klass;
    narrowKlass _compressed_klass;
  } _metadata;

Для справки, union — это механизм C++, позволяющий хранить ровно одно поле из объявленных в union, то есть размер union зависит от того, какое поле используется.

Здесь следует вспомнить о Compressed Oops — оптимизации Java, позволяющей сжимать указатели до 32 бит в 64-битной системе. Дело в том, что объединение _metadata будет хранить явно указатель на Klass, если сжатие указателей не включено, и narrowKlass если сжатие включено. Структура narrowKlass это typedef на juint, как мы узнаем из файла oopsHierarchy.hpp:

typedef juint  narrowKlass;

А juint, в свою очередь, является typedef на u4, как мы узнаем из файла hotspot/share/utilities/globalDefinitions.hpp:

// Unsigned one, two, four and eigth byte quantities used for describing
// the .class file format. See JVM book chapter 4.

typedef jubyte  u1;
typedef jushort u2;
typedef juint   u4;
typedef julong  u8;

Кратко: это типы, представляющие собой значения размером 1, 2, 4 и 8 байт соответственно. Подробнее здесь — Java Virtual Machine Specification.

Так мы понимаем, что в случае сжатия указателей используется поле _compressed_klass длиной 4 байта.

Подведем итог:

  • Mark Word имеет размер в 4 байта на 32-битной и 8 байт на 64-битной системе.
  • Klass Word имеет размер в 4 байта на 32-битной системе, а на 64-битной — 4 байта при включенном сжатии указателей и 8 байт при выключенном сжатии

Давайте проверим это на практике. Для этого мы заиспользуем утилиту JOL (Java Object Layout), позволяющую показать организацию любого Java класса в памяти, с учетом padding и align. Посмотрим на базовый класс java.lang.Object:

$ java -XX:+UseCompressedOops -jar jol-cli.jar internals java.lang.Object
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 0x0000000800000000 base address and 0-bit shift.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

Instantiated the sample instance via default constructor.

java.lang.Object 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)                           00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

Итак, мы видим, что на 64-битной системе хэдер занимает 12 байт (8 байт markWord + 4 байта сжатый Klass pointer), но для align под машинное слово JVM добавляет 4 байта, в итоге получая размер в 16 байт. Мы можем подумать, что сжатие Klass Pointer было быссмысленным, ведь мы бы в любом случае получили 16 байт, однако стоит учитывать, что это только базовый класс Object. Для инстанса Integer, который наследует Object, мы получаем свободное пространство для размещения числового значения:

 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)                           e8 39 01 00 (11101000 00111001 00000001 00000000) (80360)
     12     4    int Integer.value                             0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

Анализ массива объектов

Давайте теперь проанализируем стоимость массива объектов. Мы помним, что в его структуре добавляется 4 байта для хранения длины массива. Давайте же посмотрим на организацию памяти массива объектов:

$ java -XX:+UseCompressedOops -jar jol-cli.jar internals [I
[I object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0    16        (object header)                           N/A
     16     0    int [I.                             N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

[I — это массив объектов типа Integer. Как мы видим, хэдер занял 16 байт (8 байт markWord + 4 байта Klass Pointer + 4 байта длина массива), как и ожидалось.

Добавить комментарий

Ваш адрес email не будет опубликован.