HotspotVM的OOP模型

本文主要详细介绍Klass/Class/Oop之间的关系,从HotSpotVM的Cpp源码层面去进行相关的方法解析,还会介绍方法区的动态性的原理(运行时动态生成和加载字节码į

本文主要详细介绍Klass/Class/Oop之间的关系,从HotSpotVM的Cpp源码层面去进行相关的方法解析,还会介绍方法区的动态性的原理(运行时动态生成和加载字节码的原理)。

ps:本文中的环境使用的是OpenJDK11

1. 一起去了解HotSpotVM中的Oop模型吧!

我们以下面的代码(Main.java),去进行举例,从而去完整介绍整个Java的对象模型。

public class Main {
    public static void main(String[] args) {
        Object obj = new Object();
    }
}

1.1 编译java代码成为字节码文件(Main.class)

使用javac编译器去对Main.java去进行编译,生成一个Main.class这样一个字节码文件。

1.2 HotSpotVM进行类加载

HotSpotVM启动时,使用类加载子系统对Main类以及java类库中的一些类去进行加载。在加载时,会根据.class字节码文件中类信息保存到instanceKlass对象中(我们一般可以称之为类模板对象)、常量池信息保存到运行时常量池中,这些信息都会被存放到方法区(jdk1.8之后的版本的JDK的实现称为元空间,之前的版本称之为永久代,下面使用方法区去进行描述)中。

实际上,针对于每个字节码文件中的类,它还会生成一个Class(java/lang/Class)对象放到堆空间中,并且instanceKlass对象中还有一个字段java_mirror指向了堆中的Class对象。

HotSpotVM并不直接将instanceKlass对象暴露给java代码中去进行使用,而是暴露了一个mirror(镜像)给java程序去进行使用,这个mirror也就是java/lang/Class对象(本质上也是个oopDesc,但是它比较特殊,下面都使用"Class对象"去进行描述)。

Class对象,是不是很熟悉?其实就是我们反射中经常使用到的那个Class对象呀,HotSpotVM提供了一个Class对象给我们去访问方法区中的内容!我们在java代码层面,可以通过类似如下的几种方式去获取到Class对象。

        Object obj = new Object();
        final Class<?> cl1 = Class.forName("java.lang.Object");
        final Class<?> cl2 = Object.class;
        final Class<?> cl3 = obj.getClass();

1.3 java对象的JOL内存布局

当我们使用Object obj = new Object()这样的java代码去创建一个Object类型的对象时,会在堆中分配一个Object对象空间,并且在JVM虚拟机栈(简称VM栈)中创建一个引用obj,分配到栈帧的**局部变量表(Local Variables)**中的某个槽位中。

而一个java对象在堆中存储的内存布局,称为JOL(Java Object Layout),存储的方式也就是下面这样的一个图

244123526726ba209788cf9d.png

一个java对象主要组成部分:

  • 1.markword,在32bitVM中是4个字节,64bitVM中是8个字节,现在都是64bit虚拟机多,一般都是8个字节。
  • 2.类型指针,其实应该是klass指针,它就是指向了方法区中的instanceKlass对象。有4字节/8字节两种,取决于开不开启压缩,现在一般的VM默认都是开启压缩的,也就是一般都是4个字节。
  • 3.数组长度,占用4个字节,我们也可以知道java中数组的最大长度只有Integer.MAX_VALUE,底层规定死的只有4个字节,只有数组才有该组成部分。
  • 4.实例数据,主要存放的就是对象的成员变量。
  • 5.padding,主要用来做4/8字节填充,比如之前几个部分的长度只有22个字节,就得填充成为4/8的整数倍,需要使用padding去填充两个字节。(4字节填充还是8字节填充,取决于操作系统,以及使用的编译器以及配置的相关参数,默认32bit系统是4字节填充,64bit系统下是8字节填充,当然也可以通过参数去进行修改,下面会有提到)

Java对象的存储,其实就是采用如下的这样一个Cpp层面的oopDesc对象去进行存储的(一个java对象对应一个oopDesc),在HotSpotVM中的实现也就是下面这样的一个类oopDesc(src/hotspot/share/oops/oop.hpp):

class oopDesc {
  friend class VMStructs;
  friend class JVMCIVMStructs;
 private:
  volatile markOop _mark;  //markOop,也就是markword对象指针
  union _metadata {  //元信息,联合体,Klass指针或者是压缩指针,Klass也就是指向方法区中的类的元信息的对象
    Klass*      _klass;   //Klass为Klass类,可以开启非压缩指针,这样就占用8B去存放
    narrowKlass _compressed_klass;   //使用uint去存放...默认情况下都会使用压缩指针去存放以节省空间,只占有4B
  } _metadata;
}

而OopDesc的子类有instanceOopDescarrayOopDesc这两个,它们分别用来描述普通类型的对象、数组类型的对象的对象指针。

1.4 从HotSpotVM源码中去对instanceOopDesc去进行解析

我们来看instanceOopDesc这个子类的实现

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

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

1.4.1 对C语言中的对齐填充原理的介绍

这里我需要对C语言中对齐填充的概念进行说明一下,在32位系统下是采用4字节填充,64位系统下是8字节填充。

#include "stdio.h"
struct data
{
    long data;
    int data0;
};
int main()
{
    printf("%lu", sizeof(struct data));
}

比如上面的C语言代码在32位系统下的输出为12,在64位系统下的输出为16,就是因为它们的对齐方式不同,64位系统下long的长度+int的长度=12,不足8的倍数需要填充4个字节凑够8的倍数。我们有什么办法去修改多少字节对齐吗?当然是有的。看下面的代码

#include "stdio.h"
#pragma pack(4)
struct data
{
    long data;
    int data0;
};
int main()
{
    printf("%lu", sizeof(struct data));
}

使用上面的代码中的#pragma pack(4),就将其改为了4字节对齐,输出的结果就是12,而不是之前的16。关于padding,其实还有很多说的,比如你们可以猜猜下面的结构体使用8字节对齐占用多少个字节?(答案是32字节,不感兴趣的跳过即可,感兴趣的可以看看解析或者自己试试?)

struct p
{
    char c;
    int d;
    short i;
    double f;
    char g;
};

简单解析一下吧(可能不太直观,读者可以自行画图理解,这里画图太麻烦,暂时忽略掉它吧):

首先需要进行说明的是:在64bit系统下,char类型的变量需要满足地址能被1整除,int类型的变量需要满足地址能被4整除,short类型的变量需要满足地址能被2整除,double/long类型的变量需要满足地址能被8整除;最终整个结构体的占用的空间要能被8整除。

我们把内存看做是一个大的byte[],这样变量c(char)被放到了byte[0]处,但是变量d(int)需要内存地址能被4整除,因此放到了byte[4…7],这样byte[1…3]就变成了padding,变量i(short)需要能被2整除,因此被放到了byte[8…9]上,变量f(double)需要地址能被8整除,因此它会被放到byte[16…23]上,byte[10…15]就变成了padding,变量g(char)被放到了byte[24]上,到这里已经结束了,你是不是以为就只占用25B?上面有说这样一条原则,就是整个结构体占用的空间要能被8整除,也就是做8字节填充,因此byte[25…31]都成为了padding,最终占用的空间就是32B。

1.4.2 对HotSpotVM中HeapWord和HeapWordSize的说明

在HotSpotVM中,将内存的地址的访问抽象成为了HeapWord,我们通过源码查看HeapWord的定义

class HeapWord {
  friend class VMStructs;
 private:
  char* i;
 public:
  char* value() { return i; }
};

其实就是包装了一个char*的指针,代表的是一个内存地址。VM提供了相应的方法申请VM内存,这些方法分配内存之后返回的都是一个**HeapWord*对象,表示的是VM中的内存地址**。

而HeapWordSize呢?它是做什么的?我们找到它的定义

const int HeapWordSize        = sizeof(HeapWord);

我们发现它其实就是计算HeapWord占用的空间大小,计算来干什么?其实它计算的就是当前平台下的指针变量占用多少个字节char*其实就是一个普通的指针变量,在32位系统下指针变量占用的长度也就是4个字节,在64位系统下占用的长度也就是8个字节。也就是说HeapWordSize在32位系统下为4,64位系统下为8

1.4.3 进一步了解instanceOopDesc中的相关方法

在完成了对字节填充的概念之后,你应该能更好地理解对象的oop模型,下面我们可以看来instanceOopDesc类提供的相关方法。

比如instanceOopDesc中的下面这个header_size方法:

static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; }

