并发[2] - Java对象模型

419 阅读11分钟

前言

Java是一门面向对象的编程语言,Java程序运行过程中无时无刻都有对象被创建出来。在语言层面上,创建对象通常(例外:复制、反序列化)仅仅是一个new关键字而已,而在JVM虚拟机中,一般采用C++语言实现,它是如何表示这些Java对象呢?譬如它们是如何创建、如何布局以及如何访问的。下面以HotSpot虚拟机为例来进行分析。

HotSpot采用C++语言实现,下文中的JVM如无特殊说明,指的都是HotSpot。

Java对象创建

Java程序通过new操作符来创建一个对象,当Java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

下面代码是HotSpot虚拟机字节码解释器(bytecodeInterpreter.cpp)中的代码片段:

CASE(_new): {
        u2 index = Bytes::get_Java_u2(pc+1);
        ConstantPool* constants = istate->method()->constants();
        if (!constants->tag_at(index).is_unresolved_klass()) {
          // 确保对象的Klass已经加载
          Klass* entry = constants->resolved_klass_at(index);
          InstanceKlass* ik = InstanceKlass::cast(entry);
          if (ik->is_initialized() && ik->can_be_fastpath_allocated() ) {
            //InstanceKlass 对象计算new 对象所需内存大小
            size_t obj_size = ik->size_helper();
            oop result = NULL;
            // If the TLAB isn't pre-zeroed then we'll have to do it
            bool need_zero = !ZeroTLAB;
            //如果开启本地线程缓冲(-XX:+/-UseTLAB),直接在线程预分配没空间申请内存
            if (UseTLAB) {
              result = (oop) THREAD->tlab().allocate(obj_size);
            }
            // Disable non-TLAB-based fast-path, because profiling requires that all
            // allocations go through InterpreterRuntime::_new() if THREAD->tlab().allocate
            // returns NULL.
            if (result == NULL) {
              need_zero = true;
              // Try allocate in shared eden
            //尝试再Eden中分配内存
            retry:
              HeapWord* compare_to = *Universe::heap()->top_addr();
              HeapWord* new_top = compare_to + obj_size;
              if (new_top <= *Universe::heap()->end_addr()) {
                //CAS分配内存,失败的话转到retry重试,直到成功
                if (Atomic::cmpxchg(new_top, Universe::heap()->top_addr(), compare_to) != compare_to) {
                  goto retry;
                }
                result = (oop) compare_to;
              }
            }
            if (result != NULL) {
              // 如果需要,为对象初始化零值
              if (need_zero ) {
                HeapWord* to_zero = (HeapWord*) result + sizeof(oopDesc) / oopSize;
                obj_size -= sizeof(oopDesc) / oopSize;
                if (obj_size > 0 ) {
                  memset(to_zero, 0, obj_size * HeapWordSize);
                }
              }
              //根据是否开启偏向锁,设置对象头mark word信息
              if (UseBiasedLocking) {
                result->set_mark(ik->prototype_header());
              } else {
                result->set_mark(markOopDesc::prototype());
              }
              result->set_klass_gap(0);
              result->set_klass(ik);
              // Must prevent reordering of stores for object initialization
              // with stores that publish the new object.
              OrderAccess::storestore();
              //将对象引用入栈,继续执行下一条指令
              SET_STACK_OBJECT(result, 0);
              UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1);
            }
          }
        }
        // Slow case allocation
        CALL_VM(InterpreterRuntime::_new(THREAD, METHOD->constants(), index),
                handle_exception);
        // Must prevent reordering of stores for object initialization
        // with stores that publish the new object.
        OrderAccess::storestore();
        SET_STACK_OBJECT(THREAD->vm_result(), 0);
        THREAD->set_vm_result(NULL);
        UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1);
      }

从上面的代码可以看出,先对klass对象进行初始化工作,然后再用它来创建出oop对象。最终new 一个Java对象在JVM中是一个OOP对象表示。在Java中,每一个类都有一个Class对象,用来表示类的元数据,实例对象就通过Class对象来创建的。由此可知最终在HotSpot虚拟机中,使用Klass对象表示Class对象,OOP对象表示Java实列对象。

