从JVM层面看创建一个java对象(oop-kclass框架)

本文是由作者收集网上的资料学习总结的一点感悟,可能不完全正确,有不对的地方欢迎指出交流。
我们知道,java语言是通过C++实现的,JVM也基本是由C++编写实现的。那我们在new一个对象的时候,在jvm层面,为我们做了哪些事情呢,相信我们已经很熟悉了,先是类加载检查导入所需要的类文件,接着是内存分配初始化变量等。具体的类加载和类实例可以参考这两篇文章。
类加载 对象实例化
但是我好奇的是java语言既然是用C++实现的,在jvm中,一个java对象是如何用C++语言表示的。在开始本文之前,先复习一下C++的一些语言特性。
C++可以继承多个父类,并且在子类中可以重写父类的重名函数,也可以通过父类的指针去调用父类的重名函数。如下面的调用方式:

#include<iostream> 
using namespace std;

class BaseA {
public:
    BaseA() {
    }
    ~BaseA() {
    }
    void common() {
        cout << "BaseA called" << endl;
    }
};

class BaseB {
public:
    BaseB() {
    }
    ~BaseB() {
    }
    void common() {
        cout << "BaseB called" << endl;
    }
};

class DerivedA : public BaseA, public BaseB{
public:
    DerivedA() {
    }
    ~DerivedA() {
    }
    void common() {
        cout << "DerivedA called" << endl;
    }
};

int main()
{
    DerivedA derivedA;
    BaseA* baseA = (BaseA*)&derivedA;
    BaseB* baseB = (BaseB*)&derivedA;
    derivedA.common();  // DerivedA called
    derivedA.BaseA::common(); // BaseA called
    derivedA.BaseB::common(); // BaseB called
    baseA->common();  // BaseA called
    baseB->common();  // BaseB called
    return 1;
}
  1. 如果类B是类A的子类,则在类B的成员方法中调用类A的方法时,可以直接以 A::method(paramlist); 来调用。

  2. 若子类B重写(overwrite)了父类A的同名方法,则类A的实例调用的是类A的方法,类B的实例调用的是类B的方法;将类B实例的指针指向类A的指针变量ptr,则通过ptr调用的是类A的方法;

  3. 若定义了类B的实例B1,则通过B1调用类A的方法的用法为: B1.A::method(paramlist);

C++中还有virtual关键字,对于声明为virtual的方法的类称为虚类,在上面的代码中将父类的同名函数都加上virtual关键字后观测输出结果。

#include<iostream> 
using namespace std;

class BaseA {
public:
    BaseA() {
    }
    ~BaseA() {
    }
    virtual void common() {
        cout << "BaseA called" << endl;
    }
};

class BaseB {
public:
    BaseB() {
    }
    ~BaseB() {
    }
    virtual void common() {
        cout << "BaseB called" << endl;
    }
};

class DerivedA : public BaseA, public BaseB{
public:
    DerivedA() {
    }
    ~DerivedA() {
    }
    void common() {
        cout << "DerivedA called" << endl;
    }
};

int main()
{
    DerivedA derivedA;
    BaseA* baseA = (BaseA*)&derivedA;
    BaseB* baseB = (BaseB*)&derivedA;
    derivedA.common();  // DerivedA called
    derivedA.BaseA::common(); // BaseA called
    derivedA.BaseB::common(); // BaseB called
    baseA->common();  // DerivedA called
    baseB->common();  // DerivedA called
    return 1;
}

virtual关键字是C++实现多态的一个重要环节。子类在继承时会生成一个虚函数列表,继承的父类中有几个虚类就创建几个虚函数列表,如果子类也声明了虚函数,会和第一个虚函数列表合并(可能因不同的系统平台而不同),子类覆写父类的方法会覆盖虚函数列表中的方法,如下图:


但是java中我们知道,并没有C++中的那些复杂的继承方式,也没有virtual关键字。Java仅支持单向继承,且任何子类都可以覆写父类的方法,并且调用的时候也不会产生问题,这是因为Java中的每一个方法都默认是virtual的。在JVM内存用到了oop-klass模型来描叙对应的类及对象:oop(ordinary object ponter,普通对象指针),kclass

  • OOP 英文全程是Ordinary Object Pointe,即普通对象指针,看起来像个指针实际上是藏在指针里的对象,表示对象的实例信息。
  • Klass 保存了Java类的变量和方法,如类变量,成员变量和方法这些,描述了Java类中的元数据和虚函数列表,这些通过引用的方式保存在oop实例中

一个Java对象的表示分为对象头,实例数据,和填充对其数据(无实际作用,只是为了凑字节)。


对象头包含markword和元数据,元数据就是一个指向kclass的一个指针,描述了类的一些元数据信息。oopDesc类大的主要定义如下:

class oopDesc {
  friend class VMStructs;
  private:
      volatile markOop  _mark; // 对象头
      union _metadata {
        wideKlassOop    _klass;//普通指针
        narrowOop       _compressed_klass;//压缩类指针
      } _metadata;
      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;
      address*  address_field_addr(int offset) const;
}
class instanceOopDesc : public oopDesc {
}
 
class arrayOopDesc : public oopDesc {
}

在hotspot/share/oops/oopsHierarchy.hpp 文件中,对oop的定义如下:

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

Java提供了多种对象类型的oop,当对象为数组时,在对象头中除了markword和kclass指针,还会有一个记录数组长度的字段。

_mark用于存储对象的运行时记录信息,如哈希值、GC 分代年龄(Age)、锁状态标志(偏向锁、轻量级锁、重量级锁)、线程持有的锁、偏向线程 ID、偏向时间戳等。是一个32位或者64位的一个数据,不同位置用于记录不同的信息。

从源代码中可以看到使用了volatile关键字修饰,也是通过此,实现了使用synchronized锁时不同线程在获取锁的时候能够第一时间感知到其他线程对markword的修改。

下面是一个64位的标记结果图。



多个对象的ModelA和ModelB的定义如下:

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);
   }
}


下面是分配情况:


对象的定位访问方式实际上分为两种:句柄访问和直接指针访问,上图就是直接指针访问。直接指针访问的优点就是处理速度快,节省了一次指针定位的时间开销。句柄访问方式,需要在堆中开辟一个句柄池,栈中的reference存储的就是句柄地址,句柄包含了对象实例数据和类型数据各自的地址。句柄访问方式的优点是对象被移动时,只需要修改句柄的数据地址即可,操作简单。

其中instanceOopDesc的定义如下:

// 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() {
// offset computation code breaks if UseCompressedClassPointers
// only is true
return (UseCompressedOops && UseCompressedClassPointers) ?
klass_gap_offset_in_bytes() :
sizeof(instanceOopDesc);
}
};

其中base_offset_in_bytes方法用于返回instanceOopDesc自身属性(即对象头)的内存的偏移量,即该偏移量之后的内存用于保存Java对象实例属性。
可以从instanceOopDesc定义中,基本类型字段的实现都是在instanceOopDesc的地址的基础上加上一个偏移量算出该字段的地址,偏移量的单位是字节,各字段的偏移量和初始值等属性都保存在InstanceKlass的_fields属性中,根据该地址可以直接获取或者设置字段值

_klass指针指向InstanceKlass的结构体系如下所示:

// hotspot/src/share/vm/oops/oopsHierarchy.hpp
...
class Klass;  // Klass继承体系的最高父类
class   InstanceKlass;  // 表示一个Java普通类,包含了一个类运行时的所有信息
class     InstanceMirrorKlass;  // 表示java.lang.Class
class     InstanceClassLoaderKlass; // 主要用于遍历ClassLoader继承体系
class     InstanceRefKlass;  // 表示java.lang.ref.Reference及其子类
class   ArrayKlass;  // 表示一个Java数组类
class     ObjArrayKlass;  // 普通对象的数组类
class     TypeArrayKlass;  // 基础类型的数组类
...

kclass继承自元数据描述了一个类的所有元数据信息,如下选出其中一些比较重要的field。