我们事先说明:下面的所有介绍,都是在现在普遍的64位系统的情况下

我们根据刚刚的字节填充的概念,我们知道sizeof(instanceOopDesc)在8字节对齐的情况下为16(不管开没开启压缩指针),HeapWordSize为8,因此计算出来的header_size也就是2。

下面来看base_offset_in_bytes方法

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

我们可以看到,它其实就是判断是否进行过Klass指针压缩,从而进行不同的判断,因为如果进行过Klass指针的压缩,对象的字段(Field)就可能出现不是8字节对齐的情况,只占用了12B,因为8字节填充那么就有4字节会被白白浪费掉,压缩指针不是白压缩了吗?它的作用就是需要将浪费的4B字节都利用上来

如果没开启压缩指针,直接返回sizeof(instanceOopDesc),也就是16作为base_offset。如果开启了压缩指针,那么就需要计算出来markword的长度+压缩指针的长度,也就是12作为base_offset。那么我们可以猜到base_offset是做什么的?其实就是计算出来对象的Field的基地址相对于oop的基地址的偏移量,也就oop+base_offset=对象Field的基地址

再比如它的contains_field_offset方法,用来判断指定的offset是否包含在对象的字段所在的包含区域内,也是利用到了之前提到的base_offset_in_bytes方法。

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

从instanceOopDesc这个类中其实我们并没有在C++代码中看到它有存放该对象的Field,而是直接将其放到instanceOopDesc对象之后紧挨着,然后通过base_offset_in_bytes计算出来Field的基础偏移量,然后找到Field所处的基地址,这才是C/Cpp中指针的奥义,指针可以指万物!它不直接进行定义最终的目的嘛,其实就是为了更好利用内存罢了。

