ClassLoader类加载器

Author Avatar
丁起男 01月 13,2021
  • 在其它设备中阅读本文章

ClassLoader类加载器

加载

将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在内存上创建一个java.lang.Class对象用来封装类在方法区内的数据结构作为这个类的各种数据的访问入口

验证

主要是为了确保class文件中的字节流包含的信息是否符合当前jvm的要求,且不会危害jvm自身安全,比如校验文件格式、是否是cafe baby魔数、字节码验证等待

准备

为类变量分配内存并设置类变量(是被static修饰的变量,变量不是常量,所以不是final的,就是static的)初始值的阶段。这些变量所使用的内存在方法区中进行分配。比如private static int age = 26;类变量age会在准备阶段过后为起分配四个字节(int是4个字节)的空间,并且设置初始值为0,而不是26

若是final的,则在编译期就会设置上最终值

解析

jvm会在此阶段把类的二进制数据中的符号引用替换为直接引用

初始化

初始化阶段是执行类构造器<clinit>()方法的过程,到了初始化阶段,才真正开始执行类定义的java程序代码(或者说字节码)。比如准备阶段哪个age初始值是0,到这一步就设置为26

使用

对象都出来了,业务系统直接调用

卸载

用完了,可以被回收了

类加载器种类

启动类加载器(Bootstrap ClassLoader)

最顶层类加载器,他的父类加载器是个null,也就是没有父类加载器。负责加载jvm的核心类库,比如java.lang.*等,从系统属性中的sun.boot.class.path所指定的目录中加载类库。他的具体实现由java虚拟机底层c++实现

扩展类加载器(Extension ClassLoader)

父类加载器是启动类加载器。从java.ext.dirs系统属性所指定的目录中加载类库,或者从jdk的安装目录的jre/lib/ext子目录(扩展目录)下加载类库,如果把用户的jar文件放在这个目录下,也会自动由扩展类加载器加载。继承自java.lang.ClassLoader

应用程序类加载器(Application ClassLoader)

父类加载器是扩展类加载器。从环境变量classpath或者系统属性java.class.path所指定的目录中加载类。继承自java.lang.ClassLoader

自定义类加载器(User ClassLoader)

除了上面三个自带的以外,用户还能指定自动的类加载器,但是所有自定义的类加载器都应该继承java.lang.ClassLoader。比如热部署、tomcat都会用到自定义类加载器

双亲委派

如果一个类加载器收到了类加载的请求,他首先会从自己缓存里查找是否之前加载过这个class,加载过直接返回,没加载过的话他也不会自己去加载,而是把这个请求委派给父类加载器去完成,每一层都是如此,类似递归,一直递归到顶层父类

也就是启动类加载器,只有加载完成就会返回结果,如果顶层父类加载器无法加载此class,则会返回去交给子类加载器去尝试加载,若最底层的子类加载器也没有找到则会抛出ClassNotFoundException

为什么要有双亲委派

比如自己重写个java.lang.Object并放到Classpath中,没有双亲委派的话直接自己执行了,那不安全。

双亲委派开源保证这个类只能被顶层Bootstrap ClassLoader类加载器加载,从而确保只有jvm中有且仅有一份正常的java核心类。

如果有多个的话,那么就乱套了。比如形态的类instanceof 可能返回false,因为可能父类不是同一个类加载器加载的Object。

为什么需要破坏双亲委派模型

jdbc:

以前的用法是未破坏双亲委派模型的,比如Class.forName("com.mysql.cj.jdbc.Driver")

而在jdbc4.0以后,开始支持使用spi的方式来注册这个Driver,具体做法就是在MySQL的jar包中的META-INF/services/java.sql.Driver文件中指明当前使用的Driver是哪个,然后使用的时候就不需要我们手动的去加载驱动了,我们只需要直接获取连接就可以了。(connection con = DriverManager.getConnection(url,username,password);)

首先,理解一下为什么jdbc需要破坏双亲委派模式,原因是原生的jdbc中Driver驱动本身只是一个接口,并没有具体的实现,具体的实现是由不同数据类型去实现的。例如,MySQL的mysql-connector-*.jar中的Driver类具体实现的

原生的jdbc中的类是放在rt.jar包的,是由Bootstrap加载器进行类加载的,在jdbc中的Driver类中需要动态去加载不同数据库类型的Driver类,而mysql-connector-*.jar中的Driver类是用户自己写的代码,那Bootstrap类加载器肯定是不能进行加载的,既然是自己编写的代码,那就需要有Application类加载器去进行类加载

这个时候就引入线程上下文文件类加载器(Thread Context ClassLoader),通过这个东西程序就可以把原本需要由BootStrap类加载器进行加载的类由Application类加载器进行加载了

tomcat:

一个tomcat可以部署n个web应用,但是每个web应用都有自己的classloader,互不干扰。比如web1里面有com.test.A.class,web2里面也有com.test.A.class,如果没有打破双亲委派的话,那么web1加载完成后,web2在加载的话会冲突

因为只有一套classloader,却出现了两个重复的路径,所以tomcat打破了,他是线程级别的,不同web应用是不同的classloader

java spi方式:

比如jdbc4.0开始就是其中之一

热部署:

如何打破双亲委派

重写loadClass方法,别重写findClass方法,因为loadClass是核心入口,将其重写成自定义逻辑可以破坏双亲委派模型

如何自定义一个类加载器

只需要继承java.lang.ClassLoader类,然后覆盖他的findClass(String name)方法即可,该方法根据参数指定的类名称,返回对应的Class对象的引用

热部署原理

采取双亲委派模型的手段来实现热部署,默认的loadClass()方法先找缓存,你改类class字节码也不会热加载,所以自定义ClassLoader,去掉找缓存那部分,直接就去加载,也就是每次都重新加载

原文:https://mp.weixin.qq.com/s/2iGaiOpxBIM3msAZYPUOnQ