虚拟机类加载机制

JVM的“Exe”是怎么执行的

Posted by Timer on December 23, 2021

[TOC]

前言

​ 这块内容是最颠覆我对Java理解的部分,也是耽误时间最久的,类加载这三个字越看越不明白。类的本质是什么?为什么要加载?从哪加载到哪?既然底层是C++,那JVM执行的时候底层调用C++的new不就完事了?

Java的new操作符的具体实现

// hotspot/src/share/vm/interpreter/interpreterRuntime.cpp
...
// HotSpot中new操作符的实现函数
IRT_ENTRY(void, InterpreterRuntime::_new(JavaThread* thread, ConstantPool* pool, int index))
  Klass* k_oop = pool->klass_at(index, CHECK);
  instanceKlassHandle klass (THREAD, k_oop);
  // Make sure we are not instantiating an abstract klass
  klass->check_valid_for_instantiation(true, CHECK);
  // Make sure klass is initialized
  klass->initialize(CHECK);
  // At this point the class may not be fully initialized
  // ...
  oop obj = klass->allocate_instance(CHECK);
  thread->set_vm_result(obj);
IRT_END
...

这段代码总结一下就是:先生成一个Klass对象klass,再用klass生成oop对象obj,obj也就是java里的对象。

oop对象头:

// hotspot/src/share/vm/oops/oop.hpp
class oopDesc {
 ...
 private:
  // 用于存储对象的运行时记录信息,如哈希值、GC分代年龄、锁状态等
  volatile markOop  _mark;
  // Klass指针的联合体,指向当前对象所属的Klass对象
  union _metadata {
    // 未采用指针压缩技术时使用
    Klass*      _klass;
    // 采用指针压缩技术时使用
    narrowKlass _compressed_klass;
  } _metadata;
 ...
}

mark对象头不用多说,这里能看到关键的一点,就是Java每个对象都拥有一个klass指针,所以能够实现反射。

oop对象体:

同时,Java对象的field也都存储在oop里,oop提供了一系列方法来获取和设置field,如下:

// hotspot/src/share/vm/oops/oop.hpp
class oopDesc {
 ... 
  // 返回成员属性的地址
  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;
  // 同样是成员的地址获取方法,在GC时使用
  template <class T> T* obj_field_addr(int offset) const;   
 ...
  // instanceOop获取和设置其成员属性的方法
  oop obj_field(int offset) const;
  volatile oop obj_field_volatile(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);
  // 如果成员时基础类型,则使用其特有的方法,这里只列出针对byte类型的方法
  jbyte byte_field(int offset) const;
  void byte_field_put(int offset, jbyte contents);
 ...
}

其中第一个方法的实现如下:

// hotspot/src/share/vm/oops/oop.inline.hpp
...
// 获取对象中field的地址
inline void* oopDesc::field_base(int offset) const { 
  return (void*)&((char*)this)[offset];
}

由上述代码可知,每个field在oop中都有一个偏移量。oop通过偏移量计算出field地址,再根据地址得到数据。

OOP继承关系

// hotspot/src/share/vm/oops/oopsHierarchy.hpp
...
// Oop的继承体系
typedef class oopDesc*                            oop;
typedef class   instanceOopDesc*            instanceOop; //普通对象
typedef class   arrayOopDesc*                    arraysOop; //数组对象
typedef class     objArrayOopDesc*            objArrayOop;
typedef class     typeArrayOopDesc*            typeArrayOop;
...

成员方法在哪里?

现在知道每个对象的元信息(mark)和成员变量(field)了。但成员方法不属于oop对象,而是从klass对象中获得。

C++的动态绑定是由虚函数表实现的,代价是每个对象都要维护一个虚函数表。

而Java由于是一切皆对象,每个对象维持一个虚函数表,开销会非常大。JVM对此作出了优化,虚函数表不再由对象维护,而是由Class类型维护,因此没有在oop里找到方法调用的逻辑,因为这部分代码在klass里。

Klass是什么?

如下为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.

可见,Klass主要提供了两个功能:

(1)用于表示Java类。Klass中保存了一个Java对象的类型信息,包括类名、限定符、常量池、方法字典等。一个class文件被JVM加载之后,就会被解析成一个Klass对象存储在内存中。

(2)实现对象的虚分派(virtual dispatch)。所谓的虚分派,是JVM用来实现多态的一种机制。

Klass的继承体系

// 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;  // 基础类型的数组类
...

其中,InstanceKlass也就是Java里的Class类,记录了一个类运行时的所有信息,源码如下所示:

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

虚函数表

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

一个klassVtable可看成是由多个vtableEntry组成的数组,其中每个元素vtableEntry里面都包含了一个方法的地址。在进行虚分派时,JVM会根据方法在klassVtable中的索引,找到对应的vtableEntry,进而得到方法的实际地址,最后根据该地址找到方法的字节码并执行。

有了这些基础前提知识后,再来看类加载的过程,会更清澈。

从HotSpot的角度来说,整个类加载就是生成instanceKlass。把磁盘中的Class文件加载到内存,对数据进行校验、解析和初始化,底层的C++生成了instanceKlass,也就是Java里可用的Class。

image-20211224171413513

类加载的过程