1.5 从HotSpotVM源码中去对arrayOopDesc去进行解析

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() {
    size_t hs = align_up(length_offset_in_bytes() + sizeof(int),
                              HeapWordSize);

  // Check whether an element of a typeArrayOop with the given type must be
  // aligned 0 mod 8.  The typeArrayOop itself must be aligned at least this
  // strongly.
  static bool element_type_should_be_aligned(BasicType type) {
    return type == T_DOUBLE || type == T_LONG;
  }

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

  // Returns the offset of the first element.
  static int base_offset_in_bytes(BasicType type) {
    return header_size(type) * HeapWordSize;
  }

  // Returns the address of the first element. The elements in the array will not
  // relocate from this address until a subsequent thread transition.
  inline void* base(BasicType type) const;
  inline void* base_raw(BasicType type) const; // GC barrier invariant

  template <typename T>
  static T* obj_offset_to_raw(arrayOop obj, size_t offset_in_bytes, T* raw) {
    if (obj != NULL) {
      assert(raw == NULL, "either raw or in-heap");
      char* base = reinterpret_cast<char*>((void*) obj);
      raw = reinterpret_cast<T*>(base + offset_in_bytes);
    } else {
      assert(raw != NULL, "either raw or in-heap");
    }
    return raw;
  }

  // Tells whether index is within bounds.
  bool is_within_bounds(int index) const        { return 0 <= index && index < length(); }

  // Accessors for instance variable which is not a C++ declared nonstatic
  // field.
  int length() const {
    return *(int*)(((intptr_t)this) + length_offset_in_bytes());
  }
  void set_length(int length) {
    set_length((HeapWord*)this, length);
  }
  static void set_length(HeapWord* mem, int length) {
    *(int*)(((char*)mem) + length_offset_in_bytes()) = length;
  }

  // Should only be called with constants as argument
  // (will not constant fold otherwise)
  // Returns the header size in words aligned to the requirements of the
  // array object type.
  static int header_size(BasicType type) {
    size_t typesize_in_bytes = header_size_in_bytes();
    return (int)(element_type_should_be_aligned(type)
      ? align_object_offset(typesize_in_bytes/HeapWordSize)
      : typesize_in_bytes/HeapWordSize);
  }

  // Return the maximum length of an array of BasicType.  The length can passed
  // to typeArrayOop::object_size(scale, length, header_size) without causing an
  // overflow. We also need to make sure that this will not overflow a size_t on
  // 32 bit platforms when we convert it to a byte size.
  static int32_t max_array_length(BasicType type) {
    assert(type >= 0 && type < T_CONFLICT, "wrong type");
    assert(type2aelembytes(type) != 0, "wrong type");

    const size_t max_element_words_per_size_t =
      align_down((SIZE_MAX/HeapWordSize - header_size(type)), MinObjAlignment);
    const size_t max_elements_per_size_t =
      HeapWordSize * max_element_words_per_size_t / type2aelembytes(type);
    if ((size_t)max_jint < max_elements_per_size_t) {
      // It should be ok to return max_jint here, but parts of the code
      // (CollectedHeap, Klass::oop_oop_iterate(), and more) uses an int for
      // passing around the size (in words) of an object. So, we need to avoid
      // overflowing an int when we add the header. See CRs 4718400 and 7110613.
      return align_down(max_jint - header_size(type), MinObjAlignment);
    }
    return (int32_t)max_elements_per_size_t;
  }

};

其实主要内容和instanceOopDesc很类似的,只不过对象是存储在16B(markword+klass+length)之后那段了而压缩指针剩余的4字节用来去存放数组的length罢了(在源码的注释也给我们解释了,length字段并没有在C++代码中去进行定义,因为有可能它会利用klass指针进行压缩而剩下的一半空间,如果去进行定义了,那么该部分就利用不上了)。

1.6 对Klass的源码去进行解析

Klass这个类中其实包括蛮多东西的,我们只研究一些重点的字段。(不重要的字段暂时忽略)

class Klass : public Metadata {
  friend class VMStructs;
  friend class JVMCIVMStructs;
   protected:
  // If you add a new field that points to any metaspace object, you
  // must add this field to Klass::metaspace_pointers_do().

  // note: put frequently-used fields together at start of klass structure
  // for better cache behavior (may not make much of a difference but sure won't hurt)
  enum { _primary_super_limit = 8 };

  // The "layout helper" is a combined descriptor of object layout.
  // For klasses which are neither instance nor array, the value is zero.
  //
  // For instances, layout helper is a positive number, the instance size.
  // This size is already passed through align_object_size and scaled to bytes.
  // The low order bit is set if instances of this class cannot be
  // allocated using the fastpath.
  //
  // For arrays, layout helper is a negative number, containing four
  // distinct bytes, as follows:
  //    MSB:[tag, hsz, ebt, log2(esz)]:LSB
  // where:
  //    tag is 0x80 if the elements are oops, 0xC0 if non-oops
  //    hsz is array header size in bytes (i.e., offset of first element)
  //    ebt is the BasicType of the elements
  //    esz is the element size in bytes
  // This packed word is arranged so as to be quickly unpacked by the
  // various fast paths that use the various subfields.
  //
  // The esz bits can be used directly by a SLL instruction, without masking.
  //
  // Note that the array-kind tag looks like 0x00 for instance klasses,
  // since their length in bytes is always less than 24Mb.
  //
  // Final note:  This comes first, immediately after C++ vtable,
  // because it is frequently queried.
  jint        _layout_helper;