oop-Klass model

HotSpot使用Oop-Klass二分模型来表示Java的对象,OOP(Ordinary Object Pointer)指的是普通对象指针,其中Klass对应着Java对象的类型(Class)。为了实现Java方法调用的动态绑定,HotSpot使用了与C++虚函数类似的机制,同时为了避免每个对象都维护一个虚函数表,于是就设计了Klass类。

**为什么要设计一套oop-klass**呢?

以下是HotSpot源码中对采用oop/klass模型的解释:

One reason for the oop/klass dichotomy in the implementation is that we don't want a C++ vtbl pointer in every object. Thus,normal oops don't have any virtual functions. Instead, they forward all "virtual" functions to their klass, which does have a vtbl and does the C++ dispatch depending on the object's actual type. (See oop.inline.hpp for some of the forwarding code.)

大致意思是不希望在每个对象中都有一个C++ VTBL指针,将所有“虚拟”函数转发给它们的KLASS,它们具有VTBL,并且根据对象的实际类型进行C++调度。

VTBL:虚函数表,C++的虚函数的作用就是为了实现多态的机制,每个包含虚函数的类都具有一个虚函数表(virtual table),在这个类对象的地址空间的最靠前的位置存有指向虚函数表的指针。在虚函数表中,按照声明顺序依次排列所有的虚函数。由于C++在运行时并不维护类型信息,所以在编译时直接在子类的虚函数表中将被子类重写的方法替换掉。

在HotSpot中把对象拆分为OOP和Klass,OOP表示对象的实例数据,其中不含有任何虚函数。而Klass表示类数据,提供了虚函数表以实现多态,如果每个对象都维护一张虚函数表,内存开销将会非常大

OOP结构

oopDesc是Oop体系中的最高父类:

//oop对象最高父类
typedef class oopDesc*                            oop;
//普通对象
typedef class   instanceOopDesc*            instanceOop;
//数组对象
typedef class   arrayOopDesc*                    arrayOop;
typedef class     objArrayOopDesc*            objArrayOop;
typedef class     typeArrayOopDesc*            typeArrayOop;
//表示一个Java方法
typedef class   methodOopDesc*                    methodOop;
//表示对象头
typedef class   markOopDesc*                    markOop;
...

以上**每一个类型的OOP都代表一个在JVM内部使用的特定对象的类型。**当创建一个Java对象都会在JVM内创建一个同类型的OOP对象,如普通对象则会在JVM中创建一个instanceOopDesc对象表示。

OOP对象头

