Java-底层原理-类加载机制

7 篇文章 2 订阅

Java-底层原理-类加载机制

系列文章目录

Java-底层原理-编译原理
Java-底层原理-javac源码笔记
Java-底层原理-类加载机制
Java-底层原理-clinit和init

摘要

前面写了类编译相关文章Java-底层原理-编译原理Java-底层原理-编译原理,知道了怎么从java文件到class文件的过程。

本文简要介绍从class文件到JVM内存过程即Java加载机制,还会介绍双亲委派机制的破坏,线程上下文加载器,以及JDBC Driver是如何自动加载的。

未完成

1 Java类加载

1.1 概述

广义上的加载包括加载、连接、初始化三个阶段:
Java类加载
以上几个阶段开始(是开始,而并不一定是一个完成下一个才开始)顺序固定,但需要注意的是解析阶段可能在初始化之后再进行,以支持运行期动态绑定。

下图是验证准备解析三个阶段的详图,展示了从.class文件到Class对象的过程:
类加载

1.2 加载(.class->内存

1.2.1 概述

加载工作由ClassLoader负责,读取class字节码文件,创建Class对象,放入内存:

  1. 通过类的全限定名加载class文件,构建二进制字节流
  2. 将该字节流代表的静态存储结构转化为方法区的运行时数据结构
  3. 生成代表该类的Class对象放入内存,他就是该类的访问入口

1.2.1 加载时机

1.2.1.1 隐式加载
  • 首次 new Object()
  • 首次读写类的静态非final字段(静态final字段不会造成类加载,而是在编译期处理
  • 首次调用类的静态方法(包括final和非final)
  • 首次调用某类的main方法
  • 加载子类时,父类还没加载就要先加载父类
1.2.1.2 显示加载
  • Class.forName("类全限定名")
    会触发类初始化
  • 使用 xxxClass.Class
    不会触发类初始化
  • classLoader.loadClass("类全限定名");
    不会触发类初始化

1.3 连接(验证->准备->解析

1.3.1 概述

验证类中的字节码,为静态域分配存储空间,并且如果必需的话,将解析这个类创建的对其他类的所有引用(符号引用->直接引用)。

连接又可分为验证、准备、解析三个阶段。

1.3.2 验证

验证.class字节码文件中的信息符合当前虚拟机规范,且不含危害虚拟机行为:

  • 文件格式验证
    验证.class文件能正确解析,验证通过就存入方法区
  • 元数据验证
    .class文件中的描述信息进行语义分析,检查是否符合规范,比如继承了final类或方法
  • 字节码验证
    分析数据流和控制流,检查方法体
  • 符号引用验证
    **注意,该阶段发生在解析阶段后。**检查符号引用中的访问性等

1.3.3 准备

为类的静态字段分配方法区内存,并设初值。但要注意final和非final有所不同:

  • static int i = 123,在准备阶段只会设 i 初值为0。而在初始化阶段才会将 i 设值为123。
    因为该赋值为123的语句实际上是一条putstatic指令,在程序编译阶段存放于<clinit>,等到类初始化阶段才会执行。
  • 而final static int i = 123 这种常量会在准备阶段就直接赋值为123

1.3.4 解析

主要是符号引用转为直接引用,主要针对类或接口、字段、类方法(static method)、接口方法、方法类型、方法句柄和调用点限定符。

在编译阶段,无法确定具体的内存地址,所以会使用符号来代替,即对抽象的类或接口进行符号填充为符号表。在类加载的解析阶段,JVM 将符号替换成具体的内存地址,也就是符号引用转直接引用。

  • 这里的符号引用是指任意形式的字面量且能无歧义地准确定义到目标。他存储于.class文件的常量池。
  • 直接饮用是指向目标的指针、相对偏移量或是间接定位到目标的句柄。他存储于运行时常量池,这是相当于将常量标识为已解析,以后不用再重复解析。

1.4 初始化(clinit->Class对象

1.4.1 概述

初始化阶段是类加载的最后一步,此时才会真正开始执行java应用程序代码(字节码)。此阶段中,会真正为类变量(static 非final)赋初值,以及做其他资源的初始化工作。

总的来说,初始化阶段会执行类构造器即<clinit>方法。

1.4.2 触发时机

1.4.2.1 主动引用

虚拟机规范规定以下情况需要执行类初始化,这些行为称为主动引用

  1. new(创建类,但不是创建数组)、getstatic(读类变量)、putstatic(写类变量)、invokestatic(调用类方法)4条字节码指令,如果目标类未初始化,就需要执行初始化:
    • 首次 new Object()
    • 首次读写类的静态非final字段(静态final字段不会造成类加载,而是在编译期处理
    • 首次调用类的静态方法(包括final和非final)
  2. 首次调用某类的main方法
  3. 子类初始化时,父类还未初始化就要先初始化父类
  4. 反射调用
  5. 使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个句柄所在类还未初始化,则会先触发其初始化。
1.4.2.2 被动引用

除主动引用情况以外的称为被动引用,不会触发类初始化,比如

  1. 常量访问不会触发初始化,因为其在编译阶段就存入调用类的常量池,而没有直接引用类。在编译成.class文件后,调用类对该常量的引用就变为对自己类的常量池的引用了,和该常量的出处类无关了。
  2. 通过子类引用父类静态非final字段,不会导致子类初始化,但会导致父类初始化
  3. 类数组的定义不会触发类的初始化

1.4.3 接口初始化

接口内也有<clinit>用来初始化接口中的类变量,但接口与类不同的是,接口初始化时无需先初始化父接口。而是要用到其类变量时时才会初始化。

2 Java类加载器

2.1 类加载器的分类

2.1.1 简介

当前版本jdk是采用双亲委派机制:

类加载机制

  • 自底向上委托加载
    加载类时,ClassLoader总是会首先委托其父ClassLoader尝试加载,层层向上直到BootstrapClassLoader
  • 自顶向下尝试加载
    BootstrapClassLoader
    ->ExtClassLoader
    ->AppClassLoader
    ->用户自定义的ClassLoader
    父ClassLoader会检查自己的路径是否可以加载该类,如果不行,就返回该加载请求给其子ClassLoader,自顶向下返回。
  • 如果父ClassLoader全部不能加载,该ClassLoader才会自己尝试加载。

ClassLoader加载Class大概思路如下:

  • 如果加载成功,就生成该类的Class对象,放入内存,并返回;
  • 如果还是没有成功加载,就抛出ClassNotFoundException

ClassLoader源码解析在第3章

2.1.2 BootstrapClassLoader-启动类加载器

BootstrapClassLoader是启动类加载器,由C++实现。

当使用System.out.println(obj.getClass().getClassLoader());时结果为null

通过以下代码打出BootstrapClassLoader加载的文件:

public class BootStrapTest
{
    public static void main(String[] args)
    {
        URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
        for (int i = 0; i < urls.length; i++) {
            System.out.println(urls[i].toExternalForm());
        }
        System.out.println("--------------------------");
        System.out.println("sun.boot.class.path=\n" + System.getProperty("sun.boot.class.path"));
    }
}

结果如下:

file:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/classes
--------------------------
sun.boot.class.path=
/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/sunrsasign.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/classes

sun.boot.class.path也可以看到BootstrapClassLoader加载的文件。除了rt.jar以外还加载了一些其他文件。

2.1.3 ExtClassLoader-扩展类加载器

扩展类加载器,加载jre/lib/ext下的文件。

该类属于sun.misc.Launcher的子类,继承自java.net.URLClassLoader

static class ExtClassLoader extends URLClassLoader

当使用getParent()的时候是null,表示是被BootstrapClassLoader加载,即BootstrapClassLoaderExtClassLoader的父加载器。

2.1.4 AppClassLoader-应用程序类加载器

加载classpath下的jarclass文件,由由-classpath-cp或JAR中的Manifestclasspath属性定义,一般是用户代码及其依赖。

该类也属于sun.misc.Launcher的子类,同样继承自java.net.URLClassLoader

static class AppClassLoader extends URLClassLoader

当使用getParent()的时候是ExtClassLoader,表示即ExtClassLoaderAppClassLoader的父加载器。

但需要注意的是,AppClassLoader位于rt.jar,也是被BootstrapClassLoader加载。

2.1.5 自定义类加载器

推荐继承java.lang.ClassLoader重写findClass方法,而不是直接重写loadClass,否则可能破坏原有的双亲委派加载机制。

在某些时候,需要定制加载逻辑,就需要自己开发ClassLoader。比如位于网络的字节码文件。或是加密了的字节码文件。

下面是一个简单示例:

public class MyClassLoader extends ClassLoader {

    private String path = null;

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

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] bytes = loadByte(name);
            return defineClass(name, bytes, 0, bytes.length);
        } catch (Exception e) {
            throw new ClassNotFoundException("类找不到" + name);
        }
    }

    private byte[] loadByte(String name) throws Exception {
        String fileName = path + File.separator + replaceSeparator(name) + ".class";

        try (FileInputStream fis = new FileInputStream(fileName)) {
            byte[] data = new byte[fis.available()];
            fis.read(data);
            return data;
        } catch (Exception e) {
            throw e;
        }
    }

    private String replaceSeparator(String name) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < name.length(); i++) {
            if (name.charAt(i) != '.') {
                sb.append(name.charAt(i));
            } else {
                sb.append(File.separator);
            }
        }
        return sb.toString();
    }

    public static void main(String[] args) throws Exception {
        MyClassLoader classLoader = new MyClassLoader("/home/wyj");
        Runtime.getRuntime().exec("javac /home/wyj/Test.java");
        Class<?> test = classLoader.loadClass("Test");
        System.out.println(test.getClassLoader());
    }
}

