上一节JVM拾遗-3-类装载机制讲了JVM如何将类装载到虚拟机以供后续使用 那么JVM是如何创建类的实例呢?该对象是如何分配内存的?

1. 实例化

1.1 创建对象的方式

Java对象的创建, 有多种方式,最简单就是new XXClass, 还可以通过反射xx.clone(),反序列化以及黑科技Unsafe.allocateInstance等方法.

new和反射创建对象实例的时候,会初始化实例字段.

如果类没有构造器,会默认添加构造器,并且编译成<init>方法. 默认生成的构造器里,如果父类有无参构造器, 会隐式递归调用父类的构造器.

如下类:

1
2
3
4
5
  public class TestClass {
    public void test() {
      TestClass t = new TestClass();
    }
  }

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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

那两行invokespecial指令是用来调用类构造器或者类的私有方法的.

可以看到,TestClass类添加了默认的构造器,生成了<init>方法,同时调用了Object类的<init>方法,也就是java.lang.Object类的构造方法. 也可以显式调用父类的构造器, 使用super(a,b,c)这样的形式即可

1.2 虚拟机如何处理new指令

new指令会实例化一个对象, JVM如何处理new指令生成具体的对象并不是规范的一部分,所以这里指的JVM实现指的是Hotspot的实现

下面是new指令实例化对象的过程:

  1. JVM会判断对应的类是否被加载,链接,初始化, 判断的依据是根据符号引用找方法区是否有类的数据.如果没有需要进行类的加载过程
  2. 为对象分配内存, 有2种方式
  • 指针提升(bump pointer, 注意没翻译错), 如果堆内存是规整的, 那么只需要把指针移动一个对象大小的距离就完成了分配
  • 空闲列表(free list), 如果堆内存是不连续的,碎片很多, JVM会维护一个可用内存队列, 从中查出可用内存分配给对象. 具体使用哪种方式依据堆内存的情况而定,而堆内存的情况很大受到gc的影响,后面会细谈
  1. 初始化内存, JVM会将分配到的内存初始化为0值.
  2. 设置对象的元数据信息到对象头, 例如对象的hashcode,gc分带年龄,偏向锁状态等信息

一般来讲,new指令后面都会跟着invokespecial来执行<init>方法,也就是执行类构造器里的逻辑.

对象创建的并发安全问题

JVM处理对象创建的并发安全一般有2种方法:

  1. CAS操作加上失败重试
  2. 内存分配的动作按照线程划分在不同的内存空间区域进行,这些预先分配的内存仅属于该线程私有,也称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB), 可以通过-XX:UseTLAB来设置

2. 对象的内存布局

对象的内存布局也不是JVM规范的一部分,属于实现的细节,Hotspot将对象分成3部分,分别是:

  1. 对象头(Object Header)
  2. 实例数据(Instance Data)
  3. 对齐填充(Padding)

2.1 Object Header

对象头由标记字段(Mark Word)类型指针(Klass Pointer)组成

Mark Word用于存储该对象的运行时数据, 包括:

  • 对象hashcode, gc分代年龄
  • 锁记录指针, 重量级锁指针
  • 偏向线程id, 偏向时间戳

在64位的虚拟机上标记字段一般是8个字节,类型指针也是8个字节,总共就是16个字节.

可以使用-XX:UseCompressedOops来开启压缩指针, 以减少对象的内存使用量, 默认是开启的.

类型指针指向的是对象的元数据信息, 也就是对象所属类的信息.

HotSpot实现

Hotspot一般用OOP-Klass二分模型来实现, OOP(Ordinary Object Pointer)是普通对象指针,Klass用来描述具体的类型.

以下是openjdk的实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// hotspot/src/share/vm/oops/oop.hpp
class oopDesc {
  friend class VMStructs;
 private:
  // mark word真身
  volatile markOop  _mark;
  // 元数据指针
  union _metadata {
    Klass*      _klass;
    narrowKlass _compressed_klass;
  } _metadata;

  ...

 public:
  markOop  mark() const         { return _mark; }
  Klass* klass() const;
  narrowKlass* compressed_klass_addr();
  
 private:
  // field addresses in oop
  void*     field_base(int offset)        const;

  jbyte*    byte_field_addr(int offset)   const;
  jchar*    char_field_addr(int offset)   const;
  jboolean* bool_field_addr(int offset)   const;
  jint*     int_field_addr(int offset)    const;
  jshort*   short_field_addr(int offset)  const;
  jlong*    long_field_addr(int offset)   const;
  jfloat*   float_field_addr(int offset)  const;
  jdouble*  double_field_addr(int offset) const;
  Metadata** metadata_field_addr(int offset) const;

