虚拟机设计团队把类加载阶段中“通过一个类的全限定名来获取描述此类的二进制字节流”的这个动作放在Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码模块称为 “类加载器”
简而言之,类加载器就是在类加载过程的加载阶段中完成获取某类的二进制字节流的一个东西(代码模块),我们通过这个东西去获取类的字节码信息,并存放到方法区和生成该类的Class对象。
显式加载:
Class.forName
或 ClassLoader.loadClass
去主动加载某个类
隐式加载:
如new
了某个类,会隐式的触发该类的加载,在Java的双亲委派模型机制中,通常是由应用委托到扩展委托到根类加载器,然后不行再回调回来
显示加载:
一般显式加载是为了指定什么类加载去加载什么类;或者想在使用之前先加载类
对于任意一个类,都需要由加载它的类加载器和这个类本身一起确立其在Java虚拟机中的唯一性。
什么意思呢?就是说,如果你要在Java程序中判断类是否相等,既是否是同一个类。那必须满足一个前提条件
意思就是说,如果现在有一个Test.class字节码文件,被两个类加载器所加载,他们所得到的类是不同的类。所以如果你要比较两个类是否相等,或两个对象的类是否相同,前提条件是必须是类由同一个类加载器锁加载,否则比较没有意义。
每个类加载器有自己的命名空间,类似的概念还有初始类加载器,定义类加载器,运行时包等,但是为了不让概念复杂化,所以这里我们只要了解,相互之间访问关系就好了
JAVA类装载器classloader和命名空间namespace - @作者:飞天金刚
不同类加载器的命名空间关系:
- 同一个命名空间内的类是相互可见的,即可以互相访问。
- 父加载器的命名空间对子加载器可见。
- 子加载器的命名空间对父加载器不可见。
- 如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类相互不可见。
委托性
可见性
唯一性
只存在两种不同的类加载器
启动类加载器是使用C++实现的,是虚拟机自身的一部分;除了启动类加载器,其他所有类加载器都是由Java语言实现的,独立于虚拟机外部,并且全部都继承于抽象类java.lang.ClassLoader
从Java开发人员的角度分类,类加载器还可以划分的更细致一些,大致分为3类
中文翻译名称是有很多的
启动类加载器又叫根类加载器,引导类加载器
它用来加载Java核心库(JAVA_HOME/jre/lib/rt.jar 或 sun.boot.class.path路径下)的内容,是由C++语言实现的,是Java虚拟机本身的部分,并不继承自java.lang.ClassLoader类
加载扩展类加载器,应用程序类加载器,并指定他们的父类加载器
sun.misc.Laucher$ExtClassLoader
实现,继承自java.lang.ClassLoadersun.misc.Laucher$AppClassLoader
实现,继承自java.lang.ClassLoader如上图展现的类加载器之间的的这种层次的关系,就称为类加载器的双亲委派模型
这里的类加载器之间的父子关系一般不会以继承的关系去实现,而是使用组合的方式来复用父加载器的代码。
总结起来就是,所有类加载器收到加载请求,第一时间是把请求转移给父类,让父类去加载,只有父类不能加载该类时,子类才会尝试自己去加载。
好处就是保证Java程序的稳定性和安全性。
比如我们的java.lang.Object类, 它是存放在rt.jar包中的,所以无论哪个类加载器要加载这个类,都会把请求转移顶层的启动类加载器去执行,因此Object类在程序的各种类加载器环境中都是同一个类。
相反如果没有实现双亲委派模型,我们用户自己编写了一个java.lang包下的Object类,那么系统将会出现多个不同的Object类,Java类体系中最基础的行为也就无法得到保证,应用程序就将会变的一片混乱。
比一定所有的类加载器都需要实现双亲委派模型,因为这不是一个强制性的约束模型,只是Java设计者推荐的。
有些程序,比如Tomcat服务器的类加载器就不完全是双亲委派模型,而是某些部分与双亲委派模型是一个相反的模型。比如类载器收到类加载请求,第一反应就是自己尝试加载,如果不行再委派给自己的父类加载器
线程上下文类加载器(ThreadContextClassLoader
),并不是一个指定的类加载器,也不特指某个类加载器,但它通常默认是应用类加载器(AppClassLoader
), 如果是Tomcat的机制,有可能是(WebApp类加载器
)。
在Java中的体现就是,我们可以通过Thread.currentThread.getContextClassLoader
去获取当前线程的上下文类加载器,这个上下文类加载器默认是应用类加载器,也可以通过Thread.currentThread.setContextClassLoader
去修改这个线程的上下文类加载器。通常情况下,在线程创建的时候就会通过setContextClassLoader
方法将应用类加载器植入
它主要的作用就是让根类加载器无法加载到的类,可以通过线程去获取应用类加载器(通常)去人为加载。 比如Java的底层提供了很多接口,比如数据库接口,和调用接口的类,这样类是由根类加载器加载的,但是具体的驱动实现是在第三方Jar中实现的,当在根类加载器加载的类中调用第三方Jar包提供的实现类时,会发现根类加载器无法加载到这些实现类,所以我们需要在调用类中通过获取线程上下文类加载器,将第三方Jar包的实现类加载到虚拟机中。
简而言之就是要解决双亲委派中的逆向问题,即父加载器要加载本应子加载器加载的类。
这是双亲委派这个模型自身的缺陷导致的。我们说,双亲委派模型很好的解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API, 但没有绝对,如果基础类调用会用户的代码怎么办呢?
为何违背双亲委派模型
这是因为common包的代码通常是在我们自己写的Java类中使用的,很少有机会被启动类加载器所加载的类
所调用;既然是在自己写的Java类中使用,另一层的意思就是基本都是被应用类加载器所加载的类
使用,所以应用类加载器所加载的类调用了某个未被加载的类,从而触发该类的隐式加载,而该类能只被应用类加载,那自然也是被应用类加载器所加载,根本就用不着通过线程上下文的方式
但在常用线程下上文类加载器处理的SPI机制中,通常, 接口和调用接口的类是由启动类加载器所加载的, 而实现类是由第三方Jar包定义的,且这个Jar不能被启动类加载器和扩展类加载器所处理,不在它们的处理范围,所以只能有应用类加载器加载;但调用实现类的类(实现类可以向上转型成接口)的类加载器无法加载这个实现类,所以只能委托应用类加载器去加载,怎么委托,就是通过打破双亲委派模型的线程上下文的方式
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取当前调用线程的类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
比如ServiceLoader的load方法,ServiceLoader是启动类加载器所加载,而传入的实现类对象ServiceLoader类中被调用,则会触发ServiceLoader的类加载器 |启动类加载器去加载实现类,但启动类加载器无法加载它;那怎么办,那只能叫能加载他们的类加载器来加载罗,那怎么叫?线程上下文类加载器出场的机会就到了,在该线程被创建的时候,应用类加载器就被植入到线程中,当到ServiceLoader要使用的时候,我就从线程中取出应用类加载器去加载启动类加载器不能加载的类
这个问题我自己也纠结了很久,也查了很多资料,猜测可能是知识深度不够。但是后来想通了一些, 虽然不是非常的确定,但是大概也能说的通,如果有更好的答案,请告诉我!!
我的观点是:
所以只要出现一个需求,需要父类加载器去加载子类加载器才能加载的类时,不用思考,这肯定需要线程上下文类加载去帮忙!!
下面这篇文章讲的很好,可以看这里
Java线程上下文加载器与SPI - @作者:ideabuffer
理解TCCL:线程上下文类加载器
jvm原理(24)通过JDBC驱动加载深刻理解线程上下文类加载器机制 - @作者:魔鬼_
也许,你会感觉到困惑,为什么Java已经有一个套类加载机制了,Tomcat又有一套类加载机制呢?这不会冲突吗?这两套加载机制是独立的?还是共存的呢?
- 首先这两套机制并不冲突,但也不是共存的状态,而是独立的;当我们仅仅是跑一个Java项目,没有用到什么Web容器时,默认的类加载机制就是Java的那套;但是当我们的Java工程被打成war/jar包放在Tomcat容器时,启动的就是Tomcat的一套类加载机制了
- 同样,我们现在流行的SpringBoot会使用嵌入式的Tomcat容器,Tomcat作为一个子组成嵌入Spring容器中,那么我们现在说的Tomcat类加载机制也就不适用了,那时就会有SpringBoot自己的一套类加载机制改进来处理
虽然在SpringBoot嵌入式容器开发中,我们现在所说的这一套类加载机制也将不适用,但是我们这里也要讲究一个历史,熟悉过去,才能更好的发展未来嘛。所以我们还是要来讲讲Tomcat容器的类加载机制到底是怎么样的?相比Java默认的类加载机制又有什么好处?
下图是Tomcat在Java的基础上改进后的类加载机制
但它不仅限于此,它也在应用类加载器的基础上扩展了一些子类加载器, 比如Common类加载器
Common类加载器
而 Common类加载器又在自己的基础上扩展了Catalina类加载器和Shared类加载器
Catalina类加载器
Shared类加载器
当然还有 Shared类加载器的子类加载器:WebApp类加载器
我们常见的就是 Common类加载器
、Catalina类加载器
和Shared类加载器
以及WebApp类加载器
等几个类加载器,他们对于加载的目录就是 /common/* 、/server/* 、/shared/* (不过在这三个目录在tomcat 6之后已经合并到根目录下的lib目录下)和 /WebApp/WEB-INF/* 中的Java类库;其中WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器。
Common类加载器
Catalina类加载器
Shared类加载器
WebApp类加载器
另外通常情况下,Catalina类加载器和Shared类加载器不一定需要,所以Tomcat可能只会创建Common类加载器和WebApp类加载器
所以从上面的设计中,我们就可以知道,这样的设计是为了做隔离和避免重复加载操作,因为一个Tomcat容器只有一个进程,可能只启动一个JVM实例,但是却可以部署多个Java Web项目;这么多个Java独立项目共享一个虚拟机实例,那默认的Java类加载机制就无法满足了,所以Tomcat设计了比较复杂的类加载机制层级去保证各个Java应用共享虚拟机却能保证一定的隔离性,互补影响,互补干扰;也为了让多个实例有共有引用时,不用重复加载,只加载一次即可
我们知道双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应当由自己的父类加载器加载。
但是Tomcat为了实现Java应用共享同一虚拟机实例的隔离性,并没有完全遵守双亲委派模型的原则,而让每个WebApp类加载器加载自己的目录下的class文件,并不会传递给父类加载器。只有自己无法加载的时候,才会委派给父类加载
但是其余的部分还是按照双亲委派模型来走了的~
通常情况下,我们启动了多少个main()方法就会有多少个JVM实例,但在同一个Tomcat容器中,通常会在webapp下有多个应用;即一个Tomcat容器会启动多个Java应用,或者说有多个JavaWeb工程,那么这样的一种情况,这个Tomcat容器会启动多少个JVM实例呢?
答案就是:一个
- 一个Tomcat容器会启动一个JVM,但可能有多个JavaWeb在这同一个JVM实例中运行
- 一个Tomcat容器部署了多个应用,虽然处于同一个JVM环境,但是他们之间是无法相互调用的,这依赖于Tomcat的类加载机制的不同
Java 类加载器(ClassLoader)的实际使用场景有哪些? - @作者:知乎
一般是为了隔离性
当我们写了一个包和类名 都跟Java库一模一样的String类时(java.lang.String
);而我们的虚拟机会加载我们自定义的String吗?
实际上,并不会,它实际调用的还是Jdk的String,但是代码层面并不会报错,这是因为双亲委派模型的作用在生效。
如果没有双亲委派模型
就比如,我这里有一个项目A
,引用了Jar包B
和Jar包C
,包B
和包C
都有一个com.snailmann.Hello
类
Jar包B的Hello代码:
public class Hello {
public Hello() {
System.out.println(name);
}
private String name = "HelloB";
}
Jar包C的Hello代码:
public class Hello {
public Hello() {
System.out.println(name);
}
private String name = "HelloC";
}
项目A的测试代码:
public class Test {
public static void main(String[] args) {
Hello hello = new Hello();
}
}
我初步的猜想是,可能会发生编译错误,出现import
包时,不知道import
哪一个;但事实上,并不会报错,因为两个类的包名都是一样的
结果:
helloB
为什么会是这样一个不报错还能输出HelloB
的情况呢?不报错应用是包名没有导错,的确是这样;为什么输出HelloB
呢?
这就关系到虚拟机是先触发加载JarB
还是JarC
的com.snailmann.Hello
类,这是什么因素导致的呢?
我自己简单的测试了一下,可能就是项目Apom.xml
文件中的导入的Jar依赖顺序
如果JarB的依赖在JarC的前面,那么虚拟机加载的就是JarB的类;相反则是JarC的类