浅谈虚拟机类加载过程

本文深入探讨Java类加载过程,包括加载、连接和初始化三个阶段,重点讲解类加载器(ClassLoader)的工作原理,双亲委派机制及其实现细节。

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

类的加载过程

类的加载过程,主要有以下三步

  • 加载:通过ClassLoader加载class文件,生成Class对象
  • 连接:该过程分为三步,验证、准备、解析
    • 验证:主要是验证class文件的正确性与安全性
    • 准备:为类变量分配内存并设置类变量初始值
    • 解析:将常量池内的符号引用替换为直接引用
  • 初始化:执行类变量的赋值和静态初始化

这里将着重介绍类的加载、以及初始化过程,而不会对连接部分做阐释。

类的初始化

我们先不谈如何加载一个类,先来看看如何使用一个类,也就是类的初始化过程。
有5种情况会对类进行初始化:
1)遇到new、getstatic、putstatic或invokestatic这4条字节码指令。常见场景:使用new创建一个对象,操作静态变量或静态方法

Demo demo = new Demo(); // 使用new创建一个对象
Demo.FIELD = 1; // 设置静态变量值
System.out.println(Demo.FIELD); // 使用静态变量
Demo.method(); // 执行静态方法

2)通过反射进行调用

Class clazz = Class.forName("com.dfyang.aop.utils.Demo");

源码分析,涉及到native方法,这里只是通过参数进行了解释

@CallerSensitive
public static Class<?> forName(String className)
            throws ClassNotFoundException {
    Class<?> caller = Reflection.getCallerClass();
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
// initialize为true表示类将被初始化
private static native Class<?> forName0(String name, boolean initialize,
                                        ClassLoader loader,
                                        Class<?> caller)
    throws ClassNotFoundException;

3)当初始化一个类时,先初始化其父类(这个容易理解,毕竟子类会继承父类)
4)当虚拟机启动时,虚拟机需要初始化主类,也就是程序的入口

public static void main(String[] args) {
    System.out.println("程序入口");
}

@SpringBootApplication
public class ZooCommunityApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZooCommunityApplication.class, args);
    }
}

5)java.lang.invoke.MethodHandle实例最后解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄(这里仅了解)

这里我们已经知道了我们经常使用的类是如何进行初始化,接下来我们再来看看类的加载过程。

类加载器

首先来介绍以下ClassLoader(类加载器),其主要作用是从系统外部获取Class二进制数据流,所有Class都是由ClassLoader进行加载。
下面引用《深入理解Java虚拟机》表示其重要性。

类加载器可以说是Java语言的一项创新,也是Java语言流行的重要原因之一,它最初是为了满足Java Applet的需求二开发出来的。虽然目前Java Applet技术基本上已经“死掉”,但类加载器却在类层次划分、OSGi、热部署、代码加密等领域大放异彩,成为了Java技术体系的一块重要的基石,可谓是失之东隅,收之东隅 。——《深入理解Java虚拟机》

首先介绍ClassLoader中两个重要的方法
defineClass:该方法将字节数组转化为类的实例(具体转换过程涉及native方法)

protected final Class<?> defineClass(String name, byte[] b, int off, int len)
    throws ClassFormatError
{
    return defineClass(name, b, off, len, null);
}

findClass:这个方法需要我们重写
具体步骤:通过该方法传入类名,再将Class文件转换为字节数组,再通过defineClass方法我们就能够生成类的实例

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

下面来自定义一个类加载器,继承ClassLoader并重写findClass方法。
如下所示,该加载器指定类名,在指定路径下查找对应的class文件,并进行加载。

package com.dfyang.aop;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;

/**
 * @Author: 55411
 * @Date: 2019/8/14 22:17
 * @Description: 自定义类加载器
 */
public class MyClassLoader extends ClassLoader {

    /** 加载文件路径 */
    private String path;

    public MyClassLoader(String path) {
        this.path = path;
    }

    /**
     * 指定类名生成类对象
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] binaryStream = toBinaryStream(name);
        return defineClass(name, binaryStream, 0, binaryStream.length);
    }

    /**
     * 转换为字节数组
     * @param name
     * @return
     */
    private byte[] toBinaryStream(String name) {
        name = path + name + ".class";
        InputStream in = null;
        ByteArrayOutputStream out = null;
        try {
            in = new FileInputStream(name);
            out = new ByteArrayOutputStream();
            int i = 0;
            while ((i = in.read()) != -1) {
                out.write(i);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                in.close();
                out.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return out.toByteArray();
    }

}

下面是需要被加载的类,静态代码块中的代码将在类初始化时执行。(我是通过javac编译成class文件,放到F盘)

public class Test {
	static {
		System.out.println("Hello world!");
	}
}

执行下面代码,控制台打印:Hello world!

public static void main(String[] args) throws Exception {
   MyClassLoader myClassLoader = new MyClassLoader("F:\\");
    Class test = myClassLoader.findClass("Test");
    test.newInstance();
}

上面我们时通过将class文件放到指定的目录,然后通过类加载器指定类名生成对应类的实例。

——上面说到Class.forName会对类进行初始化,也就是整个类加载的三步,加载、连接、初始化已完成,ClassLoader加载类与其不同,只完成了加载过程。

双亲委派机制

下面介绍系统提供的3中类加载器:

  • BootstrapClassLoade(启动类加载器):加载java核心类库(JAVA_HOME\lib目录下)
  • ExtClassLoader(扩展类加载器):加载JAVA_HOME\lib\ext目录下或java.ext.dirs系统变量指定路径的所有类库
  • AppClassLoader(应用程序类加载器):加载程序所在目录(ClassPath)
  • 还有就是自定义类加载器了

可以看到系统提供的类加载器同样是对指定路径进行类的加载。

接下来介绍类加载器的双亲委派机制:
双亲委派机制的就是一个类加载器收到了类加载请求,首先不会自己尝试加载,而是会委派给父类加载器完成,每一层加载器均是如此,直到最顶层的BootStrapClassLoader(启动类加载器),再由最顶层的BootStrapClassLoader(启动类加载器)进行加载,如果无法进行加载,再委派给子类加载器完成,每一次加载器均是如此。
在这里插入图片描述
顺序的证明如下

public static void main(String[] args) throws Exception {
    MyClassLoader myClassLoader = new MyClassLoader("F:\\"); // 自定义类加载器
    System.out.println(myClassLoader.getParent()); // AppClassLoader
    System.out.println(myClassLoader.getParent().getParent()); // ExtClassLoader
    System.out.println(myClassLoader.getParent().getParent().getParent()); // null
}
// 打印日志,由于Bootstrap ClassLoader由c++编写,并未由java编写,所以为null
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@6f496d9f
null

类加载过程的部分源码分析

synchronized (getClassLoadingLock(name)) {
    // 查询类是否被加载
    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) {
        }
          if (c == null) {
             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();
         }

为什么需要双亲委派机制?
为了保证程序的正常运行,试想如果我们使用自定义类加载器加载了我们自己编写的java.lang.Object,那么程序可以正常运行吗?——当然,因为有双亲加载机制,这里是无法进行验证的。总之,双亲委派机制就是为了保证我们程序的正常运行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值