  // Klass identifier used to implement devirtualized oop closure dispatching.
  const KlassID _id;  // Klass的唯一ID标识符

  // The fields _super_check_offset, _secondary_super_cache, _secondary_supers
  // and _primary_supers all help make fast subtype checks.  See big discussion
  // in doc/server_compiler/checktype.txt
  //
  // Where to look to observe a supertype (it is &_secondary_super_cache for
  // secondary supers, else is &_primary_supers[depth()].
  juint       _super_check_offset;

  // Class name.  Instance classes: java/lang/String, etc.  Array classes: [I,
  // [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
  OopHandle _java_mirror;  // java/lang/Class的实例镜像指针
  // 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.

  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,原始的markword
  jint     _biased_lock_revocation_count;
}

存放了当前类的子类、父类、类加载器镜像(java/lang/Class)的指针、访问修饰符、原型markword等信息。还有一个我们可以关注的点,它继承了Metadata类。

我们来研究它的子类instanceKlass?(字段太多了,下面暂时忽略部分,这里的话仅仅用来做相关的说明)

class InstanceKlass: public Klass {
  friend class VMStructs;
  friend class JVMCIVMStructs;
  friend class ClassFileParser;
  friend class CompileReplay;

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

 protected:
  // If you add a new field that points to any metaspace object, you
  // must add this field to InstanceKlass::metaspace_pointers_do().

  // Annotations for this class
  Annotations*    _annotations;  // 保存的是这个类上的注解列表
  // Package this class is defined in
  PackageEntry*   _package_entry; // 保存的是这个类所在的包名
  // Array classes holding elements of this class.
  Klass* volatile _array_klasses;  // 保存的是用来支持这个类的数组类的Klass
  // Constant pool for this class.
  ConstantPool* _constants;  // 当前这个类的常量池
  // The InnerClasses attribute and EnclosingMethod attribute. The
  // _inner_classes is an array of shorts. If the class has InnerClasses
  // attribute, then the _inner_classes array begins with 4-tuples of shorts
  // [inner_class_info_index, outer_class_info_index,
  // inner_name_index, inner_class_access_flags] for the InnerClasses
  // attribute. If the EnclosingMethod attribute exists, it occupies the
  // last two shorts [class_index, method_index] of the array. If only
  // the InnerClasses attribute exists, the _inner_classes array length is
  // number_of_inner_classes * 4. If the class has both InnerClasses
  // and EnclosingMethod attributes the _inner_classes array length is
  // number_of_inner_classes * 4 + enclosing_method_attribute_size.
  Array<jushort>* _inner_classes;

  // The NestMembers attribute. An array of shorts, where each is a
  // class info index for the class that is a nest member. This data
  // has not been validated.
  Array<jushort>* _nest_members;

  // The NestHost attribute. The class info index for the class
  // that is the nest-host of this class. This data has not been validated.
  jushort _nest_host_index;

  // Resolved nest-host klass: either true nest-host or self if we are not nested.
  // By always being set it makes nest-member access checks simpler.
  InstanceKlass* _nest_host;

  // the source debug extension for this klass, NULL if not specified.
  // Specified as UTF-8 string without terminating zero byte in the classfile,
  // it is stored in the instanceklass as a NULL-terminated UTF-8 string
  const char*     _source_debug_extension;
  // Array name derived from this class which needs unreferencing
  // if this class is unloaded.
  Symbol*         _array_name;

  // Number of heapOopSize words used by non-static fields in this klass
  // (including inherited fields but after header_size()).
  int             _nonstatic_field_size;  // 非static字段,包括在header_size之后的继承的父类的字段

  // static字段的数量,包括这个klass中的oop和非oop
  int             _static_field_size;    // number words used by static fields (oop and non-oop) in this klass
  // Constant pool index to the utf8 entry of the Generic signature,
  // or 0 if none.
  u2              _generic_signature_index;  // 泛型签名的索引,是一个指向常量池的索引
  // Constant pool index to the utf8 entry for the name of source file
  // containing this klass, 0 if not specified.
  u2              _source_file_name_index;  // 源文件的名字,是一个指向常量池的索引

  // static字段的数量,只包括这个klass中的oop字段
  u2              _static_oop_field_count;// number of static oop fields in this klass

  // java对象的字段的数量
  u2              _java_fields_count;    // The number of declared Java fields
  int             _nonstatic_oop_map_size;// size in words of nonstatic oop map blocks

  int             _itable_len;           // length of Java itable (in words)
  // _is_marked_dependent can be set concurrently, thus cannot be part of the
  // _misc_flags.
  bool            _is_marked_dependent;  // used for marking during flushing and deoptimization
  bool            _is_being_redefined;   // used for locking redefinition

