张治峰的博客

类加载器子系统

2020-08-09

传送门—>> JDK源码级别理解双亲委派机制

类加载过程

一个类的加载过程有下面几步:加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载

  1. 加载: 在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的 main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的 java.lang.Class对象,存入方法区作为这个类的各种数据的访问入口。
  2. 验证: 校验字节码文件的正确性
  3. 准备: 给类的静态变量分配内存,并赋予默认值
  4. 解析: 将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过 程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用。
  5. 初始化: 对类的静态变量初始化为指定的值,执行静态代码块

类加载器

JVM中有两种类型的类加载器,由C++编写的及由Java编写的。除了启动类加载器(Bootstrap Class Loader)是由C++编写的,其他都是由Java编写的。由Java编写的类加载器都继承自类java.lang.ClassLoader。各种类加载器之间存在逻辑上的父子关系,后面可以通过代码查看。

类加载器父子关系从左到右 BootstrapClassLoader->ExtClassLoader->AppClassLoader->自定义类加载器

启动类加载器

因为启动类加载器是由C++编写的,通过Java程序去查看显示的是null,因此,启动类加载器无法被Java程序调用启动类加载器不像其他类加载器有实体,它是没有实体的,JVM将C++处理类加载的一套逻辑定义为启动类加载器。

/**
* 查看类加载器的默认加载路径 也可以通过-Xbootclasspath 自行指定
*/
public class ClassLoaderTest {
public static void main(String[] args) {
URL[] urls = Launcher.getBootstrapClassPath().getURLs();
for (URL url:urls){
System.out.println(url);
}
}
}
结果为固定路径:
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/classes

启动类加载器实际上是一段c++代码逻辑,具体看文章开头 源码级别理解类加载器。

拓展类加载器

查看拓展类加载路径 也可以通过java.ext.dirs指定

public static void main(String[] args) {
ClassLoader classLoader = ClassLoader.getSystemClassLoader().getParent();
URLClassLoader urlClassLoader = (URLClassLoader) classLoader;
URL[] urls = urlClassLoader.getURLs();
for (URL url : urls) {
System.out.println(url);
}
}
结果:
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/sunec.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/nashorn.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/cldrdata.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/jfxrt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/dnsns.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/localedata.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/jaccess.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/zipfs.jar
file:/System/Library/Java/Extensions/MRJToolkit.jar

应用类加载器

默认加载用户程序的类加载器AppClassLoader

/**
* 查看加载路径 可以通过java.class.path指定
*/
public static void main(String[] args) {
String[] urls = System.getProperty("java.class.path").split(":");
for (String url : urls) {
System.out.println(url);
}
System.out.println("================================");
URLClassLoader classLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
URL[] urls1 = classLoader.getURLs();
for (URL url : urls1) {
System.out.println(url);
}
}
结果为你项目本地包所在路径

自定义类加载器

自定义类加载器 继承类java.lang.ClassLoader

public class ClassLoadeTest1 extends ClassLoader{
public static void main(String[] args) throws ClassNotFoundException {
ClassLoadeTest1 classLoadeTest1 = new ClassLoadeTest1();
Class<?> aClass = classLoadeTest1.loadClass(Demo1.class.getName());
System.out.println(aClass);
System.out.println(aClass.getClassLoader());
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
System.out.println("findClass");
return null;
}
}
结果:
class com.example.classloade.Demo1
sun.misc.Launcher$AppClassLoader@18b4aac2
因为类加载中的双亲委派 所以打印出的加载类为AppClassLoader 这也是自定义的默认加载器

自定义类加载器也可以打破双亲委派,看源码 此处代码说明类加载器父子关系图

    /**
* 类加载过程如下
* 自定义加载器只需要重写loadClass 在向上委托判断初改成自己加载逻辑即打破双亲委派
**/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException{

synchronized (getClassLoadingLock(name)) {
//判断类是否加载过
Class<?> c = findLoadedClass(name);
//==null 没有加载
if (c == null) {
long t0 = System.nanoTime();
try {
//判断是否有父级 (这块逻辑重写 为自己加载 即打破类双亲委派)
if (parent != null) {
//有父级委托父级加载 向上委派
c = parent.loadClass(name, false);
} else {
//没有的话直接使用启动类加载器 最后调用的是navicat方法 进入c++
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();
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;
}
}

类加载器创建链

刚才已经讲了启动类加载器是没有没有实体的,只是将一段c++代码逻辑命名成启动类加载器。具体父子关系建立 见 JDK源码级别理解双亲委派机制

类加载器加载完成后的存储

类加载完成会存储在方法区 而不同的类加载器加载的类都有其相对应的一块区域。如果不同的加载器加载了同一个class 那么他们是两份数据当然也不是同一个类了

双亲委派机制

如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到顶端的启动类加载器;只有当父类加载器在其搜索范围内无法找到所需的类,并将该结果反馈给子类加载器,子类加载器会尝试去自己加载。

双亲委派图解

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

  1. 沙箱安全机制

    自己写的java.lang.String.class类不会被加载,这样便可以防止核心代码被篡改。如下代码

    package java.lang;

    /**
    * @author zhangzhifeng
    */
    public class String {
    public static void main(String[] args) {
    System.out.println("测试自定义 java.lang.String 类 是否能加载!");
    }
    }
    运行结果:因为String 类是jdk自带的类 而它并没有main方法 所以报错
  2. 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性

    如果多个类加载器 的加载路径下面都存在同一个类,只需要向上委托加载一次即可.

打破双亲委派

因为在某些情况下父类加载器需要委托子类加载器去加载class文件。受到加载范围的限制,父类加载器无法加载到需要的文件,以Driver接口为例,由于Driver接口定义在jdk当中的,而其实现由各个数据库的服务商来提供,比如mysql的就写了MySQL Connector,那么问题就来了,DriverManager(也由jdk提供)要加载各个实现了Driver接口的实现类,然后进行管理,但是DriverManager由启动类加载器加载,只能加载JAVA_HOME的lib下文件,而其实现是由服务商提供的,由系统类加载器加载,这个时候就需要启动类加载器来委托子类来加载Driver实现,从而破坏了双亲委派。类似这样的情况就需要打破双亲委派。打破双亲委派的意思其实就是不委派、向下委派

方式

  1. 自定义类加载器
    继承类ClassLoder 重写loadClass 方法 将向上委派代码进行修改即可
  2. SPI
    是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。这一机制为很多框架扩展提供了可能,比如在Dubbo、JDBC中都使用到了SPI机制

线程上下文类加载器

  1. 是什么
    一种特殊的类加载器,可以通过Thread获取,基于此可实现逆向委托加载
  2. 存在的理由
    为了解决双亲委派的缺陷而生
  3. 如何使用
    //获取
    Thread.currentThread().getContextClassLoader()
    //设置
    Thread.currentThread().setContextClassLoader(new Classloader_4());

全盘负责委托机制

“全盘负责”是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类 所依赖及引用的类也由这个ClassLoder载入。

Tags: java
使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章