2.1.6 实验

2.1.6.1 实验一
  • 实验目的:该实验观察双亲委派机制及AppClassLoaderExtClassLoader两个类本身由谁加载。
  • 实验代码
public class Test2 {
    public static void main(String[] args) {
        ClassLoader appClassLoader = Test2.class.getClassLoader();
        System.out.println("appClassLoader=" + appClassLoader);
        System.out.println("appClassLoader.getClass().getClassLoader()=" + appClassLoader.getClass().getClassLoader());
        ClassLoader extClassLoader = appClassLoader.getParent();
        System.out.println("appClassLoader.getParent()=" + extClassLoader);
        System.out.println("extClassLoader.getClass().getClassLoader()=" + extClassLoader.getClass().getClassLoader());
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        System.out.println("extClassLoader.getParent()=" + bootstrapClassLoader);
    }
}
  • 实验结果:
appClassLoader=sun.misc.Launcher$AppClassLoader@18b4aac2
appClassLoader.getClass().getClassLoader()=null
appClassLoader.getParent()=sun.misc.Launcher$ExtClassLoader@2781e022
extClassLoader.getClass().getClassLoader()=null
extClassLoader.getParent()=null
  • 实验结论:
    • AppClassLoader父加载器是ExtClassLoader
    • AppClassLoader类由BootstrapClassLoader加载
    • ExtClassLoader父加载器是BootstrapClassLoader
    • ExtClassLoader类由BootstrapClassLoader加载