  // The low two bits of _misc_flags contains the kind field.
  // This can be used to quickly discriminate among the four kinds of
  // InstanceKlass.

  static const unsigned _misc_kind_field_size = 2;
  static const unsigned _misc_kind_field_pos  = 0;
  static const unsigned _misc_kind_field_mask = (1u << _misc_kind_field_size) - 1u;

  static const unsigned _misc_kind_other        = 0; // concrete InstanceKlass
  static const unsigned _misc_kind_reference    = 1; // InstanceRefKlass
  static const unsigned _misc_kind_class_loader = 2; // InstanceClassLoaderKlass
  static const unsigned _misc_kind_mirror       = 3; // InstanceMirrorKlass

  // Start after _misc_kind field.
  enum {
    _misc_rewritten                           = 1 << 2,  // methods rewritten.
    _misc_has_nonstatic_fields                = 1 << 3,  // for sizing with UseCompressedOops
    _misc_should_verify_class                 = 1 << 4,  // allow caching of preverification
    _misc_is_anonymous                        = 1 << 5,  // has embedded _host_klass field
    _misc_is_contended                        = 1 << 6,  // marked with contended annotation
    _misc_has_nonstatic_concrete_methods      = 1 << 7,  // class/superclass/implemented interfaces has non-static, concrete methods
    _misc_declares_nonstatic_concrete_methods = 1 << 8,  // directly declares non-static, concrete methods
    _misc_has_been_redefined                  = 1 << 9,  // class has been redefined
    _misc_has_passed_fingerprint_check        = 1 << 10, // when this class was loaded, the fingerprint computed from its
                                                         // code source was found to be matching the value recorded by AOT.
    _misc_is_scratch_class                    = 1 << 11, // class is the redefined scratch class
    _misc_is_shared_boot_class                = 1 << 12, // defining class loader is boot class loader
    _misc_is_shared_platform_class            = 1 << 13, // defining class loader is platform class loader
    _misc_is_shared_app_class                 = 1 << 14, // defining class loader is app class loader
    _misc_has_resolved_methods                = 1 << 15  // resolved methods table entries added for this class
  };

  u2              _misc_flags;
  u2              _minor_version;        // minor version number of class file
  u2              _major_version;        // major version number of class file
  Thread*         _init_thread;          // Pointer to current thread doing initialization (to handle recusive initialization)
  OopMapCache*    volatile _oop_map_cache;   // OopMapCache for all methods in the klass (allocated lazily)
  JNIid*          _jni_ids;              // First JNI identifier for static fields in this class
  jmethodID*      volatile _methods_jmethod_ids;  // jmethodIDs corresponding to method_idnum, or NULL if none
  intptr_t        _dep_context;          // packed DependencyContext structure
  nmethod*        _osr_nmethods_head;    // Head of list of on-stack replacement nmethods for this class

  volatile u2     _idnum_allocated_count;         // JNI/JVMTI: increments with the addition of methods, old ids don't change

  // Class states are defined as ClassState (see above).
  // Place the _init_state here to utilize the unused 2-byte after
  // _idnum_allocated_count.
  u1              _init_state;                    // state of class,类的加载状态,是一个枚举类型,定义在上面了
  u1              _reference_type;                // reference type,引用类型(强软弱虚)

  u2              _this_class_index;              // constant pool entry,指向常量池中class的索引

  // Method array.
  Array<Method*>* _methods;  // klass的方法数组
  // Default Method Array, concrete methods inherited from interfaces
  Array<Method*>* _default_methods;  // 继承自接口的default方法数组
  // Interface (Klass*s) this class declares locally to implement.
  Array<Klass*>* _local_interfaces;  // 本地实现的接口
  // Interface (Klass*s) this class implements transitively.
  Array<Klass*>* _transitive_interfaces;  // 这个类实现的过渡接口
  // Int array containing the original order of method in the class file (for JVMTI).
  Array<int>*     _method_ordering;
  // Int array containing the vtable_indices for default_methods
  // offset matches _default_methods offset
  Array<int>*     _default_vtable_indices;

  // Instance and static variable information, starts with 6-tuples of shorts
  // [access, name index, sig index, initval index, low_offset, high_offset]
  // for all fields, followed by the generic signature data at the end of
  // the array. Only fields with generic signature attributes have the generic
  // signature data set in the array. The fields array looks like following:
  //
  // 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; 
}

在instanceKlass中存放了类的注解信息、包名、常量池信息、内部类数组、内部成员、方法数组、字段数组等相关信息。

