HotSpot虚拟机之类加载过程及类加载器

目录

一、类加载过程

1. 加载(Loading)

2. 验证(Verification) 

3. 准备(Preparation) 

4. 解析(Resolution)

5. 初始化(Initialization)

二、类加载时机

1. 主动引用(引用时主动初始化)

2. 被动引用(引用时不触发初始化) 

1):第一种被动引用

2):第二种被动引用

3):第三种被动引用

三、类加载器

1. 加载器类型

2. 双亲委派模型

3. 破坏双亲委派

四、参考资料


一、类加载过程

        一个类型从被加载到JVM内存中开始,到卸载出内存为止,它的生命周期有7个阶段:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading),如下图所示。其中加载、验证、准备、初始化、卸载的开始具有顺序性,但可以交叉进行。以下详细介绍其各个阶段。

1. 加载(Loading)

        加载(Loading)目的是将类的Class文件加载到内存,由类加载器完成加载。

2. 验证(Verification) 

        验证(Verification)目的确保Class文件包含的信息符合《Java 虚拟机规范》的全部约束要求,验证阶段是否严谨,直接决定JVM是否能承受恶意代码的攻击。注意:文件格式验证后才能存储到元空间,其他三个验证基于元空间存储结构上进行,不再读取字节流

3. 准备(Preparation) 

        准备(Preparation) 目的将类中静态变量(static)分配内存并设置初始值(默认值)

4. 解析(Resolution)

        解析(Resolution)目的将常量池的符号引用替换为直接引用。引用有:

  • 符号引用:一组符号来描述引用的目标(与内存布局无关);
  • 直接引用:直接指向目标的指针、偏移量、目标的句柄(与内存布局有关)

        解析点主要是类或接口、字段、类方法、接口方法等,其中:JDK9之前除了接口方法(public修饰)无需进行访问权限校验,其他查找结束后都要进行访问权限校验,不通过则抛出:java.lang.IllegalAccessError,JDK9之后增加了模块化访问权限;除invokedynamic指令外,会对第一次解析结果进行缓存,称为“解析缓存”

解析点

解析步骤

类或接口

当前代码处在D类,根据未解析的符号引用N去解析类或接口C的直接引用,步骤如下:

step1:C非数组类型,全限定名N传递给D的类加载器,去加载C,根据C的加载动作,也可能触发其他类的相关加载动作,一旦有任何异常,则解析失败

step2:C数组类型,若元素是引用对象,则根据step1进行加载解析;

                                 若元素是基本类型,则JVM自动生成代表该数组维度和元素类型

step3:确认D是否对C有访问权限,否则抛出java.lang.IllegalAccessError;

step4:没有任何异常,则已形成有效的类或接口。

字段

当前代码处在D类,根据未解析的符号引用N去解析字段(字段所属的类或接口C),步骤如下:

step1:首先解析D字段表class_index的索引CONSTANT_Class_info类进行解析(字段所在的类解析),后续对C进行字段搜索;

step2:C本身包含了简单名称和描述符与目标字段匹配,返回该字段的直接引用,则查找结束;

step3:否则,则查找C实现的接口和其父接口,从下往上递归搜索,有匹配,则查找结束;

step4:否则,若C不是Object类,则查找C的父类,从下往上递归搜索,有匹配,则查找结束;

step5:否则,查找失败,则抛出java.lang.NoSuchFieldError;

step6:查找成功后,D是否对该字段有访问权限,否则抛出:java.lang.IllegalAccessError。

类方法

当前代码处在D类,根据未解析的符号引用N去解析方法(方法所属的类C),步骤如下:

step1:首先解析D方法表class_index的索引CONSTANT_Class_info类进行解析(字段所在的类解析),若C是一个接口(与接口方法相反),则抛出:java.lang.IncompatibleClassChangeError;

step2:在类C本身包含了简单名称和描述符与目标方法匹配,返回该方法的直接引用,则查找结束; 

step3:否则,若C不是Object类,则查找C的父类,从下往上递归搜索,有匹配,则查找结束;

step4:否则,则查找C实现的接口和其父接口,从下往上递归搜索,有匹配说明C是个抽象类,则抛出:java.lang.AbstractMethodError

step5:否则,查找失败,则抛出java.lang.NoSuchMethodError;

step6:查找成功后,是否对该方法有访问权限,否则抛出:java.lang.IllegalAccessError。

接口方法

当前代码处在D类,根据未解析的符号引用N去解析方法(方法所属的接口C),步骤如下:

step1:首先解析D方法表class_index的索引CONSTANT_Class_info类进行解析(字段所在的类解析),若C是一个类(与类方法相反),则抛出:java.lang.IncompatibleClassChangeError;

step2:在接口C本身包含了简单名称和描述符与目标方法匹配,返回该方法的直接引用,则查找结束;

step3:否则,则查找C的父接口,从下往上递归搜索直至Object类(接口方法包含Object类中方法),有匹配,则查找结束;