2.1.6.2 实验二
  • 实验目的
    观察是否ExtClassLoader加载jre/lib/etx目录下的jar文件。
  • 实验代码:
public class ClassLoaderTest2 {
    public static void main(String[] args) {
        ClassLoader loader = ClassLoaderTest2.class.getClassLoader();
        while(loader != null) {
            System.out.println(loader);
            loader = loader.getParent();
        }
        System.out.println(loader);
    }
}
  • 实验结果
    代码放在工程里,直接在ideal里面运行,输出如下:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@75412c2f
null

将该java文件打包成jar,移动到jre/lib/ext后再次运行结果如下:

$ java -jar  jartest.jar
sun.misc.Launcher$ExtClassLoader@7852e922
null
  • 实验结论
    ExtClassLoader的确会加载jre/lib/etx目录下的jar文件,而且可把自定义的代码放入该文件,一样会被ExtClassLoader加载,而不再被AppClassLoader加载。

2.2 双亲委派的意义

2.2.1 安全

比如java.lang.Object,用户也自定义一个同样权限定名的类。但在加载时,会首先用BootstrapClassLoader加载rt.jar中的该类。

而用户自定义的同包名Object类,也会因为AppClassLoader往上寻找到祖先BootstrapClassLoader类来加载该类,但会发现该类已经被加载过导致报错。

但是你可以用重写loadClass等方法来打破这种双亲委派机制。

2.2.2 判断Class是否相同

Java中判断两个类是否相同条件有2:

  1. 全限定名相同
  2. ClassLoader相同

2.3 双亲委派机制的破坏

2.3.1 破坏1-loadClass方法重写

JDK1.2之前没有双亲委派模型,所以之前的开发者继承java.lang.ClassLoader后要做的就是重写loadClass()方法,编写自定义的类加载逻辑。

2.3.2 破坏2-ThreadContextClassLoader

ThreadContextClassLoader即线程上下文加载器

2.3.2.1 概述

Java中的委派链是:
AppClassLoader -> ExtensionClassLoader -> BootstrapClassLoader

也就是说,委派链左边的ClassLoader加载出来的类可以很自然的使用右侧的ClassLoader加载出来的类对象。

