【JVM深入理解系列1】JVM底层加载

类的加载过程

加载

加载是指查找并加载类的字节码文件,并将其转换为 JVM 内部的数据结构,即 Class 对象。Java 中的类加载器负责查找类文件,一般有三种类加载器:引导类加载器、扩展类加载器和应用程序类加载器。

在HotSpot中具体是三个步骤:

  1. 通过类的全限定名获取存储该类的class文件。
  2. 解析成运行时数据,InstanceKlasss实例。存放在方法区。
  3. 在堆区生成该类的class对象,InstanceMirrorKlass实例。

程序怎么写都可以。只要满足上面三个效果就OK。改写OpenJdk源码,满足上面三个条件就Ok

加载时机

  • 主动使用时
  1. new, getstatic, putstatic, invokestatic
  2. 反射
  3. 初始化一个类的子类去加载其父类。
  4. 启动类(main函数所在类)
  5. 当使用JDK1.7动态语言时,如果一个java.lang.invoke.MethodHandler实例最后的解析结果。REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先触发其初始化。
  • 预加载:包装类,String, Thread

因为没有指明必须从哪里获取class文件,脑洞大开的工程师开发了这些:

  1. 从压缩包中读取,jar,war
  2. 从网络中读取,web applet
  3. 动态生成,如 动态代理,CGLIB
  4. 由其他文件生成,如JSP
  5. 从数据库读取。
  6. 从加密文件中读取

连接

连接包括验证、准备和解析三个阶段:

  • 验证:验证字节码文件的格式、语义、安全等方面是否符合 JVM 规范。

  • 准备:为类的静态变量分配内存并设置默认初始值。

    说明:

实例变量是在创建对象的时候完成赋值的,没有赋初值一说。
int,long,short,char,byte,boolean,float,double,reference
如果被final修饰,在编译的时候会给属性添加ConstantValue属性,准备阶段直接完成赋值,既没有赋初值这一说。

  • 解析:将符号引用转换为直接引用,以便 JVM 快速访问类的方法和变量。

    说明: 解析后的信息存储在ConstantPoolCache类实例中

    1. 类或接口的解析。
    2. 字段解析
    3. 方法解析
    4. 接口方法解析

    解析可以发生的时机:

    1. 加载阶段解析常量池时
    2. 用的时候

openjdk是第二种思路,在执行特定的字节码指令之前进行解析:

初始化

初始化是指为类的静态变量赋值和执行静态代码块。在 Java 中,类的初始化是在首次使用时进行的,如果该类从未被使用过,则不会进行初始化。

使用

在类初始化完成后,类就可以被使用了。类的使用包括创建对象、调用方法和访问变量等操作。

卸载

卸载是指将类的字节码从内存中移除,并释放相应的资源。类的卸载由 JVM 自动完成,不需要显式调用。

各个阶段会遇到的异常

过程 异常类型 描述
加载 ClassNotFoundException 找不到指定类的定义
NoClassDefFoundError 找不到类的定义文件
UnsupportedClassVersionError JVM不支持加载的类文件版本
连接 ClassFormatError 类文件格式不正确
VerifyError 字节码验证错误
IllegalAccessError 非法访问错误
初始化 ExceptionInInitializerError 静态初始化器抛出异常
使用 NoSuchFieldError 访问不存在的类成员
NoSuchMethodError 调用不存在的方法
NullPointerException 访问空对象
卸载 JVM不支持

HotSpot虚拟机中的OOP

类型 描述
Klass 描述Java类的元数据信息,如类名、访问标志、父类、接口等
InstanceKlass 描述Java类的实例信息,如静态变量、成员变量、方法表等
ArrayKlass 描述Java数组的元数据信息,如数组长度、元素类型等
TypeArrayKlass 描述基本类型数组的元数据信息,如数组长度、元素类型等
ObjArrayKlass 描述对象数组的元数据信息,如数组长度、元素类型等
MethodKlass 描述方法的元数据信息,如方法名、参数类型、返回值类型等
ConstantPool 描述类常量池的信息,如字符串、数字、符号引用等
Symbol 描述字符串的信息,如字符数组、字符串长度、哈希值等
ConstantPoolCache 保存常量池中的一些元素,如字符串、符号引用等的解析结果

Klass与InstanceKlass

Klass 对象用于表示类或接口的定义信息,而 instanceKlass 则用于表示类的实例信息。

  • Klass

Klass 对象包含了类或接口的元数据信息,例如类名、父类名、接口名、字段信息、方法信息等。Klass 对象还包含了一些与类的实例无关的信息,例如类变量、静态方法等。

  • InstanceKlass

instanceKlass 对象则用于表示类的实例信息,例如类的实例变量、实例方法等。instanceKlass 对象与 Klass 对象之间存在继承关系,一个 Klass 对象对应一个或多个 instanceKlass 对象。当类被实例化后,虚拟机会根据 Klass 对象创建一个 instanceKlass 对象,并将该对象的指针存储在实例对象的头部,以便访问该对象的实例变量和方法。

在加载 Java 类时,虚拟机会首先创建 Klass 对象来表示类或接口的定义信息。在连接阶段,虚拟机会为 Klass 对象分配空间,并填充类变量的初始值。在初始化阶段,虚拟机会为类的静态变量赋初值,并执行类的初始化代码。当类被实例化时,虚拟机会根据 Klass 对象创建 instanceKlass 对象,并将该对象的指针存储在实例对象的头部。

一个类实例结构

MetaspaceObj

Metadata
    klass
        instanceKlass
            instanceMirrorKlass描述java.lang.Class实例
            instanceRefKlass描述java.lang.Reference的子类
            instanceClassLoaderKlass
        
        ArrayKlass
            TypeArrayKlass描述java基本类型数组的数据结构
            ObjectArrayKlass描述java中引用类型数组的数据结构。

类的元信息是存储在元空间的。
普通的Java类在JVM对应的是instanceKlass的实例。三个类:

  1. InstanceMirrorKlass:用于表示java.lang.class, Java代码中获取到的class对象,实际上是这个C++类的实例。存储在堆区,学名镜像类。
  2. InstanceRefClass:用于表示Java.lang.ref.Referece的子类。
  3. InstanceClassLoader:用于遍历某个加载器加载的类。

Java中的数组不是静态数组类型,是动态数据类型,既是运行时期生成的,Java数组的元信息用ArrayKlass的子类来表示

  1. TypeArrayKlass:表示基本类型的数组。
  2. ObjectArrayKlass:表示引用类型的数组。