Java将class文件,加载到内存中,转换为可以使用的对象的过程就是一个类加载的过程。

类加载过程

类加载过程:加载 -> 连接 -> 初始化。 ⇒ 这个过程是在启动的时候处理的,所以启动的时候会有一些性能开销。

连接过程又可分为三步:验证 -> 准备 -> 解析

什么时候触发第一个加载的阶段?

  • 遇到 new 实例化对象的时候
  • 读取第一个static字段的时候,初始化这个class (在编译期将结果放到常量池的字面量除外)
  • 调用一个类型的静态方法的时候
  • 反射
  • 子类初始化的时候,先需要初始化父类 ==》 反之不会初始化
  • jdk8的default方法,接口需要先被初始化

类加载的全过程

stage 1 加载

  • 按照类的全限定名中获取类的二进制字节流 - 没限制类字节流的来源 - 给了很多的可操作空间
    • 来自 zip 包 : jar包
    • 网络获取
    • 运行时计算 - 动态代理技术
    • 其他文件,如 jsp 文件
  • 将字节流转换为方法区的运行时的数据结构 — 虽然JDK8开始使用永久代代替了方法区,是的类的存储也在堆中的了,但是这里逻辑上描述还是使用方法区
  • 在堆中实例化一个 class 对象 ,作为程序访问方法区中的类型数据的外部接口

也有一些比较特殊的数据结构的加载,比如数组类型

如果数据类型的元素是引用的,那么就是交给类加载器完成

如果是基础类型的,那么就不需要类加载其去加载类文件,那么这个数组就与引导类加载器关联

stage 2 连接 - 验证

对加载的字节流进行基础的格式验证

  • 文件格式验证:文件魔术数】常量池的常量类型、常量编码; 保证字节流可以正确解析并且存储到方法区内
  • 元数据验证:类是否存在父类、接口是否完全实现了等语义校验
  • 字节码验证:程序流分析和数据流分析,判断语义是否合法;
  • 符号引用验证:类是否缺少外部依赖类、方法、资源等; IllegalAccessErrir , NoSuchFieldError , NoSuchMethodError

stage 3 连接 - 准备

为类中的变量、静态变量分配内存,设置类变量的初始值 - JDK8之后这里就是在堆中了

没有final static 修改的字面量的数据初始化在这里重新计算和分配空间

如果是 final static 修饰的字面量 , javac 阶段已经将内容存储在了 ConstantValue 属性里面了,这里直接赋值就可以了

stage 4 连接 - 解析

将常量池中的符号引用替换为直接引用的过程

stage 5 初始化

在加载和连接阶段,已经将涉及到的class全部加载完成了 ,这时应该是部分完成初始化

在初始化阶段,JVM开始真正执行主程序代码,开始初始化运行期的class内容了,将主导权交给了应用程序

类加载器

使用类的全限定名获取二进制流的操作独立到虚拟机外部去做,这个逻辑为类加载器完成。

一个 class 由不同的类加载器完成加载到虚拟机内部,那么本质上说就是两个不同的class对象。

JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader

  1. BootstrapClassLoader(启动类加载器) :最顶层的加载类,由 C++ 实现,负责加载 %JAVA_HOME%/lib目录下的 jar 包和类或者被 Xbootclasspath参数指定的路径中的所有类。
  2. ExtensionClassLoader(扩展类加载器) :主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类,或被 java.ext.dirs 系统变量所指定的路径下的 jar 包。
  3. AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。

双亲委派模型

每一个类都有一个对应它的类加载器。系统中的 ClassLoader 在协同工作的时候会默认使用 双亲委派模型

即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回(存在缓存机制),否则才会尝试加载。加载的时候,首先会把该请求委派给父类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。

当父类加载器无法处理时,才由自己来处理。当父类加载器为 null 时,会使用启动类加载器 BootstrapClassLoader 作为父类加载器。

