Java虚拟机加载Java类的过程

本文详细介绍了JVM加载Java类的过程,包括加载、链接、初始化三个步骤,并深入解析了类加载器的工作原理及双亲委派机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JVM加载Java类的过程

JVM加载Java类的过程可分为三步:加载、链接、初始化


1、加载

加载的过程就是查找字节流,并根据找到的字节流来创建类的一个过程。

Java语言的数据类型可以分为两大类:基本数据类型和引用数据类型。

  • 基本数据类型:由JVM预先定义好的,所以也就没有查找字节流这一说法
  • 引用数据类型:可分为四种,即类、接口、数组和泛型参数。因为泛型参数在编译过程中会被擦除,所以JVM中就只有前三种。而数组又是由JVM直接生成的,所以只有类和接口需要查找字节流

那么JVM是怎么查找到字节流的呢?其实就是通过类加载器,它主要有四类:启动类加载器、平台类加载器、应用程序类加载器和用户自定义类加载。并且这一块又涉及到另一个知识点,那便是双亲委派机制(大概内容就是如果一个类加载器收到了类加载的请求,首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成)。通过这个双亲委派机制,就可以保证同一个类只被记载一次。

经过类加载之后,这个类就算是加载进来了。


2、链接

对于链接而言,JVM实现具有灵活性,但必须保留下列属性:

  1. 在链接之前,类或者接口必须已经被完全加载
  2. 在初始化之前,类或者接口必须已经被完全验证和准备
  3. 链接过程中检测到的程序错误会抛出到程序中某个位置,并且程序会在该位置上采取某些操作,这些操作可能会直接或者间接地链接到类或者接口所涉及到的到或者接口。链接这一块就可以分为三阶段:验证、准备和解析
  • 验证阶段:此阶段就是想看class文件的前8位是不是java标识符,验证符不符合规范

  • 准备阶段:此阶段就是给静态段分配内存。除了分配内存之外,部分JVM还会在此阶段构造其他跟类层次相关的数据结构(比如实现虚方法的动态绑定的方法表,这个方法表是用来解决动态绑定问题的,解析时通过这个方法表,根据实际类型来解析获取对应的方法)。在class文件被加载到JVM之前,这个类没办法知道其他类和方法、字段所对应的具体地址,甚至都不知道本身类的方法、字段的地址。所以,如果需要引用这些成员时,Java编译器就会生成一个符号引号,在运行阶段,这个符号引用一般都可以精准地定位到具体目标上

  • 解析阶段:此阶段主要是将符号引用解析成实际引用。若符号引用指向了一个未被记载的类,或者没有被加载的类的方法或字段,此时解析阶段就会触发这个类的加载(注意:不一定会触发这个类的链接以及初始化)。并且,在解析阶段不同的JVM有不同的解析策略,例如:

    public class A {
        public void main(String[] args) {
            B b = null;
        }
    }
    
    • 策略1:链接“A”的时候发现了引用“B”,因为加载“B”
    • 策略2:链接“A”的时候发现了引用“B”,但是发现“B”没有被使用,所以暂时不加载“B”。当真正使用B的时候才进行加载(例如:b = new B() )

    因此,在一个JVM实现中,可以能采取在使用时才会解析类或者接口中的符号引用,也可能采取在该类或者接口被验证时一次性解析全部引用符号。这取决于采用的时哪种策略,也意味着解析过程可能在类或者接口被初始化后还会进行


3、初始化

在Java代码中,如果想要初始化静态字段,可以在声明的时候直接赋值,也可以选择在静态代码块中对它赋值。

直接赋值的静态字段被final修饰了,而且这个静态字段是基本数据类型或者字符串时,就会被Java编译器标记成常量值,初始化就直接被JVM完成了。除此之外的直接赋值操作,还有静态代码中中的代码,就会被Java编译器放到同一个方法中,并且命名为。

类加载的最后一步就是初始化,就是给标记为常量值的字段进行赋值,执行方法的过程。这个时候,JVM会通过加锁来确保类的方法只被执行一次