还有一个字段是值得我们去关注的,那就是_init_state,它定义为一个枚举类型ClassState,它记录的是当前类的加载状态,也就是用来说明我们的当前类究竟处于哪个阶段了?这就回到了类加载的那几个阶段:类的加载(Loading)、链接(Linking)、初始化(Initialization)。在一个类加载到HotSpotVM中,并不会立刻完成三个步骤,初始化环节需要在一个类被主动使用时才会执行。

  enum ClassState {
    allocated,  // 已分配
    loaded,  // 已加载,并添加到类的继承体系中
    linked,  // 链接/验证完成
    being_initialized,  // 正在初始化
    fully_initialized,  // 初始化完成
    initialization_error  // 初始化失败
}
  • 1.类的加载(Loading)阶段,也就是从磁盘/网络/jar包等地方获取字节码文件。
  • 2.类的链接(Linking)阶段,主要包括:验证、准备、解析三个步骤。
    • 2.1 验证主要是对字节码文件去进行验证,判断字节码是否符合当前的VM的要求。
    • 2.2 准备阶段主要给类变量(static变量)赋给0值(8种基础类型就是0,对象类型就是null),以及给一些常量去进行赋真实值(不需要new的常量,比如public static final String str = "wanna")。
    • 2.3 解析阶段主要是将常量池中的符号引用转换成为直接引用。(ps:其实这步应该是在初始化完成之后再执行)
  • 3.初始化(Initialization)阶段主要是执行类的构造器方法(<clinit>),<clinit>是javac在编译时收集所有的类变量的赋值操作和静态代码块整合完成的一个方法。(需要new的常量就会在这里完成,比如public static final String str = new String("wanna")public static final Object obj = new Object()会在这里完成赋值)

我们可以发现其实instanceKlass就是HotSpotVM的类加载器在进行类加载时,每个字节码文件就会映射并生成这样的一个instanceKlass对象放到方法区中,而instanceKlass中就存放了字节码文件中的相关信息。我们在字节码文件中看到的常量池,会被HotSpotVM进行加载,放入**运行时常量池(ConstantPool)**中,在instanceKlass中也存放了相应的引用去指向运行时常量池。

常量池对应的类ConstantPool,它也实现了Metadata这个元数据类。根据上面的信息,我们可以猜测,实现了Metadata的相关子类,都会是方法区中的成员,主要包括Klass、ConstantPool、Method等。还有一个点是很值得关注的,源码的注释中有这样的一句话:Constant pool for this class,也就是说在方法区中有很多个常量池,每个类会对应自己的一个专属常量池,在instanceKlass对象中很多字段存放的都是在常量池中的索引

1.7 针对Oop/Class/Klass做个小总结

最终形成的就是如下这样的一个结构,算是对整个java对象的模型的形象描述。

24412352733e6d4feb739d01.png

补充:

  • 1.实际上java/lang/Class对象也是使用oopDesc去进行表示的,也就是它和我们普通的java对象本质上来说是没什么区别的。
  • 2.实际上Klass的镜像java/lang/Class对象中也有保存Klass的指针(双向引用),这里暂时忽略。

1.8 HotSpotVM中方法区的动态性

HotSpotVM中方法区中的内容并不是恒定不变的,它完全是动态的,也就是说在运行时还完全使用类加载器去加载相关的字节码信息进入方法区。有什么作用?

最典型的应用就是去完成运行时动态代理,相信使用过Spring Framework这个框架的小伙伴们应该是比较熟悉SpringAOP的动态代理的(包括JDK的动态搭理、CGLIB的动态代理甚至是AspectJ的动态代理)。

1.8.1 了解JDK类库中的defineClass方法

在JDK类库的ClassLoader类中有下面这样的native方法defineClass,主要作用就是交给VM,让它往方法区中加载一个instanceKlass对象并创建相应的镜像mirror对象。

    static native Class<?> defineClass1(ClassLoader loader, String name, byte[] b, int off, int len,
                                        ProtectionDomain pd, String source);

    static native Class<?> defineClass2(ClassLoader loader, String name, java.nio.ByteBuffer b,
                                        int off, int len, ProtectionDomain pd,
                                        String source);

在大名鼎鼎的Unsafe类中也提供的相关defineClass的方法去供我们使用

    public native Class<?> defineClass0(String name, byte[] b, int off, int len,
                                        ClassLoader loader,
                                        ProtectionDomain protectionDomain);

1.8.2 了解ASM框架动态生成字节码的基本原理

我们下面使用一个最典型的方式去直接动态加载字节码。我们使用到一个框架ASM,熟悉C/Cpp的朋友应该从名字来看我们就知道是和汇编相关的,没错,这个框架就是和汇编相关的,这是一个字节码(java的汇编)生成的框架,感兴趣的小伙伴可以自行学习ASM框架相关的更多知识。

我这里需要完成的任务是,我想要生成一个com.wanna.asm.HelloWorld类,然后去实现它的toString方法。

既然使用到asm,就得导入相关的依赖(使用maven进行导入)

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <asm.version>9.0</asm.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.ow2.asm</groupId>
      <artifactId>asm</artifactId>
      <version>${asm.version}</version>
    </dependency>
    <dependency>
      <groupId>org.ow2.asm</groupId>
      <artifactId>asm-commons</artifactId>
      <version>${asm.version}</version>
    </dependency>
    <dependency>
      <groupId>org.ow2.asm</groupId>
      <artifactId>asm-util</artifactId>
      <version>${asm.version}</version>
    </dependency>
    <dependency>
      <groupId>org.ow2.asm</groupId>
      <artifactId>asm-tree</artifactId>
      <version>${asm.version}</version>
    </dependency>
    <dependency>
      <groupId>org.ow2.asm</groupId>
      <artifactId>asm-analysis</artifactId>
      <version>${asm.version}</version>
    </dependency>
  </dependencies>