instanceOopDesc实例对象继承自oopDesc,oopDesc的定义载Hotspot源码中的 oop.hpp`文件中,主要有两个成员属性:

class oopDesc {
  ...
 private:
  //存储对象的运行时记录信息 markword
  volatile markOop _mark;
  //类指针数据
  union _metadata {
    //未压缩Klass对象指针
    Klass*      _klass;
    //压缩Klass对象指针 (-XX:+UseCompressedClassPointers 开启)
    narrowKlass _compressed_klass;
  } _metadata;
 ...
}

_mark_metadata被称为对象头,其中前者存储对象的运行时记录信息;后者是一个指针,指向当前对象所属的Klass对象。

markOop的存储结构在32位和64位系统中有所差异,但是具体存储的信息是一样的。在32位系统中,markOop一共占32位,存储结构如图下所示:

从图中可以Java对象头存储着HashCode、线程ID、分代年龄以及对象锁信息,不同的状态下结构也不同。

OOP对象体

JVM将Java对象的field存储在oop对象体中,oop提供了一系列的方法来获取和设置field,并且针对每种基础类型都提供了特有的实现。

public:
  // 获取成员属性在OOP的地址
  inline void* field_addr(int offset)     const;
  inline void* field_addr_raw(int offset) const;
  // Access to fields in a instanceOop through these methods(通过这些方法访问instanceOop中的字段。)
  oop obj_field_access(int offset) const;
  oop obj_field(int offset) const;
  void obj_field_put(int offset, oop value);
  void obj_field_put_raw(int offset, oop value);
  void obj_field_put_volatile(int offset, oop value);

  Metadata* metadata_field(int offset) const;
  Metadata* metadata_field_raw(int offset) const;
  void metadata_field_put(int offset, Metadata* value);

  Metadata* metadata_field_acquire(int offset) const;
  void release_metadata_field_put(int offset, Metadata* value);
  //基础类型
  jbyte byte_field(int offset) const;
  void byte_field_put(int offset, jbyte contents);

  jchar char_field(int offset) const;
  void char_field_put(int offset, jchar contents);

  jboolean bool_field(int offset) const;
  void bool_field_put(int offset, jboolean contents);

  jint int_field(int offset) const;
  jint int_field_raw(int offset) const;
  void int_field_put(int offset, jint contents);

  jshort short_field(int offset) const;
  void short_field_put(int offset, jshort contents);
  ...

每个field在oop中都有一个对应的偏移量(offset),oop通过该偏移量得到该field的地址,再根据地址得到具体数据。因此,Java对象中的field存储的并不是对象本身,而是对象的地址

Klass结构

Klass向JVM提供两个功能,如下为HotSpot源码中对Klass的功能介绍:

A Klass provides:
 1: language level class object (method dictionary etc.)
 2: provide vm dispatch behavior for the object
Both functions are combined into one C++ class.

一个是实现语言层面的Java类,另外实现对象的虚分派(virtual dispatch)。所谓的虚分派,是JVM用来实现多态的一种机制。

跟OOP一样,Klass也有一个继承体系:

//Klass继承体系的最高父类
class  Klass;
//在虚拟机层面描述一个Java类
class   instanceKlass;
//专有instantKlass,表示java.lang.Class的Klass
class     instanceMirrorKlass;
//专有instantKlass,表示java.lang.ref.Reference的子类的Klass
class     instanceRefKlass;
//表示methodOop的Klass
class   methodKlass;
//表示constMethodOop的Klass
class   constMethodKlass;
//表示methodDataOop的Klass
class   methodDataKlass;
// 普通对象的数组类
class     ObjArrayKlass;
//基础类型的数组类
class     TypeArrayKlass;

不同于Oop,Klass在InstanceKlass下又设计了3个子类,其中InstanceMirrorKlass用于表示java.lang.Class类型,该类型对应的oop特别之处在于其包含了static field,因此计算oop大小时需要把static field也考虑进来;InstanceClassLoaderKlass主要提供了遍历当前ClassLoader的继承体系;InstanceRefKlass用于表示java.lang.ref.Reference及其子类。

Klass对象信息

Klass的主要用途之一就是保存一个Java对象的类型信息,一下是Klass主要信息:

class Klass : public Metadata {
 protected:
  ...省略

  // 类名[Ljava/lang/String;, etc.  Set to zero for all other kinds of classes.
  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
  // 当前类所属的java/lang/Class对象对应的 InstanceMirrorKlass
  OopHandle _java_mirror;
  // 类的直接父类
  Klass*      _super;
  // First subclass (NULL if none); _subklass->next_sibling() is next one
  // 第一个子类 (NULL if none); _subklass->next_sibling() 为下一个
  Klass* volatile _subklass;
  // Sibling link (or NULL); links all subklasses of a klass
  Klass* volatile _next_sibling;

  // All klasses loaded by a class loader are chained through these links
   // 串联起被同一个ClassLoader加载的所有类(包括当前类)
  Klass*      _next_link;
  // The VM's representation of the ClassLoader used to load this class.
  // Provide access the corresponding instance java.lang.ClassLoader.
  // 对应用于加载当前类的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.

  JFR_ONLY(DEFINE_TRACE_ID_FIELD;)

  // 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;

  // vtable length
  int _vtable_len;
...
}

可以看到Klass继承自Metadata,Metadata是的“元空间”(Metaspace)的实现,这也意味着Java对象的类型信息存储在方法区,而不是在堆中。其中包含了一个类的基本信息,接下来看下表示普通对象类型的InstanceKlass所包含的信息,它继承自Klass,在父类的基础上增加了不少信息:

class InstanceKlass: public Klass {
 ...
 public:
  InstanceKlass() { assert(DumpSharedSpaces || UseSharedSpaces, "only for CDS"); }
  // 当前类的状态
  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
  };

 private:
  static InstanceKlass* allocate_instance_klass(const ClassFileParser& parser, TRAPS);

 protected:
  //当前类的注解
  Annotations*    _annotations;
  // 定义的package
  PackageEntry*   _package_entry;
  //当前类数组中持有的类型
  Klass* volatile _array_klasses;
  //当前类的常量池
  ConstantPool* _constants;
  //当前类的内部类信息
  Array<jushort>* _inner_classes;
  //类信息索引
  Array<jushort>* _nest_members;
  ...
  // Method array.
  Array<Method*>* _methods;
  // 接口中默认方法
  Array<Method*>* _default_methods;
  // 这个类在本地声明要实现的接口
  Array<InstanceKlass*>* _local_interfaces;
  // Interfaces (InstanceKlass*s) this class implements transitively.
  Array<InstanceKlass*>* _transitive_interfaces;
  // 保存当前类所有方法的位置信息
  Array<int>*     _method_ordering;
  // 保存当前类所有default方法在虚函数表中的位置信息
  Array<int>*     _default_vtable_indices;
  //保存当前类的field信息(包括static field),数组结构为:
  // f1: [access, name index, sig index, initial value index, low_offset, high_offset]
  // f2: [access, name index, sig index, initial value index, low_offset, high_offset]
  //      ...
  // fn: [access, name index, sig index, initial value index, low_offset, high_offset]
  Array<u2>*      _fields;
  ...
}

可以看到在InstanceKlass记录了类的注解、方法、属性等信息,其中_fields中的每个元素都包含了当前field都偏移量信息,用于在oop中找到对应field的地址。

虚函数表(vtable)

虚函数表(vtable)主要是为了实现Java中的虚分派功能而存在。HotSpot把Java中的方法都抽象成了Method对象,InstanceKlass中的成员属性_methods就保存了当前类所有方法对应的Method实例。HotSpot并没有显式地把虚函数表设计为Klass的field,而是提供了一个虚函数表视图,并在类初始化时创建出来。

虚函数定义在Klass

 // vtables
  klassVtable vtable() const;
  int vtable_length() const { return _vtable_len; }

一个klassVtable可看成是由多个vtableEntry组成的数组,其中每个元素vtableEntry里面都包含了一个方法的地址信息,实现如下:

class vtableEntry {
 public:
  // size in words
  static int size()          { return sizeof(vtableEntry) / wordSize; }
  static int size_in_bytes() { return sizeof(vtableEntry); }

  static int method_offset_in_bytes() { return offset_of(vtableEntry, _method); }
  Method* method() const    { return _method; }
  Method** method_addr()    { return &_method; }

 private:
  Method* _method;
  void set(Method* method)  { assert(method != NULL, "use clear"); _method = method; }
  void clear()                { _method = NULL; }
  void print()                                        PRODUCT_RETURN;
  void verify(klassVtable* vt, outputStream* st);

  friend class klassVtable;
};

在进行虚分派时,JVM会根据方法在klassVtable中的索引,找到对应的vtableEntry,进而得到方法的实际地址,最后根据该地址找到方法的字节码并执行。

Method* Klass::method_at_vtable(int index)  {
#ifndef PRODUCT
  assert(index >= 0, "valid vtable index");
  if (DebugVtables) {
    verify_vtable_index(index);
  }
#endif
  return start_of_vtable()[index].method();
}

总结

每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个instanceKlass,保存在元空间,用来在JVM层表示该Java类。当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这个对象中包含了两部分信息,对象头以及元数据。对象头中有一些运行时数据,其中就包括和多线程相关的锁的信息。元数据其实维护的是指针,指向的是对象所属的类的instanceKlass