一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载 、验证、准备、解析、初始化、使用卸载七个阶段,其中验证、准备、解析三个部分统称为连接。

  1. 加载

    把类的字节码载入方法区,元空间创建对应的instanceClass,堆中生成对应的Class对象。具体的说,要完成三件事:

    • 通过一个类的全限定名来获取定义此类的二进制字节流
    • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
    • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
  2. 链接-验证

    验证类是否符合JVM规范

  3. 链接-准备

    为static变量分配空间,设置默认值。

    • static变量在JDK7之前存储于instanceKlass的末尾,从JDK7开始存储在_java_mirror末尾,也就是Class中。
    • 准备阶段只分配空间,赋值在初始化阶段完成。
    • 如果static是final的基本类型,那么编译阶段就确定了,赋值在准备阶段就完成。
    • 如果static是final的引用类型,那么赋值会在初始化阶段完成。
  4. 链接-解析

    把常量池中的符号引用解析为直接引用。

  5. 初始化

    初始化也就是调用()V,虚拟机会保证这个方法的线程安全。

    初始化时机

    • main方法所在类,会首先被初始化
    • 首次访问这个类的静态变量或静态方法
    • 子类初始化联动父类初始化
    • 子类访问父类静态对象,只会触发父类初始化
    • Class.forName
    • new

    不会初始化的情况

    • 访问类的static final静态变量(基本类型或String)不会初始化。

    • 获得类对象.class不会初始化(因为instanceKlass在加载时就生成)

    • 创建该类的数组不会初始化

    • 类加载器的loadClass不会初始化

类加载器

类加载器的作用是:把加载过程的第一步,通过一个类的全限定名来获取定义此类的二进制字节流,放到JVM外部实现。

我的第一个疑问是:仅仅是把一串byte从磁盘加载到内存,为什么要放到外部实现,还要有那么多种类的加载器,意义是什么?

  1. 首先,是为了区分同名的类。假设某个服务器部署很多独立的应用,这些应用拥有很多同名但是不同版本的类库。这时候想要加载这些类的时候做好区分,只能不同应用拥有自己独立的加载器。
  2. 其次,可以增强类。类加载器可以在 load class 时对 class 进行重写和覆盖。比如面向切片用的动态代理,可以只对某个类库修改而不对其他类库产生影响,就是通过每个类都有自己独立的类加载器。

双亲委派

双亲委派是一种加载类的约定,要求越基础的类由越上层的加载器进行加载,基础的类之所以叫基础,是因为它总是作为被用户继承、调用的存在。

具体做法是:向上递归寻找是否加载过,如果没有加载过,再由上至下依次寻找类。说白了就是防止重复加载,比如系统已经定义了String,自定义的String就无效了。

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 先检查是否已经被加载了
            Class<?> c = findLoadedClass(name);
            if (c == null) { // 如果还没加载,找上级 
                long t0 = System.nanoTime();
                try {
                    if (parent != null) { // 如果上级是bootstrap,那么返回null
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }

                if (c == null) {
                    // 如果依然没有找到,则从当前节点开始往其子类一个个find
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

破坏双亲委派

  1. 在 jdk 1.2 之前,那时候还没有双亲委派模型,不过已经有了 ClassLoader 这个抽象类,所以已经有人继承这个抽象类,重写 loadClass 方法来实现用户自定义类加载器。

    而在 1.2 的时候要引入双亲委派模型,为了向前兼容, loadClass 这个方法还得保留着使之得以重写,新搞了个 findClass 方法让用户去重写,并呼吁大家不要重写 loadClass 只要重写 findClass。

    这就是第一次对双亲委派模型的破坏,因为双亲委派的逻辑在 loadClass 上,但是又允许重写 loadClass,重写了之后就可以破坏委派逻辑了。

  2. 双亲委派说白了就是下级做事要请求上级,上级要请求上上级,但是对于有些情况,是上级要向下级请求的。

    例如:jdbc对不同的厂商提供了同样的标准接口,不同数据库的类不可能出现在核心lib目录下,这个时候启动类加载器加载的DriverManger会调用下层加载器初始化各个驱动,没有遵守双亲委派由下而上的规则。

    public class ClassLoaderTest {
        public static void main(String[] args) throws SQLException, ClassNotFoundException {
            Connection connection = DriverManager.getConnection("jdbc:mysql://xxx.xxx.xxx.xxx:3306/izaya", "xxx", "xxx");
            System.out.println(DriverManager.class.getClassLoader());
            Enumeration<Driver> drivers = DriverManager.getDrivers();
            while (drivers.hasMoreElements()){
                Driver driver = drivers.nextElement();
                System.out.println(driver);
                System.out.println(driver.getClass().getClassLoader());
            }
        }
    }
    

    输出结果:

    null
    com.mysql.cj.jdbc.Driver@52cc8049
    sun.misc.Launcher$AppClassLoader@18b4aac2
    

    DriverManger有如下静态代码块

        static {
            loadInitialDrivers();
            println("JDBC DriverManager initialized");
        }
    

    loadInitialDrivers用ServiceLoader对每个Driver接口进行了初始化

    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    
    public static <S> ServiceLoader<S> load(Class<S> service) {
            ClassLoader cl = Thread.currentThread().getContextClassLoader();
            return ServiceLoader.load(service, cl);
    }
    

    可以看出对第三方接口的加载使用了线程上下文加载器。这个加载器默认就是应用程序加载器。

  3. 模块化

    暂不了解,不发言,有空补。