方法的一些小细节:

  • 方法与构造方法不同,它不需要显示地调用父类的方法,JVM虚拟机会保证在子类的方法执行之前,父类的方法已经执行完毕。因此JVM中第一个被执行的方法的类肯定是java.lang.Object
  • 方法对于类或者接口而言并不是必须的,倘若一个类中没有静态代码块,也没有对应的变量的赋值操作,那么Java编译器可以不为这个类生成方法
  • 接口中不能使用静态代码块,但仍有static静态变量的赋值操作,所以也会有方法。但是接口执行方法时不需要先执行父类接口的方法,只有当父类接口中定义的变量被使用到时,才会执行方法
  • JVM会保证一个类的方法在多线程环境中被正确地加锁、同步。如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的方法,其他线程都需要阻塞等待

当类或者接口的方法执行之后,JVM才算是成功地加载了Java类。


4、类的初始化何时会被触发

那么,类的初始化何时会被触发呢?JVM规范列举了以下几种触发情况:

  • 当JVM启动时,初始化用户指定的主类
  • 当遇到用以新建目标实例的new指令时,初始化new指令的目标类
  • 当遇到调用静态方法的指令时,初始化该静态方法所在的类
  • 当遇到访问静态字段的指令时,初始化该静态字段所在的类
  • 子类的初始化会触发父类的初始化
  • 若一个接口定义了default方法,那么直接或者间接地执行实现该接口的类的初始化,也会触发该接口的初始化
  • 使用反射API对某个类进行反射调用时,初始化这个类
  • 当初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在类

5、类加载器 ClassLoader

Java程序的执行需要依靠JVM,并且JVM在进行类执行时会通过设置的CLASSPATH环境属性进行指定路径的字节码文件加载,JVM加载字节码文件的操作就需要使用到类加载器(CLassLoader),如图 5-1所示:

在这里插入图片描述

图 5-1

5.1、类加载器简介

JVM进行程序类的加载器需要类加载器,为了保证Java程序的执行安全性,Java内部提供有类加载器的支持,JVM提供有3种类加载器,其操作关系如图 5-1-1所示:

图 5-1-1
  • BootStrap(根加载器,又称系统类加载器):由C++语言编写的类加载器,是在Java虚拟机启动后进行初始化操作,主要是加载Java底层系统提供的核心类库
  • PlatformClassLoader类加载器(平台类加载器):使用Java语言编写的类加载器(JDK 1.8之前为ExtClassLoader,扩展类加载器),主要目的是进行模块加载
  • AppClassLoader(应用程序类加载器):主要目的是加载CLASSPATH所指定的类文件或者JAR文件

tips:JVM为什么提供3类加载器,为什么不直接设计成一个

主要是为了系统安全,设置了不同级别的类加载器,在Java装载类的时候使用的是“全盘负责委托机制”,这里面有两层含义。

  • 全盘负责:是指当一个ClassLoader进行加载时,除非显示地使用了其他的类加载器,该类所依赖以及所引用的类也有同样的ClassLoader进行加载
  • 责任委托:先委托父类加载器进行加载,再找不到父类时才由自己负责加载,并且类不会重复加载

这样设计的有点在于当有一个伪造系统类(假设伪造为java.lang.String)出现时,利用全盘负责委托机制就可以保证java.lang.String类永远都是由BootStrap类加载器加载。这样就保证了系统的安全,所以此类加载又称为“双亲加载”,即由不同的类加载器负责加载指定的类。

范例1:获取自定义类加载器

public class ClassLoaderTestDemo {
    public static void main(String[] args) {
        String string = "Java";
        System.out.println(string.getClass().getClassLoader());
    }
}

/*
输出结果:
null
*/

本程序获取了String类对应的类加载器信息,但是输出结果为null,这是因为BootStrap根加载器是由C++语言编写而并非Java语言的,所以只能以null的形式返回。

范例2:获取自定义类加载器

//定义一个User类
class User {
}

public class ClassLoaderTestDemo {
    public static void main(String[] args) {
        User user = new User();
        System.out.println(user.getClass().getClassLoader());
        System.out.println(user.getClass().getClassLoader().getParent());
        System.out.println(user.getClass().getClassLoader().getParent().getParent());
    }
}

/*
输出结果:
jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc
jdk.internal.loader.ClassLoaders$PlatformClassLoader@880ec60
null
*/