// hotspot/src/share/vm/oops/klass.hpp
class Klass : public Metadata {
...
  // 类名,其中普通类名和数组类名略有不同
  // 普通类名如:java/lang/String,数组类名如:[Ljava/lang/String;
  Symbol*     _name;
  // 最后一个secondary supertype
  Klass*      _secondary_super_cache;
  // 保存所有secondary supertypes
  Array<Klass*>* _secondary_supers;
  // 保存所有primary supertypes的有序列表
  Klass*      _primary_supers[_primary_super_limit];
  // 当前类所属的java/lang/Class对象对应的oop
  oop       _java_mirror;
  // 当前类的直接父类
  Klass*      _super;
  // 第一个子类 (NULL if none); _subklass->next_sibling() 为下一个
  Klass*      _subklass;
  // 串联起当前类所有的子类
  Klass*      _next_sibling;
  // 串联起被同一个ClassLoader加载的所有类(包括当前类)
  Klass*      _next_link;
  // 对应用于加载当前类的java.lang.ClassLoader对象
  ClassLoaderData* _class_loader_data;
  // 提供访问当前类的限定符途径, 主要用于Class.getModifiers()方法.
  jint        _modifier_flags;
  // 访问限定符
  AccessFlags _access_flags;    
...
}

表示普通对象类型的InstanceKlass所包含的信息,它继承自Klass,在父类的基础上增加了不少信息,如下列出较为重要的一些:

// hotspot/src/share/vm/oops/instanceKlass.hpp
class InstanceKlass: public Klass {
...
  // 当前类的状态
  enum ClassState {
    allocated,  // 已分配
    loaded,  // 已加载,并添加到类的继承体系中
    linked,  // 链接/验证完成
    being_initialized,  // 正在初始化
    fully_initialized,  // 初始化完成
    initialization_error  // 初始化失败
  };
  // 当前类的注解
  Annotations*    _annotations;
  // 当前类数组中持有的类型
  Klass*          _array_klasses;
  // 当前类的常量池
  ConstantPool* _constants;
  // 当前类的内部类信息
  Array<jushort>* _inner_classes;
  // 保存当前类的所有方法.
  Array<Method*>* _methods;
  // 如果当前类实现了接口,则保存该接口的default方法
  Array<Method*>* _default_methods;
  // 保存当前类所有方法的位置信息
  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]
  //     [generic signature index]
  //     [generic signature index]
  //     ...
  Array<u2>*      _fields;
...
}

从中可以看到一个类的基本信息都有包括进来存储。我们知道C++中的继承多态是通过虚函数表(vtable)实现的,Java默认所有的函数都是虚的,所以_methods数组中国存放了一个类的所有函数方法,是提供了一个虚函数表视图,并在类初始化时创建出来。

// hotspot/src/share/vm/oops/instanceKlass.hpp
class InstanceKlass: public Klass {
...  
  // 返回一个新的vtable,在类初始化时创建
  klassVtable* vtable() const;
  inline Method* method_at_vtable(int index);
..
}
// 以下为方法对应实现
// hotspot/src/share/vm/oops/instanceKlass.cpp
...
// vtable()的实现
klassVtable* InstanceKlass::vtable() const {
  return new klassVtable(this, start_of_vtable(), vtable_length() / vtableEntry::size());
}
// method_at_vtable()的实现
inline Method* InstanceKlass::method_at_vtable(int index)  {
  ... // 校验逻辑
  vtableEntry* ve = (vtableEntry*)start_of_vtable();
  return ve[index].method();
}

Java通过这种kclass的设计,将公共的类元信息提取保存在方法区中,每个对象只需要指针引用到KClass对象。避免了像C++那样每个对象都持有一个虚函数列表指针(具体的这样设计原因还不清楚)。

此外,除了OOP-kclass,还有一个handle体系,对其进行了封装,handle对象中有指针指向oop实例,oop中也有指针指向kclass对象。这样通过一个handle对象可以获取到Java对象的所有数据。这样的好处是将oop交由GC管理,方便垃圾回收。


参考文献:
https://www.jianshu.com/p/ef1482bbd9e3
https://juejin.cn/post/6844904055421009928
https://blog.csdn.net/qq_25179481/article/details/114495140

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容