类加载器的作用
类加载器的作用就是把类的字节码加载到jvm。同时jvm规定,程序员可以用java代码来自定义类加载器,把类的字节码信息加载到jvm中。jvm再使用类加载器的时候,并不是一次性把所有的类都加载进来,而是在用到某个类的时候,发现jvm中没有才去加载它。
类加载器的类型
从java虚拟机角度来看,类加载器分成C++语言实现的启动类加载器属于虚拟机的而一部分,和java语言实现所有其他类的加载器,独立于虚拟机外部并且全部集成自抽象类java.lang.ClassLoader
从开发者角度来看,类加载器还可以划分为下面3种
- 启动类加载器(Bootstrap ClassLoader):负责加载系统的核心类(<JAVA_HOME>\lib目录下,或者被-Xbootclasspath参数所指定的),有了它之后才能保证java基本的运行环境。例如java.lang.Object,java.lang.String都是由它加载的。
- 扩展类加载器(Extension ClassLoader):负责加载系统<JAVA_HOME>\lib\ext目录中的,或者是被java.ext.dirs系统变量所指定的路径中的目录,也就是加载对jdk的扩展类,面向jni的那些代码。
- 应用程序类加载器(Application ClassLoader):负责加载用户类。也就是我们平时java程序员自己写的代码程序类。
这三类加载器是配合加载的:Application ClassLoader -> Extension ClassLoader -> Bootstrap ClassLoader
什么是双亲委派
再加载一个类时,如果有加载器还有父类加载器,那么会尝试让父类加载器去加载该类。只有当父类没有加载过该类,并且父类也无法加载该类时,才会自己加载。
举个例子:
比如,我写了一个类aaa.bbb.ccc.HaHa.class,并且放到系统的ext指定目录,然后我们在写一个普通的A类,在A类中import aaa.bbb.ccc.HaHa
然后HaHa haha = new HaHa();
并且这个A类是classpath下面的一个普通用户类,此时需要去加载aaa.bbb.ccc.HaHa类了,那么这个HaHa由谁去加载呢?
有一个结论:一个类被加载时的默认类加载器,和它外围的类加载器是同一个。也就是说,这里的HaHa是被加载的类,它的外围类是A。而A类是再classpath下的一个用户类,所以A会被Application ClassLoader所加载,那么根据结论HaHa也会被Application ClassLoader加载。
根据双亲委派机制,来看一下工作流程:
Application ClassLoader拿到HaHa类,他不会立即去加载,它会先让其父加载器 Extension ClassLoader 去加载。 Extension ClassLoader也不会贸然去加载,因为Extension ClassLoader也有父加载器Bootstrap ClassLoader,会让 Bootstrap ClassLoader去加载。 Bootstrap ClassLoader已经没有爸爸了,拿到aaa.bbb.ccc.HaHa
一看,老子没有加载过,老子只加载java核心系统类,这个类我加载不了!
于是Ext知道Boot加载不了,就尝试自己去加载,发现在目录下<JAVA_HOME>\lib\ext找到了这个类能够加载。于是就把aaa.bbb.ccc.HaHa加载进虚拟机。此时Ext已经加载了HaHa类,那么App会直接拿到了加载后的类,并不会调用自己的loadClass去加载HaHa了。
再来看一个例子
假设有一个aaa.bbb.ccc.HaHa2.class
放到classpath
下,同样再A类中import aaa.bbb.ccc.HaHa
然后HaHa haha = new HaHa();
此时的类加载流程
- app开始尝试让其父类ext去加载HaHa2
- Ext不会贸然加载,会询问boot加载过没有
- boot说没有,老子加载不了
- ext就尝试自己加载,ext在指定目录中搜搜,发现扩展目录中没有HaHa2这个类,告诉儿子app说我也加载不了。
- app这个时候开始调用自己的loadClass方法,去类路径下找。而app默认的搜索路径就是我们的classpath。
再做个假设:如果aaa.bbb.ccc.HaHa2.class
不在classpath下,那么此时App也加载不到.HaHa2。那么这时候程序就会报NoSuchClassError
反射的类加载
刚刚我们讨论了new的情况,其实反射也会去加载类,比如Class.ForName()
或者classLoader.loadClass
。
假如,我们写了一个自定义的类加载器ClassLoader1,集成了抽象类ClassLoader。并且覆盖了其中findClass方法,从指定的路径去加载字节码。
现在有一个com.java.ABC.class再classpath下。你用loader.loadClass(“com.java.ABC”),这个类依然会被app加载。因为所有自定义类加载器,其默认的父类就是app,所以com.java.ABC.class,会被APP截胡,从而加载掉。
为什么要设计双亲委派
类加载器虽然只用于实现类的加载作用,但是实际意义远远不限于类加载阶段。所有的类最终都会被加载到内存中,对于任意一个类如何确定它再内存中的唯一性是根据类的全限定名+同一类加载器
。更通俗的来讲就是即使这两个类来源于同一Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那么这两个类就必定不相等。
java.lang.String是我们很常用的一个类,根据双亲委派的这种模型,所以无论哪一个类需要使用String,最终都会被Bootstrap ClassLoader所加载。相反如果没有使用双亲委派模型的话,由各个类加载器自行加载,那么系统中将会有多个不用的String类,java类型体系中最基础的行为也就无法保证,应用程序将会一片混乱。
思考Tomcat的隔离环境
tomcat有这样的需求:部署在同一个服务器上的两个web应用程序,所使用的Java类库可以实现相互隔离。是什么意思呢?
假设有两个应用不是在tomcat里面。一个是AppOne,一个是AppTwo.
- AppOne用了spring3.x的框架
- AppTwo用了spting4.x的框架
spring3.x和spring3.x的很多类,全限定名都是一样的,但是具体实现都会有差别。如果tomcat用同一个类加载器去加载这两个应用类会发生什么:
例如先加载AppOne,然后拿起ApplicationClassloader把很多spring3.x的类,按照全限定名都加载到JVM中。这时候AppTwo启动了需要一些spring4.x的类,本来AppTwo也需要拿起ApplicationClassLoader按照全限定名去加载类。但是发现jvm里面的类已经存在了。因为类的唯一性都是按照类的全限定名+ApplicationClassLoader
来确定的。以上情况就会导致两个部署在tomcat的应用所以来的类发生的混乱。
至于怎么解决?我暂时还没搞懂,等搞懂了补上。