  ...省略非关键代码...

其中OOP的实现就是instanceOopDescarrayOopDesc,分别是普通对象实现和数组对象实现, 均继承自上面的oopDesc, 数组对象比普通对象多一个长度字段. oopDesc存放有_mark_metadata, _mark就是标记字段(mark word), 而_metadata里就有一个指针指向Klass.

限于篇幅,markOop.hpp的代码就不贴了,大致就是上面提到的信息。

类型指针(上面_meta__klass指针)指向的Klass信息, 即类在JVM的真身, 看看就好.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// hotspot/src/share/vm/oops/instanceKlass.hpp
class InstanceKlass: public Klass {
  ...

  // See "The Java Virtual Machine Specification" section 2.16.2-5 for a detailed description
  // of the class loading & initialization procedure, and the use of the states.
  // 类的加载状态, JVM规范里的各个阶段可能会穿插进行
  enum ClassState {
    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
  };
  ...
}

// hotspot/src/share/vm/oops/klass.hpp
class Klass : public Metadata {
  friend class VMStructs;
 protected:
  jint        _layout_helper;
  juint       _super_check_offset;
  Symbol*     _name;

  // Cache of last observed secondary supertype
  Klass*      _secondary_super_cache;
  // Array of all secondary supertypes
  Array<Klass*>* _secondary_supers;
  // Ordered list of all primary supertypes
  Klass*      _primary_supers[_primary_super_limit];
  // java/lang/Class instance mirroring this class
  oop       _java_mirror;
  // Superclass
  Klass*      _super;
  // First subclass (NULL if none); _subklass->next_sibling() is next one
  Klass*      _subklass;
  // Sibling link (or NULL); links all subklasses of a klass
  Klass*      _next_sibling;

  // All klasses loaded by a class loader are chained through these links
  Klass*      _next_link;

  // The VM's representation of the ClassLoader used to load this class.
  // Provide access the corresponding instance java.lang.ClassLoader.
  ClassLoaderData* _class_loader_data;

  jint        _modifier_flags;  // Processed access flags, for use by Class.getModifiers.
  AccessFlags _access_flags;    // Access flags. The class/interface distinction is stored here.

  // Biased locking implementation and statistics
  // (the 64-bit chunk goes first, to avoid some fragmentation)
  jlong    _last_biased_lock_bulk_revocation_time;
  markOop  _prototype_header;   // Used when biased locking is both enabled and disabled for this type
  jint     _biased_lock_revocation_count;

  TRACE_DEFINE_KLASS_TRACE_ID;

  // Remembered sets support for the oops in the klasses.
  jbyte _modified_oops;             // Card Table Equivalent (YC/CMS support)
  jbyte _accumulated_modified_oops; // Mod Union Equivalent (CMS support)

  ...省略不重要的部分...

类的各种数据非常多,不过都是实现细节,为了更好的存储JVM规范里的class file format对应的数据,如类的名字符号,符号引用,常量池数据, 方法引用等等 而继承的类Metadata如下:

// hotspot/src/share/vm/oops/metadata.hpp
class Metadata : public MetaspaceObj {
 public:
  NOT_PRODUCT(Metadata()     { _valid = 0; })
  NOT_PRODUCT(bool is_valid() const volatile { return _valid == 0; })
  ...一堆辅助函数...

  int identity_hash()                { return (int)(uintptr_t)this; }

  ...

没错,这个identity_hash函数就是hashcode()函数的真身, 可以看到返回了对象地址:).

Metadata继承的MetaspaceObj则是8以后才有的, java8移除了永久代(PermGen),可能因为经常溢出(Spring: 你们看我干啥?), 生活在元空间的对象都要继承这个类,这个放后面聊.

2.2 Instance Data

实例数据存在instanceOopDesc的父类oopDesc类的实例里,就是上面那堆私有属性, JVM还会对字段重排序,相同的宽度可能被分到一起,比如long/double.所以父类的变量可能出现在子类之前,子类中较窄的变量也可能插入到父类的间隙。

Hotspot采用的方法是直接指针访问对象, 如图:

计算对象的大小可以单独讲一小节,也比较有意思,后面再说。

2.3 Padding

对齐填充是最常见的优化手段,CPU一次寻址一般是2的倍数,所以一般会按照2的倍数来对齐提高CPU效率.这个似乎没什么好讲的. 此外,JVM上对齐填充也方便gc, JVM能直接计算出对象的大小, 就能快速定位到对象的起始终止地址.

3. 总结

本节回顾了java对象的创建方式,以及hotspot执行new指令的一系列操作,包括寻找加载类,分配初始化内存,设置对象的元数据信息到header

同时我们通过代码分析了hotspot中对象的内存布局,分为Object Header, Instance DataPadding, 其中对象头包括mark wordklass pointer, mark word里有丰富的对象运行时信息,比如hashcode,gc分代年龄等,而klass pointer指向一个c++对象Klass的实例,也就是JVM类的真身.存放的类的各种信息,如类的名称,继承的父类,实现的接口,字段信息,方法信息等.而Instance Data存放了运行时类的实例数据值, 字段数据的地址会被重排序优化掉。

instanceKlass对象放在了方法区,instanceOop放在了堆,instanceOop的引用放在了JVM栈.

希望大家有所收获:).