public Class<?> loadClass(String name)throws ClassNotFoundException {
            return loadClass(name, false);
    }
    protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
            // 首先判断该类型是否已经被加载
            Class c = findLoadedClass(name);
            if (c == null) {
                //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
                try {
                    if (parent != null) {
                         //如果存在父类加载器,就委派给父类加载器加载
                        c = parent.loadClass(name, false);
                    } else {
                    //如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
                        c = findBootstrapClass0(name);
                    }
                } catch (ClassNotFoundException e) {
                 // 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }

每个类加载都有一个父类加载器,我们通过下面的程序来验证。

public class ClassLoaderDemo {
    public static void main(String[] args) {
        System.out.println("ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader());
        System.out.println("The Parent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent());
        System.out.println("The GrandParent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent().getParent());
    }
}

Output

ClassLodarDemo's ClassLoader is sun.misc.Launcher$AppClassLoader@18b4aac2
The Parent of ClassLodarDemo's ClassLoader is sun.misc.Launcher$ExtClassLoader@1b6d3586
The GrandParent of ClassLodarDemo's ClassLoader is null

AppClassLoader的父类加载器为ExtClassLoaderExtClassLoader的父类加载器为 null,null 并不代表ExtClassLoader没有父类加载器,而是 BootstrapClassLoader

The Java platform uses a delegation model for loading classes. The basic idea is that every class loader has a “parent” class loader. When loading a class, a class loader first “delegates” the search for the class to its parent class loader before attempting to find the class itself.

对于tomcat

class org.apache.catalina.loader.ParallelWebappClassLoader class java.net.URLClassLoader class sun.misc.LauncherExtClassLoader

双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类。

类的加载

类加载有三种方式:

1、命令行启动应用时候由JVM初始化加载 2、通过Class.forName()方法动态加载 3、通过ClassLoader.loadClass()方法动态加载

package com.pdai.jvm.classloader;
public class loaderTest {
        public static void main(String[] args) throws ClassNotFoundException {
                ClassLoader loader = HelloWorld.class.getClassLoader();
                System.out.println(loader);
                //使用ClassLoader.loadClass()来加载类,不会执行初始化块
                loader.loadClass("Test2");
                //使用Class.forName()来加载类,默认会执行初始化块
//                Class.forName("Test2");
                //使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态块
//                Class.forName("Test2", false, loader);
        }
}
 
public class Test2 {
        static {
                System.out.println("静态初始化块执行了!");
        }
}

分别切换加载方式,会有不同的输出结果。

Class.forName()和ClassLoader.loadClass()区别?

  • Class.forName(): 将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;
  • ClassLoader.loadClass(): 只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
  • Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。

破坏双亲委派模型 - loadClass

load -> find -> define

  • loadClass 递归调用父类加载器loadClass,如果加载失败则会调用自定义的findClass方法
  • findClass 类加载逻辑
  • defineClass 把类字节数组变成类

如果不打破双亲委派机制,重写findClass方法即可

如果打破双亲委派机制,重写整个loadClass方法

破坏双亲委派模型 - SPI

JNDI 是在 JDK1.3的时候加入到 rt.jar , 对资源进行查找和集中管理 , 需要调用其他厂商的 classpath 下面的 SPI 接口代码。

但是因为 JNDI 管理类是一个基础类,由 BootstracpClassloader 加载的, 但是 SPI 是 AppClassloader 加载到的。

于是引入了线程上下文加载器 : ContextClassLoader 是一种与线程相关的类加载器,类似 ThreadLocal,每个线程对应一个上下文类加载器,主要是用了打破类加载器中的委托机制的。 即在 Thread 类里面加一个线程的上下文(默认是 AppClassLoader),方便其他的类加载器加载的class获取到这个上下文,如JNDI , 以此来完成对SPI 的加载。获取到加载的内存class对象。

此时即当SPI只有一个的时候,可以直接硬编码获取到上下文的 AppClassLoader 完成对 Driver的加载获取到 class , 然后被JNDI 管理。

但是当SPI存在多个,如多个驱动的时候,就无法完成了 直到在JDK6 的时候,使用 ServicelLoader机制,加载 META-INF/services 里面的配置信息完成 SPI 代码的加载

ServiceLoader<Driver> load = ServiceLoader.load(Driver.class);

上面的内部实现中,其实也是获取到ContextClassLoader完成加载,不同的是,SPI的配置允许加载多个Driver了

public final class ServiceLoader<S> implements Iterable<S> {
    public static <S> ServiceLoader<S> load(Class<S> service) {
        // (5)获取当前线程上下文加载器
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
 
    public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
        return new ServiceLoader<>(service, loader);
    }
    //(6)
    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = svc;
        loader = cl;
        reload();
    }

破坏双亲委派模型 - OSGi

OSGI实现了模块级的热插拔,每一个Bundle都可以声明自己import的package 和自己 export 的package。那么对于类加载,自己import , 肯定是交给export它的bundle的类加载器完成加载。

使用自定义类加载机制,让每一个 Bundle 都有一个自己的类加载器,需要替换BUndle的时候,就把Bundle和类加载器一个替换掉。

此时,类加载不再是树结构了,而是网状结构了。

那么这个结构,可能就会出现A B 两个bundle相互依赖,loadclass 又是一个同步方法,很容易出现死锁。

JDK7之后,ClassLoader增加了 registerAsParallelCapable 方法对并行的类加载注册声明,将锁级别从Classloader降低到加载的这个类名的级别。

双亲委派的案例 - Tomcat

对于web服务器,各个不同的web程序的类加载器都有一个公共的jar需要加载,这些jar一般都是使用一个公共的类加载器完成加载,不然会浪费内存中的方法区空间。

tomcat6之后,/lib下面就是存储这些jar的位置;

对于WEB-iNF里面的,很显然就是web程序自己的classloader完成加载,其父亲就是lib的classloader。

对于JSP文件也是如此,JSP是文本文件,修改后重新替换实现的原理就是每一个JSP文件都存在一个子的JasperLoader,当检测到JSP修改的时候,会替换掉目前JasperLoader的实例来实现HotSpot

URLClassloader

源码我们可以发现AppClassLoader和ExtClassLoader都是Launcher的静态内部类,继承自URLClassLoader

class URLClassLoader extends SecureClassLoader implements Closeable
  • SecureClassLoader:扩展了ClassLoader,并为定义具有相关代码源和权限的类提供了额外支持,这些代码源和权限默认情况下由系统策略检索。
  • URLClassLoader:继承自SecureClassLoader,支持从jar文件和文件夹中获取class,继承于classloader,加载时首先去classloader里判断是否由启动类加载器加载过。

推荐阅读

https://blog.csdn.net/xyang81/article/details/7292380

https://juejin.im/post/5c04892351882516e70dcc9b

http://gityuan.com/2016/01/24/java-classloader/

https://jawhiow.github.io/2019/04/24/java/详解常用类加载器:ContextClassLoader/

https://www.jianshu.com/p/b75a89014e27