但反过来就不行,比如JNDI服务(用于对资源进行集中管理和查找)位于rt.jar(由BootstrapClassLoader加载),它需要调用独立厂商实现部署在应用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代码(应该由AppClassLoader加载),但BootstrapClassLoader无法加载这些用户代码,所以引入了线程上下文件类加载器(ThreadContextClassLoader)。有了他,JNDI服务就可以去加载所需要的SPI代码了,即父加加载器请求子加载器去完成类加载。实际上这就破坏了双亲委派机制。

如果没有通过setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的ThreadContextClassLoader。这个是在线程对象init的时候做的。

Java应用运行时的初始ThreadContextClassLoader就是AppClassLoader

除了JNDI,JDBC, JCE, JAXB, JBI等设计SPI的加载动作基本都采用此方式。

2.3.2.2 DriverManager例子

我们通常使用的jdbc连接代码如下:

Class.forName("com.mysql.jdbc.Driver");
DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "user", "pwd");

执行Class.forName会使用当前调用类的ClassLoader(AppClassLoader)来加载com.mysql.jdbc.Driver,该Driver<clinit>如下:

static {
    try {
        java.sql.DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {
        throw new RuntimeException("Can't register driver!");
    }
}

因为此时DriverManager还未加载过,且这里调用了其静态方法,所以又会去加载、初始化DriverManager。注意该DriverManager位于java.sql包中,打在rt.jar内,所以会因为双亲委派机制从而被BootstrapClassLoader加载。DriverManager初始化时的<clinit>方法代码如下:

static {
   loadInitialDrivers();
   println("JDBC DriverManager initialized");
}

loadInitialDrivers方法中有如下代码:

// 这里的Driver是java.sql.Driver
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);

该load方法如下:

