类加载器(ClassLoader)
class文件在jvm经过加载、链接、初始化三个步骤,链接分为验证、准备、解析三个步骤
只有加载是程序员能控制的,其它几个步骤都是由jvm自动运行的。
触发class文件加载的时机:
1、当创建类的实例,例如:使用new关键字、通过反射、克隆、反序列化
2、当调用类的静态方法
3、当使用类或接口的静态字段(final常量除外)
4、当初始化子类,要求先初始化父类
5、作为启动虚拟机,含有main方法的那个类
java.lang.ClassLoader:
1、public Class<?> loadClass(String name) throws ClassNotFoundException
给定一个类名,加载一个类,返回代表这个类的 Class 实例,如果找不到类,则返回异常。
2、protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError
根据给定的字节码流 b 定义一个类,off 表示位置,len 表示长度。该方法只能被子类使用,不能被覆盖修改。
3、protected Class<?> findClass(String name) throws ClassNotFoundException
查找一个类,这是重载 ClassLoader 时,最重要的系统扩展点。这个方法会被 loadClass 调用,用于自定义查找类的逻辑,如果不需要修改类加载默认机制,只是想改变类加载的形式,就可以重载该方法。
4、protected final Class<?> findLoadedClass(String name)
去寻找已经加载的类,该方法只能被子类使用,不能被覆盖修改。
ClassLoader有哪些:
1、Bootstrap ClassLoader(启动类加载器),负责加载 <JAVA_HOME>/lib 目录中的,或者别-Xbootclasspath 参数指定的路径。并且是被虚拟机识别的,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会加载
2、Extension ClassLoader(扩展类加载器),由sun.misc.Launcher$ExtClassLoader
实现,负责加载 <JAVA_HOME>/lib/ext 目录中的。或者被 java.ext.dirs 系统变量所指定的路径中的所有类库
3、Aplication ClassLoader(应用类加载器),由sun.misc.Launcher$AppClassLoader
实现,由于这个类是 ClassLoader 中的 getSystemClassLoader 方法的返回值,也称为系统类加载器,负载加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器。一般情况下,这个就是程序中默认的类加载器
双亲委派机制
双亲委派机制的工作过程是:如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把这个请求委派父类加载器去完成。每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个请求(他的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
双亲委派模型固然有着优点,能够让整个系统保持了类的唯一性。但在有些场合,却不适合,也就是说,顶层的启动类加载器的代码无法访问到底层的类加载器。如 rt.jar 中代码无法访问到应用类加载器
在 Java 平台中,把核心类(rt.jar)中提供外部服务,可由应用层自行实现的接口,通常可以称为 Service Provider Interface,即 SPI, jdk内置的spi实现为ServiceLoader
在 rt.jar 中的抽象类需要加载继承他们的在应用层的子类实现,但是以目前的双亲委派模型是无法实现的。
因此 JDK 引用了一个不太优雅的设计,上下文类加载器。也就是将类加载放在线程上下文变量中。通过 Thread.getContextClassLoader(), Thread.setContextClassLoader(ClassLoader) 这两个方法获取和设置 ClassLoader,这样,rt.jar 中的代码就可以获取到底层的类加载了
双亲模式是虚拟机的默认行为,但并非必须这么做,通过重载 ClassLoader 可以修改该行为。事实上,很多框架和软件都修改了,比如 Tomcat,OSGI。具体实现则是通过重写 loadClass 方法,改变类的加载次序。比如先使用自定义类加载器加载,如果加载不到,则交给双亲加载
由不同的 ClassLoader 加载的同名类属于不同的类型,不能相互转化和兼容。
而这个特性就是我们实现热替换的关键
破坏双亲委派机制
-
基础类调用会用户的代码
JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK1.3时就放进去的rt.jar),但它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能“认识“这些代码啊。因为这些类不在rt.jar中,但是启动类加载器又需要加载。 -
为了实现热插拔,热部署,模块化,意思是添加一个功能或减去一个功能不用重启,只需要把这模块连同类加载器一起换掉就实现了代码的热替换,如:OSGI
-
Tomcat
Tomcat的类加载机制
- 隔离,不同web应用依赖的相同类库的不同版本
一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。 - 共享,不同web应用依赖的相同类库的相同版本
部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机,这是扯淡的。 - 隔离,web容器依赖类库和web应用依赖的类库
web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。 - jsp动态修改
web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情,否则要你何用? 所以,web容器需要支持 jsp 修改后不用重启。
![](https://img.haomeiwen.com/i16458394/a2c01a41c18ec0fc.png)
Tomcat自己定义的类加载器:
- CommonClassLoader
加载/common/*中的类库。
Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问。 - CatalinaClassLoader
加载/server/*中的类库。
Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见。 - SharedClassLoader
加载/shared/*中的类库(在tomcat 6之后已经合并到根目录下的lib目录下)。
各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见。 - WebappClassLoader
加载/WebApp/WEB-INF/*中的类库。
各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见。
CommonClassLoader能加载的类都可以被Catalina ClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和Shared ClassLoader自己能加载的类则与对方相互隔离。
WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。
而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。
tomcat 为了实现隔离性,没有遵守双亲委派机制,每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器。
网友评论