本程序自定义一个User类,并且获取了该类的所有加载器,通过结果可知,自定义的类和系统类所使用的是不同的类加载器。

5.2、自定义ClassLoader类

Java最大的特点在于可以方便地提供类加载的支持,这样使得程序的开发拥有极大的灵活性。除了系统提供的内置类加载器外,也可以利用继承ClassLoader的方法实现自定义类加载器的定义。

范例3:自定义类加载器

//定义一个需要加载的程序类
public class Message {
    public void send() {
        System.out.println("Java Define ClassLoader");
    }
}
/*
自定义类加载器
由于需要将加载的二进制数据文件转为Class类的处理,所以可以使用ClassLoader类提供的difineClass()方法实现转换
*/
public class CustomClassLoader extends ClassLoader {
    //定义要加载的类文件完整路径
    private static final String MESSAGE_CLASS_PATH = "D:" + File.separator + "Message.class";


    /**
     * 进行指定类的加载操作
     *
     * @param classNmae 类的完整名称“包.类”
     * @return 返回一个指定类的Class对象
     * @throws Exception 如果类文件不存在则无法加载
     */
    public Class<?> loadData(String classNmae) throws Exception {
        //读取二进制文件
        byte[] data = this.loadClassData();

        //读取到了
        if (data != null) {
            return super.defineClass(classNmae, data, 0, data.length);
        }

        return null;
    }


    //通过文件进行类的加载
    private byte[] loadClassData() throws Exception {
        InputStream input = null;

        //将数据加载到内存中
        ByteArrayOutputStream bos = null;
        byte[] data = null;

        try {
            //实例化内存流
            bos = new ByteArrayOutputStream();

            //文件流加载
            input = new FileInputStream(new File(MESSAGE_CLASS_PATH));

            //读取数据
            input.transferTo(bos);

            //字节数据取出
            data = bos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (input != null) {
                //关闭输入流
                input.close();
            }
            if (bos != null) {
                //关闭内存流
                bos.close();
            }
        }
        return data;
    }
}
//使用自定义类加载器进行类加载并调用方法
public class CustomClassLoaderTestDemo {
    public static void main(String[] args) throws Exception {
        //实例化自定义类加载器
        CustomClassLoader classLoader = new CustomClassLoader();
        //进行类的加载
        Class<?> cls = classLoader.loadData("edu.zhku.util.Message");

        //由于Message类并不在CLASSPATH中,所以此时无法直接将对象转为Message类型,只能通过反射调用
        //实例化对象
        Object obj = cls.getDeclaredConstructor().newInstance();
        //获取方法
        Method method = cls.getDeclaredMethod("send");
        //方法调用
        method.invoke(obj);
        System.out.println();

        System.out.println("类加载器执行的顺序:");
        System.out.println(cls.getClassLoader());
        System.out.println(cls.getClassLoader().getParent());
        System.out.println(cls.getClassLoader().getParent().getParent());
        System.out.println(cls.getClassLoader().getParent().getParent().getParent());
    }
}

/*
输出结果:
Java Define ClassLoader

类加载器执行的顺序:
edu.zhku.util.CustomClassLoader@79b4d0f
jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc
jdk.internal.loader.ClassLoaders$PlatformClassLoader@73f792cf
null
*/

本程序利用自定义类加载器的形式直接加载磁盘上的二进制字节码文件,并利用 ClassLoader提供的difineClass()方法将二进制数据转换为Class类实例,这样就可以利用反射进行对象实例化与方法调用。由上述执行结果可知,当程序利用自定义加载器实现了类的加载操作,首先执行的就是自定义类加载器。


6、双亲委派机制

6.1、双亲委派机制简介

当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。

通俗来讲,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器(注意:双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码)去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载

整个过程中,直到到达Bootstrap ClassLoader之前,都是没有哪个加载器选择自己加载的。如果父加载器无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。

6.2、双亲委派机制的过程

双亲委派机制的流程图,如图 6-2-1所示:

图 6-2-1

6.3、双亲委派机制的作用

双亲委派机制的作用有两个,分别为:

  • 防止重复加载同一个 .class文件。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
  • 保证核心.class不能被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。

注:此文章只是本人在学习过程中记录的笔记,若有错误的地方,敬请指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

窝在角落里学习

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值