step4:否则,则查找C实现的接口和其父接口,从下往上递归搜索,若不同的父接口中有多个匹配的方法时,返回其中一个方法,则查找结束;

step5:否则,查找失败,则抛出java.lang.NoSuchMethodError。

注意:

    a.JDK9之前除了接口方法(public修饰)无需进行访问权限校验,其他查找结束后都要进行访问权限校验,不通过则抛出:java.lang.IllegalAccessError;JDK9之后增加了模块化访问权限;

    b.类方法和接口方法存储的地方不同:

       类方法CONSTANT_Methodref_info、接口方法CONSTANT_InterfaceMethodref_info

5. 初始化(Initialization)

        初始化(Initialization)目的是执行类构造器<clinit>()方法的过程类初始化<clinit>()在编译期自动生成,且自动收集类变量并赋值和静态语句块的语句合并产生

        注意:类构造器<clinit>()方法,先隐式调用父类该方法;而实例构造器<init>()方法,则显式调用父类构造器

二、类加载时机

1. 主动引用(引用时主动初始化)

        主动引用是引用时主动初始化,有且只有6种需立即初始化,如下图所示。

2. 被动引用(引用时不触发初始化) 

        被动引用是引用时不触发初始化,如下三种情况:

1):第一种被动引用
package com.cmmon.instance.classLoad;

/**
 * @author tcm
 * @version 1.0.0
 * @description 被动引用:引用时不触发初始化 —— 子类引用父类的静态字段,不会导致子类初始化
 * @date 2023/5/18 10:22
 **/
public class NotInitialization {

    /**
     * 静态字段时,只有直接定义这个字段的类才会被初始化
     * 因此:通过子类来引用父类定义的静态字段,则只会初始化父类而不初始化子类
     *
     * 添加参数:-XX:+TraceClassLoading会导致子类加载
     * [Loaded com.cmmon.instance.classLoad.SuperClass from file:/E:/idea%20project/instance-test/target/test-classes/]
     * [Loaded com.cmmon.instance.classLoad.SubClass from file:/E:/idea%20project/instance-test/target/test-classes/]
     */
    public static void main(String[] args) {
        System.out.println(SubClass.value);
        /*
            结果:并没有初始化子类SubClass
            SuperClass init!
            1234
         */
    }

}
class SuperClass {

    static {
        System.out.println("SuperClass init!");
    }

    public static int value = 1234;

}

class SubClass extends SuperClass {

    static {
        System.out.println("SubClass init!");
    }

}
2):第二种被动引用
package com.cmmon.instance.classLoad;

/**
 * @author tcm
 * @version 1.0.0
 * @description 被动引用:引用时不触发初始化 —— 数组来引用类,不会导致该类的初始化
 * @date 2023/5/18 11:40
 **/
public class NotInitialization2 {

    /**
     * 通过数组来引用类,不会导致该类的初始化
     */
    public static void main(String[] args) {
        SuperClass2[] sca = new SuperClass2[10];
        /*
            没有输出:SuperClass init!
         */
    }

}

class SuperClass2 {

    static {
        System.out.println("SuperClass init!");
    }

    public static int value = 1234;

}
3):第三种被动引用
package com.cmmon.instance.classLoad;

/**
 * @author tcm
 * @version 1.0.0
 * @description 被动引用:引用时不触发初始化 —— 常量在编译阶段会加入到调用类的常量池中,不会导致常量所在的类初始化
 * @date 2023/5/18 17:33
 **/
public class NotInitialization3 {

    /**
     * 编译阶段通过常量传播优化后,常量直接存入到调用类的常量池中,不会导致常量所在的类初始化
     * 因此:NotInitialization3与ConstClass两个类在编译成Class文件后,则不存在任何联系了
     */
    public static void main(String[] args) {
        System.out.println(ConstClass.HELLO_WORLD);
        /*
            没有输出:ConstClass init!
         */
    }

}

class ConstClass {

    static {
        System.out.println("ConstClass init!");
    }

    public static final String HELLO_WORLD = "Hello World";

}

三、类加载器

1. 加载器类型

        类加载过程中加载(Loading)阶段是由类加载器完成,如下表所示根据不同层面对类加载器进行分类。注意:类的唯一性由类全限定名且类加载器共同决定,每个类加载器都一个独立的类名称空间

分类层面

分类

特点

JVM角度分类

启动类加载器

(Bootstrap ClassLoader)

1.JVM自身的一部分

2.由C++实现

其他类加载器

1.全部继承java.lang.ClassLoader

2.由Java语言实现

三层类加载器

(JDK8与之前版本)

启动类加载器

(Bootstrap ClassLoader)

1.负责加载${JAVA_HOME}/lib目录的类库

2.该加载器被Java程序直接引用,JVM自身的一部分。

扩展类加载器

(Extension ClassLoader)