public static <S> ServiceLoader<S> load(Class<S> service) {
    // 这里就设置用了线程上下文加载器,即当前用户主线程的AppClassLoader来加载类
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

后续加载用户用SPI注册的各Driver类时,就是用当前设置的线程上下文加载器AppClassLoader来加载。

具体可以参考Java-SPI

2.3.2.3 Tomcat例子

2.3.3 破坏3-OSGI

第三次破坏源于用户对程序动态性的追求而导致的。这里的动态性指代码热替换HotSwap, 模块热部署等。

OSGI是业界中的Java模块化标准,实现模块化的热部署的关键则是它自定义的类加载机制的实现,即每个程序模块都要一个自己的类加载器。当需热替换时,就把程序模块连同加载器一起换掉。

在OSGi环境下,类加载器不再是双亲委派中的树状结构,而是进一步发展为网状结构。

2.4 ClassLoader的可见性

ClassLoader的可见性是指:

  • 子加载器可以看见所有的父加载器加载的类
  • 而父加载器看不到子加载器加载的类

小例子如下:

public class ClassLoaderTest {

    public static void main(String args[]) {
        try {
            // print ClassLoader of this class
            System.out.println("ClassLoaderTest.getClass().getClassLoader() : "
                                 + ClassLoaderTest.class.getClassLoader());

            // trying to explicitly load this class again using Extension class loader
            Class.forName("test.ClassLoaderTest", true
                            ,  ClassLoaderTest.class.getClassLoader().getParent());
        } catch (ClassNotFoundException ex) {
            Logger.getLogger(ClassLoaderTest.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

}

该例子会抛出ClassNotFoundException,也就是说ExtClassLoader不能看见其子ClassLoader AppClassLoader所加载的类。

2.5 ClassLoader的单例性

ClassLoader的单例性是指:

  • 在同一个ClassLoader范围内,只会加载同一个类一次。这是由双亲委派机制保证的。
  • 但不同的ClassLoader可以通过重写loadClass方法破坏双亲委派机制,从而使得一个JVM内,一个类有多个不同ClassLoader加载生成的Class对象放在内存。但需要注意的是,他们之间不能互相转换!

2.6 ClassLoader与Class类型比较

两个对象的Class类型是否相同,判断条件有两个:

  • 两个Class全限定名相同
  • 两个Class的加载时的ClassLoader是否相同

以下代码可以简单验证:

自定义类加载器如下,注意重写了loadClass方法,破坏了双亲委派机制:

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Objects;

public class CustomClassLoader extends ClassLoader
{
    private static String BASE_PATH = CustomClassLoader.class.getResource("/").getPath();
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException
    {
        try {
            Class findClass = findLoadedClass(name);
            if (!Objects.isNull(findClass)) {
                return findClass;
            }
            //包+类完整磁盘路径
            String filePath = BASE_PATH + name.replace(".", File.separator) + ".class";
            File file = new File(filePath);
            if(!file.exists()){
                return super.loadClass(name);
            }
            try(FileInputStream fis = new FileInputStream(file)){
                if(fis == null){
                    return super.loadClass(name);
                }
                byte[] b = new byte[fis.available()];
                fis.read(b);
                return defineClass(name, b, 0, b.length);
            }

        }
        catch (IOException e) {
            throw new ClassNotFoundException(name);
        }
    }
}

测试代码如下:

public class CusClassLoaderClassConvertTest {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        ClassLoader myLoader = new CustomClassLoader();
        String subCompanyFullName = "demos.classInitialization.classloader.custom.SubCompany";
        SubCompany subCompany = (SubCompany)(myLoader.loadClass(subCompanyFullName).newInstance());
    }
}

最终输出如下:

Exception in thread "main" java.lang.ClassCastException: 
demos.classInitialization.classloader.custom.SubCompany cannot be cast 
to demos.classInitialization.classloader.custom.SubCompany
	at demos.classInitialization.classloader.custom.CusClassLoaderClassConvertTest.main(CusClassLoaderClassConvertTest.java:7)

可以看到,全限定相同的类因为ClassLoader不同,导致转换失败,抛出异常ClassCastException

2.7 ClassLoader实例隔离性

2.7.1 概述

父加载器加载的类不能直接使用未加载的子加载器加载的类

比如有一个由AppClassLoader加载的A类,使用一个由来自于网络上的类B,由于此时只能用当前A类进行父类委托加载,所以无法加载来自于网络的类B,而必须提前用专用的网络类ClassLoader加载B后才能使用!这又就是所谓隔离性。

位于不同分支的不同ClassLoader加载的类不能相互引用。要了解这个原因,先认识几个概念:

2.7.2 定义类加载器

指真正加载类的ClassLoader。如java.lang.Object的定义类加载器就是BootstrapClassLoader

2.7.3 初始类加载器

指参与加载类的ClassLoader。如java.lang.Object的初始类加载器就是BootstrapClassLoader, ExtClassLoaderAppClassLoader(父类委托)。

2.7.4 命名空间

JVM为每个ClassLoader实例维护了一个类似表的结构,记录该ClassLoader实例作为初始类加载器参与加载的所有类,这称为命名空间,可隔离不同类加载器实例加载的Class文件。

注意是ClassLoader实例,比如AppClassLoader加载器有两个实例对象loader1和loader2,那么这两个实例对象加载的Class文件也是互相隔离。

所以用户代码可以使用java.lang.Object,是因为他们都在AppClassLoader的命名空间中。

而反过来,java.lang.Object还在BootstrapClassLoaderExtClassLoader两个命名空间,所以不能直接访问用户代码。

  • 如果两个ClassLoader之间没有直接或间接父子关系,那么他们各自加载的类互不可见。
  • 当两个不同命名空间的类互相不可见时,可采用反射机制来访问对方的属性和方法。

2.7.5 不同加载器加载的相同类不可相互转化

3 ClassLoader源码解析

3.1 ClassLoader.loadClass

加载类的主要的方法是loadClass,代码如下:

// 父加载器
private final ClassLoader parent;
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        // 先在ClassLoader获取该ClassName对应的同步锁对象
        // 注意,这里的并行度是按每个类锁定
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            // 先检查该类全限定名是否已经被加载到JVM
            Class<?> c = findLoadedClass(name);
            if (c == null) {
            // 此时没有加载该类
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                    // 存在父加载器(不是BootStrapClassLoader)
                        // 就尝试让父加载器加载类,但不连接
                        c = parent.loadClass(name, false);
                    } else {
                        // 否则尝试用BootStrapClassLoader加载该类
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                // 此时还没有加载该类
                    long t1 = System.nanoTime();
                    // 就调用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;
        }
    }

3.2 ClassLoader.findClass

但在JDK1.2之后,不再提倡重写loadClass(),而是应该重写新加入的findClass方法:

protected Class<?> findClass(String name) throws ClassNotFoundException {
	// 该方法默认没有实现,只是抛出一个ClassNotFoundException
    throw new ClassNotFoundException(name);
}

3.3 URLClassLoader.findClass

启动类加载器BootstrapClassLoader是用C++实现,而扩展类加载器ExtClassLoader和应用程序类加载器AppClassLoader都继承自URLClassLoaderfindClass方法直接用的URLClassLoaderfindClass方法:

// 要加载的类的全限定名
// 比如是demos.classInitialization.classloader.order.EntityC
protected Class<?> findClass(final String name)
        throws ClassNotFoundException
{
    final Class<?> result;
    try {
        result = AccessController.doPrivileged(
            new PrivilegedExceptionAction<Class<?>>() {
                public Class<?> run() throws ClassNotFoundException {
                    // 那么这里path为demos/classInitialization/classloader/order/EntityC.class
                    String path = name.replace('.', '/').concat(".class");
                    Resource res = ucp.getResource(path, false);
                    if (res != null) {
                        try {
                            // 使用从指定Resource获取的类字节来转为Class对象 
                            // 必须先连接,然后生成的类才能使用它。
                            return defineClass(name, res);
                        } catch (IOException e) {
                            throw new ClassNotFoundException(name, e);
                        }
                    } else {
                        return null;
                    }
                }
            }, acc);
    } catch (java.security.PrivilegedActionException pae) {
        throw (ClassNotFoundException) pae.getException();
    }
    if (result == null) {
        // 如果结果为空,直接抛ClassNotFoundException
        throw new ClassNotFoundException(name);
    }
    // 返回得到的Class对象
    return result;
}

3.4 URLClassLoader.defineClass

接着看看URLClassLoaderdefineClass方法:

/*
 * 使用从指定的资源中获取的class bytes 来定义一个Class对象
 * 最终得到的Class必须在使用前被解析
 */
private Class<?> defineClass(String name, Resource res) throws IOException {
    long t0 = System.nanoTime();
    int i = name.lastIndexOf('.');
    // file:/xxx/javaDemos/target/classes/
    URL url = res.getCodeSourceURL();
    if (i != -1) {
        String pkgname = name.substring(0, i);
        // 检查包是否已经加载过
        Manifest man = res.getManifest();
        // 
        definePackageInternal(pkgname, man, url);
    }
    // 从class字节码中读取数据并转为Class
    java.nio.ByteBuffer bb = res.getByteBuffer();
    if (bb != null) {
        // 直接使用ByteBuffer读取(字节缓冲)
        CodeSigner[] signers = res.getCodeSigners();
        CodeSource cs = new CodeSource(url, signers);
        sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
        return defineClass(name, bb, cs);
    } else {
        byte[] b = res.getBytes();
        // must read certificates AFTER reading bytes.
        CodeSigner[] signers = res.getCodeSigners();
        CodeSource cs = new CodeSource(url, signers);
        sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
        return defineClass(name, b, 0, b.length, cs);
    }
}

3.5 ClassLoader.getSystemClassLoader

所有继承自ClassLoader且没有重写getSystemClassLoader方法的ClassLoader,通过getSystemClassLoader方法得到的AppClassLoader都是同一个实例,类似单例模式。

// system ClassLoader
private static ClassLoader scl;
// 一旦system ClassLoader被设置后,就把sclSet设为true
private static boolean sclSet;

@CallerSensitive
public static ClassLoader getSystemClassLoader() {
    // 初始化SystemClassLoader(AppClassLoader)
    initSystemClassLoader();
    if (scl == null) {
        return null;
    }
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkClassLoaderPermission(scl, Reflection.getCallerClass());
    }
    // 返回的就是AppClassLoader
    return scl;
}

3.6 源码小结

其实ClassLoader加载类的流程就是以下几步:

  1. JVM调用ClassLoader.loadClass方法
  2. 按类来获取同步锁,保证类加载的线程安全
  3. 存在父加载器(不是BootStrapClassLoader),就尝试让父加载器加载类,但不连接
  4. 否则尝试用BootStrapClassLoader加载该类
  5. 如果父类不能加载,就自己尝试加载
  6. 以上几步如果成功会得到代表该类的Class对象,失败抛出ClassNotFoundException
  7. 如果resolve等于true,还需要调用resolveClass方法连接该Class对象
  8. 最后返回得到的Class对象

4 类加载器应用

4.1 Tomcat

4.1.1 简介

主流的Java Web服务器如Tomcat, Jetty等都实现了自定义的类加载器,原因如下:

  1. 同服务器上的多个Web应用程序所使用的Java类库相互隔离。比如,他们使用了同一个类库的不同版本。一般服务器需要保证类库可以被若干引用互相隔离独立使用。
  2. 部署在同个服务器上的两个Web应用程序使用的相同Java类库可共享。比如一个Tomcat内的不用应用程序都使用Sping,如果目录放置多个Spring类库,一是会浪费磁盘空间,二是加载列库到服务器内存时如果不能共享同时有多份会浪费内存,且方法区可能过度膨胀。
  3. 应用服务器本身有的也是用Java实现,必须与应用隔离,保证服务器使用的类库安全独立。
  4. 支持JSP应用的Web服务器,大多需要支持HotSwap功能。

4.1.2 Tomcat 7.x的ClassLoader机制

Tomcat 7.x,目录结构如下:
Tomcat7
与许多服务器应用相同,Tomcat有各种类加载器(实现自java.lang.ClassLoader),以允许容器的不同部分和容器上运行的Web应用程序能访问不同的用于存放class和资源文件的库。此机制用于提供Servlet规范2.4版中定义的功能。

在Web应用中的ClassLoader机制和Java中的双亲委派机制有少许不同,但总体原则相同。Tomcat7.x在启动时会创建一组ClassLoader,也有父子关系,模型如下:
Tomcat7ClassLoader

  • Bootstrap
    包含由JVM提供的基础运行时class文件,以及$JAVA_HOME/jre/lib/ext下的扩展jar文件。
  • System
    SystemClassLoaderCLASSPATH的环境变量初始化。**所有通过SystemClassLoader加载的类都对Tomcat内部类和Web应用程序可见。**但使用bin/catalina.sh启动时会忽略CLASSPATH环境变量,而是从以下位置构建SystemClassLoader:
    • bin/bootstrap.jar
      包含用于初始化Tomcat服务器的main()方法,以及它依赖的ClassLoader实现类。
    • bin/tomcat-juli.jar或bin/tomcat-juli.jar
      日志相关实现类
    • bin/commons-daemon.jar
      来自Apache Commons Daemon项目的类。 这个JAR文件不存在于catalina.bat | .sh脚本构建的CLASSPATH中,而是从bootstrap.jarmanifest文件中引用。
  • Common
    CommonClassLoader包含对Tomcat内部类和所有Web应用程序都可见的其他类。
    通常,不应将用户的应用代码类放在此处。CommonClassLoader搜索的位置由conf/catalina.properties中的common.loader属性定义。 默认设置将按列出的顺序搜索:
    ${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar
    即包含lib下的未打包的class文件和资源文件及已打包的jar文件,如下图:
    tomcat/lib/
  • WebappX
    会为部署在单个Tomcat实例中的每个Web应用程序创建一个WebappClassLoader。可以让WEB-INF/classes里的class和资源文件以及WEB-INF/lib里的jar文件仅对当前单个Web应用程序可见,而对其他Web应用不可见,保证隔离性。

如上所述,Tomcat的Web ClassLoader模型与Java的不同(根据Servlet规范2.4版,第9.7.2节Web应用程序类加载器中的建议)。 当处理Web应用程序的WebappX ClassLoader发起的加载类的请求时,此ClassLoader将首先在本地库查找,而不是像Java委派模型那样委派给父加载器。

有一种例外情况,即作为JRE基类的一部分的那些类不能被覆盖。 对于某些类(例如J2SE 1.4+中的XML解析器组件),Java认可的功能可以用于Java 8.

最后,类加载器将显式忽略包含Servlet API类的任何JAR文件 - 也就是说,不要在Web应用程序中包含此类JAR

4.1.3 小结

Tomcat中除了上面提到的WebappX ClassLoader(处理Web应用程序的WebappX ClassLoader发起的加载类的请求时,此ClassLoader将首先在本地库查找,而不是像Java委派模型那样委派给父加载器。)以外,所有其他的几种ClassLoader都遵循双亲委派模式。

因此,默认情况下,从Web应用程序的角度来看,类或资源文件的加载将按以下顺序查找以下存储库:

  1. JVM的Bootstrap classes
  2. Web应用程序的/WEB-INF/classes
  3. Web应用程序的/WEB-INF/lib/*.jar
  4. SystemClassLoader加载的类
  5. CommonClassLoader加载的类

而如果配置了<Loader delegate="true"/>表示采用双亲委派机制,那就和传统的Java加载顺序相同,具体顺序如下:

  1. JVM的Bootstrap classes
  2. SystemClassLoader加载的类
  3. CommonClassLoader加载的类
  4. Web应用程序的/WEB-INF/classes
  5. Web应用程序的/WEB-INF/lib/*.jar

此外,还可以配置更复杂的共享模式架构:
Tomcat7Shared
默认情况下,未定义ServerShared ClassLoader。 通过在conf/catalina.properties中定义server.loader和或shared.loader属性的值,可以使用这种更复杂的层次结构:

  • Server
    ServerClassLoader加载的类值对Tomcat内部类可见,对用户Web应用程序不可见。相当于Tomcat专属ClassLoader。
  • Shared
    SharedClassLoader对所有用户Web应用程序都可见,可用在他们之间共享代码。然而,任何对shared代码的修改都需要Tomcat服务重启。

还可以参考这篇文章Tomcat7源码解读(四) —— 类加载器1

4.2 URLClassLoader

来自Apache Zeppelin 的org.apache.zeppelin.plugin.PluginManager的一段代码,用来加载不在classPath的plugin jar的代码:

// launcherPlugin为目标类全限定名
public synchronized InterpreterLauncher loadInterpreterLauncher(String launcherPlugin,
                                                                   RecoveryStorage recoveryStorage)
          throws IOException {
	// 存储实例对象缓存
  if (cachedLaunchers.containsKey(launcherPlugin)) {
    return cachedLaunchers.get(launcherPlugin);
  }
  String launcherClassName = "org.apache.zeppelin.interpreter.launcher." + launcherPlugin;
  LOGGER.info("Loading Interpreter Launcher Plugin: " + launcherClassName);
  // 如果目标类属于系统自带的,则已加入classpath,可直接用AppClassLoader加载
  if (builtinLauncherClassNames.contains(launcherClassName) ||
          Boolean.parseBoolean(System.getProperty("zeppelin.isTest", "false"))) {
    try {
      InterpreterLauncher launcher = (InterpreterLauncher)
              (Class.forName(launcherClassName))
                      .getConstructor(ZeppelinConfiguration.class, RecoveryStorage.class)
                      .newInstance(zConf, recoveryStorage);
      return launcher;
    } catch (InstantiationException | IllegalAccessException | ClassNotFoundException
            | NoSuchMethodException | InvocationTargetException e) {
      throw new IOException("Fail to instantiate InterpreterLauncher from classpath directly:"
              + launcherClassName, e);
    }
  }
	// 获取填充了要加载的目标地址的URLClassLoader实例
  URLClassLoader pluginClassLoader = getPluginClassLoader(pluginsDir, "Launcher", launcherPlugin);
  InterpreterLauncher launcher = null;
  try {
  	// 使用URLClassLoader加载目标类、初始化,此后会将所有url list的jar加入classpath
    launcher = (InterpreterLauncher) (Class.forName(launcherClassName, true, pluginClassLoader))
            .getConstructor(ZeppelinConfiguration.class, RecoveryStorage.class)
            .newInstance(zConf, recoveryStorage);
  } catch (InstantiationException | IllegalAccessException | ClassNotFoundException
          | NoSuchMethodException | InvocationTargetException e) {
    throw new IOException("Fail to instantiate Launcher " + launcherPlugin +
            " from plugin pluginDir: " + pluginsDir, e);
  }
	// 将加载好的类实例放入缓存
  cachedLaunchers.put(launcherPlugin, launcher);
  return launcher;
}
// 获取填充了要加载的目标地址的URLClassLoader实例
private URLClassLoader getPluginClassLoader(String pluginsDir,
                                            String pluginType,
                                            String pluginName) throws IOException {
	// 拼接目录路径,如/zeppelin/plugins/Launcher/YarnInterpreterLauncher
  File pluginFolder = new File(pluginsDir + "/" + pluginType + "/" + pluginName);
  if (!pluginFolder.exists() || pluginFolder.isFile()) {
    LOGGER.warn("PluginFolder " + pluginFolder.getAbsolutePath() +
            " doesn't exist or is not a directory");
    return null;
  }
  List<URL> urls = new ArrayList<>();
  // 遍历目标目录,将下面的所有jar全部添加到url list
  for (File file : pluginFolder.listFiles()) {
    LOGGER.debug("Add file " + file.getAbsolutePath() + " to classpath of plugin: "
            + pluginName);
    urls.add(file.toURI().toURL());
  }
  if (urls.isEmpty()) {
    LOGGER.warn("Can not load plugin " + pluginName +
            ", because the plugin folder " + pluginFolder + " is empty.");
    return null;
  }
  // 返回填充了要加载的目标地址的URLClassLoader实例
  return new URLClassLoader(urls.toArray(new URL[0]));
}

参考文档

-《深入理解JVM虚拟机》

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值