我们编写如下的java代码,为了去生成我们的目标类:

public class HelloWorldDump implements Opcodes {

    public static byte[] dump() {
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        /**
         * 第一个参数设置字节码的版本
         * 第二个参数设置类的访问标识符,这里主要用到ACC_PUBLIC用来设置该类是公有的(public关键字),以及ACC_SUPER设置它是有父类的(extends关键字)
         * 第三个参数设置我们要访问的类的类全限定名为com/wanna/asm/HelloWorld
         * 第四个参数设置签名
         * 第五个参数设置父类的全限定名java/lang/Object
         * 第六个参数设置实现的接口为null
         */
        cw.visit(V1_8, ACC_PUBLIC | ACC_SUPER, "com/wanna/asm/HelloWorld", null, "java/lang/Object", null);

        //创建目标类的构造器方法
        {
            //创建构造器方法
            MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            //想要访问一个方法的code部分...
            mv1.visitCode();
            mv1.visitVarInsn(ALOAD, 0);
            //调用Object父类的构造器方法...需要使用到的是invoke special字节码
            mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
            //写一个return字节码表示返回空
            mv1.visitInsn(RETURN);
            mv1.visitMaxs(1, 1);
            mv1.visitEnd();
        }
        //创建目标类的toString方法
        {
            //访问HelloWorld的toString方法,并设置方法描述为()Ljava/lang/String;,也就是返回类型为String,并且参数为空
            MethodVisitor mv2 = cw.visitMethod(ACC_PUBLIC, "toString", "()Ljava/lang/String;", null, null);
            //想要访问方法的code部分...
            mv2.visitCode();
            //使用ldc字节码往栈中压入一个实例...
            mv2.visitLdcInsn("This is a HelloWorld object.");
            //写一个areturn字节码表示返回一个引用类型的对象...
            mv2.visitInsn(ARETURN);
            mv2.visitMaxs(1, 1);
            mv2.visitEnd();
        }
        cw.visitEnd();

        return cw.toByteArray();
    }
}

主要就提供了一个HelloWorldDump.dump方法,返回的是一个byte数组。但是仅仅这样做,JVM并不会加载我们的com.wanna.asm.HelloWorld类,我们还需要提供自己的类加载器。(为了简便,让它只能加载这个类,加载到其它类直接抛出异常)

public class MyClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        if ("com.wanna.asm.HelloWorld".equals(name)) {
            byte[] bytes = HelloWorldDump.dump();
            return defineClass(name, bytes, 0, bytes.length);
        }
        throw new ClassNotFoundException("Class Not Found: " + name);
    }
}

编写我们的测试代码,使用反射去拿到这个类的Class对象:

public class HelloWorldRun {
    public static void main(String[] args) throws Exception {
        MyClassLoader classLoader = new MyClassLoader();
        Class<?> clazz = classLoader.loadClass("com.wanna.asm.HelloWorld");
        Object instance = clazz.getDeclaredConstructor().newInstance();
        System.out.println(instance);
    }
}

得到下面的运行结果

This is a HelloWorld object.

也就是说,我们成功地将我们使用代码生成的com.wanna.asm.HelloWorld加载到VM中了,在方法区中已经有该类的元信息、在堆中已经有该类的Class对象。

出于好奇,我们就想看看HelloWorldDump生成的byte数组里面究竟是什么东东?我们直接使用输出流输出到文件中!

        final String filePath = "/Users/wanna/Desktop/Code/java/jdksrc/jdk_debug/src/main/java/com/wanna/asm";
        try (FileOutputStream fos = new FileOutputStream(filePath + "/" + "HelloWorld.class")) {
            fos.write(HelloWorldDump.dump());
        }

使用javap -v com.wanna.asm.HelloWorld命令看到如下内容:

image.png

其实我们生成的byte数组,就是字节码呀!而我们ASM框架的作用就是帮助我们生成这样一个的符合.class文件规范的byte数组,然后再交给类加载器去进行加载到VM当中罢了。

了解到这里之后,相信你已经开始了解VM的动态性了,我们再深一步,去看看JDK动态代理的源码怎么样?

1.8.3 了解JDK类库提供的Proxy.newInstance的方法源码

我们编写下面的测试代码

public class JDKProxy {
    public interface ProxyTest {
        public String getName();
    }

    static class ProxyTestImpl implements ProxyTest {
        @Override
        public String getName() {
            return "wanna";
        }
    }

    static class InvocationImpl<T> implements InvocationHandler {
        T target;

        public InvocationImpl(T target) {  //必须得把target传进来,不然没有目标对象的实现类调用不了...
            this.target = target;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Object ret = method.invoke(target, args);
            ret += "666";
            return ret;
        }
    }

    public static void main(String[] args) throws Exception {
        Object instance = Proxy.newProxyInstance(JDKProxy.class.getClassLoader(),
                new Class<?>[]{ProxyTest.class},
                new InvocationImpl<ProxyTest>(new ProxyTestImpl()));

        Method method = instance.getClass().getMethod("getName");
        Object ret = method.invoke(instance);
        System.out.println(ret);
    }
}