1.负责加载${JAVA_HOME}/lib/ext目录的类库

2.由sun.misc.Launcher.ExtClassLoader类实现;

应用程序类加载器

(Application ClassLoader)

1.负责加载用户类路径classpath路径下的所有类库

2.由sun.misc.Launcher.AppClassLoader类实现;

自定义类加载器

(User ClassLoader)

用户自定义类加载器

模块化类加载器

(JDK9)

与JDK8有些变动

1.平台类加载器(PlatForm ClassLoader)代替扩展类加载器

2.取消${JAVA_HOME}/lib/ext、${JAVA_HOME}/jre目录;

3.平台类加载器和应用程序类加载器不在继承java.net.URLClassLoader,而继承BuiltinClassLoader。

注意:

     a.类的唯一性:类的全限定名 + 类加载器,每个类加载器都一个独立的类名称空间

     b.类加载器之间的层次关系,称为:“双亲委派模型”(Parents Delegation Model);

     c.父类加载器是否为null,来引导类加载器达到双亲委派,为null则使用启动类加载器

        如下代码所示,比较两个类是否相等:是否同一个全限定名的类 + 类加载器相同。

package com.cmmon.instance.classLoad;

import java.io.IOException;
import java.io.InputStream;

/**
 * @author tcm
 * @version 1.0.0
 * @description 每个类加载器拥有一个独立的类名称空间
 * @date 2023/5/24 14:17
 **/
public class ClassLoadNamespace {

    /**
     * 比较两个类是否相等:是否同一个全限定名的类 + 类加载器相同
     * 每个类加载器拥有一个独立的类名称空间
     * 两个类的“相等”:类的Class对象的哈希码、equals()、isAssignableFrom()、instanceof
     */
    public static void main(String[] args) throws Exception {
        // 自定义类加载器
        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is == null) {
                        return super.loadClass(name);
                    }

                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        };

        Object object = myLoader.loadClass("com.cmmon.instance.classLoad.ClassLoadNamespace").newInstance();
        System.out.println(object.getClass());  // class com.cmmon.instance.classLoad.ClassLoadNamespace
        System.out.println(object instanceof com.cmmon.instance.classLoad.ClassLoadNamespace); // false
        System.out.println(object.getClass().getClassLoader()); // com.cmmon.instance.classLoad.ClassLoadNamespace$1@7b3300e5

        Object object2 = new ClassLoadNamespace();
        System.out.println(object2.getClass()); // class com.cmmon.instance.classLoad.ClassLoadNamespace
        System.out.println(object2 instanceof com.cmmon.instance.classLoad.ClassLoadNamespace); // true
        System.out.println(object2.getClass().getClassLoader()); // sun.misc.Launcher$AppClassLoader@18b4aac2
    }

}

2. 双亲委派模型

        如下图所示,双亲委派执行过程及其优点。JDK9维持三层类加载器和双亲委派,变动了委派给父类时,先判定该类是否归属到哪一个模块中,如有则委派给负责该模块的加载器

        父类加载器是否为null,来引导类加载器达到双亲委派,为null则使用启动类加载器。核心方法为java.lang.ClassLoader#loadClass(java.lang.String, boolean),如下源代码所示。

protected Class<?> loadClass(String name, boolean resolve)
	throws ClassNotFoundException
{
	synchronized (getClassLoadingLock(name)) {
		// First, check if the class has already been loaded
		Class<?> c = findLoadedClass(name);
		if (c == null) {
			long t0 = System.nanoTime();
			try {
				if (parent != null) {
					c = parent.loadClass(name, false);
				} else {
					c = findBootstrapClassOrNull(name);
				}
			} catch (ClassNotFoundException e) {
				// ClassNotFoundException thrown if class not found
				// from the non-null parent class loader
			}

			if (c == null) {
				// If still not found, then invoke findClass in order
				// to find the class.
				long t1 = System.nanoTime();
				c = findClass(name);

				// this is the defining class loader; record the stats
				sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
				sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
				sun.misc.PerfCounter.getFindClasses().increment();
			}
		}
		if (resolve) {
			resolveClass(c);
		}
		return c;
	}
}

3. 破坏双亲委派

        双亲委派模型并不是强制约束的模型,而是Java设计器推荐给开发者的类加载实现方式。所以Java中大部分类加载器都遵循双亲委派模型,但也有例外,如:Tomcat、OSGi(动态模块化规范 _ 灵活的类加载器架构)。

        Tomcat6之前的类库目录如下图所示。

        Tomcat服务器加载器架构及对应的委派关系如下。

四、参考资料

HotSpot虚拟机之Class文件及字节码指令_爱我所爱0505的博客-CSDN博客

jvm之java类加载机制和类加载器(ClassLoader)的详解_超级战斗王的博客-CSDN博客

java类加载机制_xiaolong_java的博客-CSDN博客

类加载器分类_类加载器的分类_.小鲤鱼的博客-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值