1.类的加载过程

196 阅读8分钟

类加载过程

参考文章:juejin.cn/post/684490…

image.png

Java 的类加载过程可以分为 5 个阶段:加载、验证、准备、解析和初始化。这 5 个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后。

Loading(加载)

JVM 在该阶段的主要目的是将字节码(可能是 class 文件、也可能是 jar 包,甚至网络资源)转化为二进制字节流加载到内存中,并生成一个代表该类的 java.lang.Class 对象。作为方法区这个类的各种数据的访问入口

[通过io流技术,将字节码传入内存中]

类被加载到方法区中后主要包含 运行时常量池、类型信息、字段信息、方法信息、类加载器的

引用、对应class实例的引用等信息。

类加载器的引用: 这个类到类加载器实例的引用

对应class实例的引用:类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的

对象实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。

注意,主类在运行过程中如果使用到其它类,会逐步加载这些类。jar包或war包里的类不是一次性全部加载的,是使用到时才加载。

Verification(验证)

校验字节码文件的正确性

验证阶段参考文章:blog.csdn.net/en_joker/ar…

JVM 会在该阶段对二进制字节流进行校验,只有符合 JVM 字节码规范的才能被 JVM 正确执行。该阶段是保证 JVM 安全的重要屏障。验证阶段大致上会完成下面4个阶段的验证工作:文件格式验证、元数据验证、字节码验证、符号引用验证。

  • 确保二进制字节流格式符合预期(比如说是否以 cafe babe 开头)。
  • 是否所有方法都遵守访问控制关键字的限定。
  • 方法调用的参数个数和类型是否正确。
  • 确保变量在使用之前被正确初始化了。
  • 检查变量是否被赋予恰当类型的值。

Preparation(准备)

JVM 会在该阶段对类变量(也称为静态变量,static 关键字修饰的)分配内存并初始化(对应数据类型的默认初始值,如 0、0L、null、false 等)。[在编译的二进制字节码里面已经规定好了需要准备的空间大小,字节码里面还保存了接口数量占两个字节大小,十进制就是65535个接口数量。]

[非静态变量会不会准备空间大小?]:不会

假如有这样一段代码:

public String chenmo = "沉默";
public static String wanger = "王二";
public static final String cmower = "沉默王二"; 

chenmo 不会被分配内存,而 wanger 会;但 wanger 的初始值不是“王二”而是 null。需要注意的是,static final 修饰的变量被称作为常量,和类变量不同。常量一旦赋值就不会改变了,所以 cmower 在准备阶段的值为“沉默王二”而不是 null。

Resolution(解析)

将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如 main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过 程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用。

Initialization(初始化)

对类的静态变量初始化为指定的值,执行静态代码块 。

类加载器

引导类加载器(也叫作:启动类加载器)、扩展类加载器、应用类加载器、自定义加载器

image.png

非继承关系,都间接继承了 ClassLoader类;

三者通过 private final ClassLoader parent; 维系关系。

引导类加载器(启动类加载器) Bootstrap ClassLoader

负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如 rt.jar、charsets.jar等 。

扩展类加载器 Extention ClassLoader

负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR 类包 。

应用程序类加载器 Application ClassLoader

负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类

[ClassPath路径具体是什么路径?]

自定义加载器 User ClassLoader

负责加载用户自定义路径下的类包 。可能有些冲突的类,tomcat 启动多个war应用时候,就打破了双亲委派。使用的自定义类加载器。

创建类加载器的过程

image.png

类运行加载全过程图可知其中会创建JVM启动器实例sun.misc.Launcher。 sun.misc.Launcher初始化使用了单例设计模式(饿汉式),保证一个JVM虚拟机内只有一个 sun.misc.Launcher实例。 在Launcher构造方法内部,其创建了两个类加载器,分别是 sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应用类加载器)。

JVM默认使用Launcher的getClassLoader()方法返回的类加载器AppClassLoader的实例加载我们 的应用程序。

  public Launcher() {
    Launcher.ExtClassLoader var1;
    try {
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }

    try {
    //构造应用类加载器,在构造的过程中将其父加载器设置为ExtClassLoader,
			//Launcher的loader属性值是AppClassLoader,我们一般都是用这个类加载器来加载我们自己写的应用程序
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);//
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }

    ......省略代码
}

双亲委派的过程

jvm 加载一个类时,交给指定的类加载器。当前类加载器并不是直接进行请求类加载。过程如下

  • 当前类加载器[一般就是 应用类加载器] 会先在 ClassLoader.loadClass 中的 findLoadedClass(name); 进行查找,是否加载过此类。如果加载过直接返回,没有的话就先向上传递给父加载器。

  • 父加载器[扩展类加载器]接受到类加载请求,也不会直接加载。而是重复上述 步骤1)操作。传递给启动类加载器。扩展类加载器的 parent ==null

  • 启动类加载器也是重复1)步骤。如果发现没有,就在自己负责的区域目录 jre/lib 下面查找。发现没有,就返回空。

  • 扩展类启动器发现父加载器没有找到,就在自己负责的区域目录 jre/lib/ext 下面查找。发现没有就返回空。

  • 应用类加载器发现父加载器没有找到,就开始干活了在classpath下查找[我们的代码都在这里] 发现找到了,开始真正的加载类信息。

  • 上述就是双亲委派的过程。

我们来看下双亲委派的核心代码,双亲委派是如何向上委派的?

ClassLoader中的方法loadClass,类加载的核心方法[双亲委派的过程]。这里是线程安全的synchronized。

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();
                //都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类
                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;
    }
}

为什么要设计双亲委派机制?

  • 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改。
  • 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性。

看完了双亲委派试试强度?hollis大神高品质双亲委派十连问:juejin.cn/post/691631…

疑问?

符号引用是什么?

我们写的java代码想要执行,是需要先编译成.class文件的。每一个类都有自己的.class文件,.class文件中记录着该类的所有信息,比方说teacher类里面有个student 类。在teacher字节码里面就会保存到student的引用。但是此时还没有被jvm所加载,这时候就只能用一个符号代替这个引用。

javap 命令执行出来的类信息,常量池中后面的#+数字就是符号引用。在jvm 解析阶段就会替换成直接引用。顺着符号引用就可找到对应关系、真正地址。

动态连接是什么?

前置技术:了解些字节码技术

每一个栈帧都包含指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。

在class文件格式的常量池中存有大量符号引用(1.类的全限定名,2.字段名和属性,3.方法名和属性),字节码的方法调用指令就是以常量池中指向方法的符号引用为参数。这些符号引用一部分会在连接的解析阶段转为直接引用(向目标的指针、相对偏移量或者是一个能够直接定位到目标的句柄),这种转化称为静态解析。还有一部分引用会在运行期间转化为直接引用,这部分称为动态连接。