我们点进去Proxy.newInstance方法的源码。

    public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h) {
        final Class<?> caller = System.getSecurityManager() == null
                                    ? null
                                    : Reflection.getCallerClass();
        /*
         * Look up or generate the designated proxy class and its constructor.
         */  //查找或生成指定的代理类及其构造函数。
        Constructor<?> cons = getProxyConstructor(caller, loader, interfaces);  //获取到构造器

        return newProxyInstance(caller, cons, h);  //使用Constructor#newInstance去new一个对象
    }

我们主要关心getProxyConstructor方法:

    private static Constructor<?> getProxyConstructor(Class<?> caller,
                                                      ClassLoader loader,
                                                      Class<?>... interfaces)
    {
        // optimization for single interface
        if (interfaces.length == 1) {  //如果接口的数量是一个
            Class<?> intf = interfaces[0];  //获取到接口就行
            if (caller != null) {
                checkProxyAccess(caller, loader, intf);
            }
            return proxyCache.sub(intf).computeIfAbsent( //在computeIfAbsent里面计算出来代理类的构造器
                loader,
                (ld, clv) -> new ProxyBuilder(ld, clv.key()).build()    //最终会回调ProxyBuilder#build,在里面创建字节码文件
            );
        } else {  //如果接口有很多个,需要克隆一份接口出来去完成...
            // interfaces cloned
            final Class<?>[] intfsArray = interfaces.clone();  //把接口克隆一份
            if (caller != null) {
                checkProxyAccess(caller, loader, intfsArray);
            }
            final List<Class<?>> intfs = Arrays.asList(intfsArray);
            return proxyCache.sub(intfs).computeIfAbsent(
                loader,
                (ld, clv) -> new ProxyBuilder(ld, clv.key()).build()
            );
        }
    }

我们主要关心ProxyBuilder.build方法:

        Constructor<?> build() {
            Class<?> proxyClass = defineProxyClass(module, interfaces);  //获取到Class对象(通过写字节码,再使用类加载器加载进来)
            final Constructor<?> cons;
            try {
                cons = proxyClass.getConstructor(constructorParams);  //从代理类Class对象获取构造器
            } catch (NoSuchMethodException e) {
                throw new InternalError(e.toString(), e);
            }
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    cons.setAccessible(true);  //设置构造器为可访问的
                    return null;
                }
            });
            return cons;
        }

我们似乎看到了熟悉的代码defineProxyClassgetConstructor,我们主要关心defineProxyClass方法

        //定义即将要代理的类Class对象(通过直接写字节码进行生成)...
        private static Class<?> defineProxyClass(Module m, List<Class<?>> interfaces) {
            String proxyPkg = null;     // package to define proxy class in
            int accessFlags = Modifier.PUBLIC | Modifier.FINAL;  //设置访问标志位为PUBLIC|FINAL
       //------------------------------此处省略n行代码-----------------------------------------------
            /*
             * Choose a name for the proxy class to generate.
             */
            //在代理的包后面跟上$Proxy前缀以及一个唯一的数(nextUniqueNumber)
            long num = nextUniqueNumber.getAndIncrement();
            String proxyName = proxyPkg.isEmpty()
                                    ? proxyClassNamePrefix + num
                                    : proxyPkg + "." + proxyClassNamePrefix + num;

            ClassLoader loader = getLoader(m);  //获取到类加载器
            trace(proxyName, m, loader, interfaces);

            /*
             * Generate the specified proxy class.
             */  //生成特殊的代理类的二进制的字节码数组,里面会写字节码到一个byte数组当中,写字段、方法、字节码等...
            byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                    proxyName, interfaces.toArray(EMPTY_CLASS_ARRAY), accessFlags);
            try {  //通过UNSAFE类去定义一个类,它是个native方法,是交给JVM去做了...
                Class<?> pc = UNSAFE.defineClass(proxyName, proxyClassFile,
                                                 0, proxyClassFile.length,
                                                 loader, null);
                reverseProxyCache.sub(pc).putIfAbsent(loader, Boolean.TRUE);
                return pc;
            } catch (ClassFormatError e) {
                /*
                 * A ClassFormatError here means that (barring bugs in the
                 * proxy class generation code) there was some other
                 * invalid aspect of the arguments supplied to the proxy
                 * class creation (such as virtual machine limitations
                 * exceeded).
                 */
                throw new IllegalArgumentException(e.toString());
            }
        }

我们可以很明显的看到这里生成了一个byte数组,然后使用UNSAFE.defineClass去将这个类加载到VM中,终于破案了!也就是说JDK动态代理最终的原理也就是创建一个byte数组(字节码),然后交给VM去进行加载,和我们之前的使用ASM框架去生成代码的原理基本上类似。

1.8.4 针对方法区动态性的小总结

JDK动态代理使用的就是Proxy.newInstance方法去通过生成byte数组从而去defineClass,然后去new的对象。而CGLIB动态代理呢?则是采用第一种方式,也就是ASM框架的方式去完成的动态代理,也是想方设法生成字节码数组,最终交给VM去完成defineClass,再完成对象的创建。

Comment