ClassLoader小记

发布时间 2023-10-25 15:08:20作者: ecnu_lxz

类加载器小记

作用

用来加载 Class,寻找类或接口字节码文件进行解析并构造JVM内部对象,负责将 Class 的字节码形式转换成内存形式的 Class 对象。
字节码可以来自于磁盘文件 *.class,也可以是 jar 包里的 *.class,也可以来自远程服务器提供的字节流,字节码的本质就是一个字节数组[]byte,字节流
工作具体点就是,静态变量分配空间,静态变量和代码块初始化,处理变量符号引用
Java内置了3个ClassLoader,BootstrapClassLoaderExtensionClassLoaderAppClassLoader

联系,对比

具有C++中的命名空间的隔离作用,位于不同(没有祖先关系的)ClassLoader 中名称一样的类实际上是不同的类,每一个类加载器都包含多个类,每一个类内部都会记录加载该类的类加载器

延迟加载,懒加载

JVM 运行并不是一次性加载所需要的全部类的,程序在运行的过程中会逐渐遇到很多不认识的新类,这时候就会调用 ClassLoader 来加载这些类。加载完成后就会将 Class 对象记录在 ClassLoader 里面
在调用某个类的静态方法时,首先这个类肯定是需要被加载的,但是并不会触及这个类的属性字段,那么属性字段的类别 Class 就可以暂时不必去加载,但是它可能会加载静态字段相关的类别,因为静态方法会访问静态字段,普通的属性字段的类别需要等到你实例化对象的时候才可能会加载。

Parents Delegate,所谓双亲委派,代理模式

翻译得不太恰当吧、、、、、
ClassLoader也具有类似继承的机制,内部记录其Parent ClassLoader
想到个例子,类似于医院实习生遇到不会的求助自己导师,导师不会再求助导师的导师,挂个个实习生号最终来的可能是个院士
即,递归进行求助

作用

这样可以避免重复加载,当Parent ClassLoader已经加载了该类的时候,就没有必要子ClassLoader再加载一次
如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义类型,这样会存在非常大的安全隐患
但是完全可以自己写一个ClassLoader来加载自己写的java.lang.String类啊
但是会发现也不会加载成功,因为java.*开头的类,必须由bootstrapclassloader来加载
这样的机制限制会造成一定的问题,后面打破双亲委派机制再讲

加载类的流程,啃老

首先是BootstrapClassLoader加载核心类,ExtensionClassLoader加载扩展类,AppClassLoader加载用户类
AppClassLoader在加载新的类时,会先交给Parent ClassLoader做,解决不了的,自己再想办法,这叫啃老

未知类的类加载器,全盘负责委托机制

运行过程中,遇到了一个未知的类,会选择哪个 ClassLoader 来加载它呢?
当前执行的方法所在的Class,该Class的ClassLoader负责加载遇到的未知类,负责到底
全盘负责是指当一个ClassLoder装载一个类时,除非显式地指明使用另外的ClassLoder,该类所依赖及引用的类也由这个ClassLoder载入

BootstrapClassLoader

没有Parent ClassLoader
是用C/C++写的,用于加载JVM运行时的核心类,如java.util.*java.io.*java.lang.*
如果一个类的加载器是BootstrapClassLoader,那该类中记录的类加载器就是null
我理解起来就是,BootstrapClassLoader加载核心类之前,内存中啥对象也没有,类似于最初的创建人,加载完一切后就自己退出该游戏圈了,并没有在内存中构建自己,别人就无法引用它

ExtensionClassLoader

Parent ClassLoader为null,表示BootstrapClassLoader
负责加载 JVM 扩展类,比如 swing 系列、内置的 js 引擎、xml 解析器 等等,这些库名通常以 javax 开头,它们的 jar 包位于 JAVA_HOME/lib/ext/*.jar

AppClassLoader

Java默认的类加载器
Parent ClassLoader为ExtensionClassLoader
用于加载 Classpath 环境变量里定义的路径中的 jar 包和目录。程序员编写的代码以及使用的第三方 jar 包通常都是由它来加载

JAVA类装载代码

隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中。
显式装载, 通过Class.forname()等方法,显式加载需要的类,Class.forname()中可以指定使用哪个类加载器

打破Parent Delegate

前面提到的ClassLoader的代理模式机制限制,会造成一定的问题,问题就在于Java允许第三方提供实现代码,自己仅仅定义接口规范
Java 提供了Service Provider Interface,SPI,允许第三方为这些接口提供实现
常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等
这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)
SPI 的接口规范是 Java 核心库的一部分,是由BootstrapClassLoader加载
那么SPI 的实现类什么时候加载?当然是触发未知类加载的时候,也即是SPI接口变量指向一个具体实现类时
比如SPI接口名为A,在执行A a = new xxxxx()
由于是A进行的调用,根据上面的全盘负责机制,将由A的ClassLoader来加载实现代码
但是A的ClassLoader是BootstrapClassLoader吖,无能为力
所以这个时候需要切换一下,指定AppClassLoader来进行加载,这就轮到contextClassLoader上场了

线程中的contextClassLoader

contextClassLoader的作用就是为了破坏Java类加载代理委托机制,使程序可以逆向使用类加载器
Thread类的实现中,有一个属性字段是contextClassLoader,指向一个类加载器
默认情况下和父线程的类加载器一样,因为是复制过来的
默认情况下线程的contextClassLoader默认就是AppClassLoader
在 SPI 接口的实现代码中使用线程contextClassLoader,来加载 SPI 实现的类

举例JNDI,JDBC实现类:

为了能让应用程序访问到这些jar包中的实现类,要用appClassLoarder去加载这些实现类
先用getContextClassLoader取得当前线程的ClassLoader(即appClassLoarder),再去加载这些实现类即可

其他作用

跨线程共享类,同一个ClassLoader内部包含的类是一样的
如果不同的线程使用不同的 contextClassLoader,那么不同的线程使用的类就可以隔离开来
如果我们对业务进行划分,不同的业务使用不同的线程池,线程池内部共享同一个 contextClassLoader,线程池之间使用不同的 contextClassLoader,就可以很好的起到隔离保护的作用,避免类版本冲突