在JVM中并不是一次性把所有的文件都加载到,而是一步一步的,按照需要来加载。
java类加载模式与web容器的类加载模式
1 概述
任何一个java文件都是先编译成 .class文件,然后再经过jvm解释成机器码。只要拥有.class文件和jvm,那么任何一个平台都可以运行
2 ClassLoader 简介
2.1 ClassLoader 类几个重要方法:
- getParent() 返回该类加载器的父类加载器。
- loadClass(String name) 加载名称为name的类,返回的结果是 java.lang.Class类的实例。
- findClass(String name) 查找名称为name的类,返回的结果是 java.lang.Class类的实例。
- findLoadedClass(String name) 查找名称为name的已经被加载过的类,返回的结果是 java.lang.Class类的实例。
- defineClass(String name, byte[] b, int off, int len) 把字节数组 b中的内容转换成Java 类,返回的结果是 java.lang.Class类的实例。这个方法被声明为 final的。
- resolveClass(Class c) 链接指定的 Java 类。
2.2 ClassLoader中loadClass方法的源码
1 | protected synchronized Class loadClass(String name, boolean resolve) |
2.3 ClassLoader抽象类中默认实现的两个构造函数
1 | protected ClassLoader() { |
3 java的类加载流程和双亲委派模型
3.1 类加载器介绍
- 从Java虚拟机的角度来说,只存在两种不同类加载器:一种是**启动类加载器(Bootstrap ClassLoader)**,这个类加载器使用C++语言实现(只限HotSpot),是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类
java.lang.ClassLoader
- 从Java开发人员的角度来看,类加载还可以划分的更细致一些,绝大部分Java程序员都会使用以下3种系统提供的类加载器:
- 启动(Bootstrap)类加载器:引导类装入器是用本地代码实现的类装入器,它负责将 /lib 下面的类库加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
- 标准扩展(Extension)类加载器:它负责将 < Java_Runtime_Home >/lib/ext 或者由系统变量 java.ext.dir 指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。
- 系统(System)类加载器:它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器。
3.2 类加载器的创建
这三个加载器都是jvm通过一个Launcher类来初始化创造的 ,三个类加载器的创建分别在这相应的三个内部类中
1 | /*静态内部类继承了URLClassLoader*/ |
ExtentionClassLoader 类似于这种方式去创建,那BootStrapClassLoader是怎么创建的呢? 其实java中并没有这个类,只是一个概念,它嵌入到jvm中了,由c++编写的。也就是jvm一启动就会有这个类,但是在Launcher类中规定了它加载类所在的路径
1 | private static String bootClassPath = System.getProperty("sun.boot.class.path"); |
从上面的代码也看到了 AppclassLoader 继承的是URLClassLoader,但它的父加载器却是ExtentionClassLoader ,采用这种方式是有两个方面:
- 继承URLClassLoader,是为了继承这个类中的findClass()方法为了找到规定路径下的类。继续看URLClassLoader的源码:
1 | protected Class<?> findClass(final String name) |
前面有个问题是只是找到字节码文件,并没有加载它,答案就在这里,通过defineClass(name, res)来加载它
**为什么不在loadClass()中加载它,而是通过findClass()来加载? **
- 所有类加载器中的基类ClassLoader中有一个属性
1 | private final ClassLoader parent; |
是通过类与类组合的方式来定义一个父加载器的,为了构成双亲委派模型。
3.3 双亲委派模型
双亲委派机制的工作流程
- 当前ClassLoader首先从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。 每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,等下次加载的时候就可以直接返回了。
- 当前classLoader的缓存中没有找到被加载的类的时候,委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到bootstrp ClassLoader.
- 当所有的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。
3.4 三个类加载器的关系
1 | public static void main(String[] args) { |
1 | 代码输出如下: |
通过以上的代码输出,我们可以判定系统类加载器的父加载器是标准扩展类加载器,但是我们试图获取标准扩展类加载器的父类加载器时确得到了null,就是说标准扩展类加载器本身强制设定父类加载器为null。我们还是借助于代码分析一下:
4 类加载疑问
4.1 那么为什么要设计双亲委派模型呢?
假设,整个系统中没有这个设计,还是上面的程序,没有双亲委派模型,那么String这个类就由AppclassLoader加载了,然后可能在有另一个自定义的加载器加载String这个类了,那么程序就乱套了!!到处都是真假难辨的String。而有了双亲委派模型,不管是哪个加载器加载的String,这个类都是由BootStrapClassLoader加载。双亲委派模型最主要的作用是保证java核心类的安全性。
注意:一个类可以被多次加载,但是一个加载器只能加载一次,而且判断一个类是不是相同的,是比较 包名+类名+类加载器id 当某个classloader加载的所有类实例化的对象都被GC回收了,那么这个加载器就会被回收掉。
4.2 双亲委派模型是不可改变的吗?
首先,明确双亲委派模型是一种规范,在自定义类加载器的时候完全可以重写loadClass()方法中的逻辑。这里回答一下前面的问题,为什么不用loadClass()实现类加载的功能,而是用findClass()。这是为了把委派模型的逻辑和类加载器要实现的逻辑分离开了。所以一般自定义类加载器loadClass()一般不动,而是重写findClass()。
4.2.1 什么时候破坏双亲委托
4.2.1.1 java需要连接数据库
来看一个例子java需要连接数据库,但是数据库的品种这么多,每家厂商写的程序都不同,为了让每个厂商统一,java制定了一系列规范的接口,放在BootStrapClassLoader可以加载的路径中。所有厂商只要按照这些接口规范写就好了,并且统一有一个管理的类在DriverManager。这样java制定者和厂商都开开心心,这时候出现了一个问题。 根据委派模型,A类中引用了B类
1 | public class A{ |
那么默认的B的加载器也是由A的加载器加载的!!
所以通过DriverManager统一来得到Driver的话,那么BootStrapClassLoader默认是加载 java.sql 包下的Driver接口。但实际上必须要加载它的实现类。 可是,根据委派模型,父类加载器去调用子类加载器是不可能完成的 必须由BootStrapClassLoader来加载,这就难办了,BootStrapClassLoader说我的工作就是加载 %JRE_HOME%\lib 下的,要我加工作量,不干!!况且我也找不到在哪啊,我只知道它的接口啊,并不知道它的实现类
于是,出现了一个设计,在线程Tread类中内置一个
1 | /* The context ClassLoader for this thread 默认是appclassloader ,可以自己设置*/ |
因此在父类加载器需要调用子类加载器的时候,就可以通过
1 | Thread.currentThread().getContextClassLoader(); |
来获取想要的类加载器。 下面来看JDBC的源码: 在java.sql.DriverManager中
1 | //从Thread.currentThread中取出appclassloader |
4.2.1.2 web中的热部署
java web中的热部署,指的是热部署是在不重启 Java 虚拟机的前提下,能自动侦测到 class 文件的变化,更新运行时 class 的行为。在编写java程序的时候,每一次改动源代码,就必须重启一次应用程序,那java的这个缺陷是由什么导致的呢?
- java类只能由类加载器加载一次,这样在改动代码后,如果不重启就不能被重新加载。
- java类一般被系统自带的appclassloader 来加载。
然而热部署对于开发来说实在太重要了,想想当你调试js代码的时候,一个屏幕源代码,一个屏幕浏览器,随时随地的观察代码带来的变化。而java对于这一点,由于它语言的特性导致很难做到这一点。 如果想要实现热部署,需要做哪些工作呢?
- 销毁ClassLoader
- 创建新的ClassLoader去加载更新后的class类文件。
第一步销毁ClassLoader,如果要做到这一步,那么这个类文件一定不能是appclassloader加载的,因此要自定义ClassLoader。 第二步,要做到的话,首先必须有一个监听器去监听类发生变化,然后才能相应的创建ClassLoader去加载。 这也是Tomcat服务器在处理类加载的时候进行的做法,每一个web应用都有一个相应的类加载器。原理图如下:
并且Tomcat自定义的类加载器并不遵循双亲委派模型,而是先检查自身可不可以加载这个类,而不是先委派。前面也提到了,BootStrap,System和Common 这三个类加载器遵守委派模型同时加载java以及tomcat本身的核心类库。这样做的好处是更好的实现了应用的隔离,但是坏处就是加大了内存浪费,同样的类库要在不同的app中都要加载一份。(当然可以配置使得不是所有的app都要被加载,默认是全部加载) Tomcat中当类发生改变时,监听器监听到触发StandardContext.reload(),然后销毁以前的类加载器,重新创造一个类加载器。
**小结: **
- Bootstrap类加载器是用本地代码实现的类装入器,不允许不允许直接通过引用进行操作。
- Java提供的类加载器都会继承ClassLoader抽象类 。
- 系统类加载器(AppClassLoader)为默认的父加载器。
- 父子加载器并非继承关系,系统类加载器的父加载器是标准扩展类加载器,标准扩展类加载器本身强制设定父类加载器为null。
- 当委托父类加载器去加载,如果父加载器为null,会调用Bootstrap类加载器加载。
- 开发者创建类加载器是通过调用createClassLoader方法实现的 。
- CommonClassLoader能加载的类都可以被Catalina ClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和Shared ClassLoader自己能加载的类则与对方相互隔离
- 各个WebAppClassLoader实例之间相互隔离。
- JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。