首先我们需要明确,java中的类只会加载一次,而且java中的类是一种惰性加载,姬用到哪个类,才会去加载哪和个类这个其实很好理解,我们在实际开发项目中引入了很多包,这里面的每个类不一定都能用到,jvm启动运行的时候不可能一次把所有的类都加载一遍。一定是程序运行过程中需要哪个类去加载哪个类。
那么那些场景会导致类的主动加载呢?
1、这是最常见的一种类的加载方式,当我们创建一个对象的时候,首都需要看一下这个类是否加载,如果没有加载类,需要先加载类。2、访问类的静态变量,
3、访问类的静态方法
4、对某个类进行反射操作
5、初始化子类会导致父类的初始化
6、启动类,执行main函数所在类导致类的初始化
这里需要注意的是 构造一个类的数组不会导致类的加载,仅仅是分配了一定的内存空间 比如 A[]a=new A[5];这里不会有A的类加载,另外 A a=null,这种情况也不会有类的加载。
了解了类的加载的触发过程,那我们需要探究一下类的加载过程,类的加载过程可以分为一下几步
1、加载 从磁盘通过IO读取claaa字节码文件,往jvm虚拟机加载
2、验证 校验字节码文件的正确性,是否符合ava规范
3、准备 给类的静态变量分配内存,并且分配默认值,int->0,boolean->false,引用类型->null等
4、解析 将符号引用转变为直接引用(静态链接过程)将静态方法(方法名,变量名)转化为内存地址。静态方法的符号对应地址在类加载期间就可以确定,并且不会改变,所以可以做静态链接
5、初始化 对类的静态变量初始化赋真正的值 执行静态代码块(这里要多说一次,静态代码块就是这个时候执行的,因为类只加载一次,所以静态代码块也只执行一次,小伙伴们面试经常碰到面试题,看一段代码,判断执行结果,而这段代码往往就有静态代码块,其实考察的就是类加载故丛横)
我们知道java是一门面向对象语言,,前面我们了解了类的加载过程,那么对于java来说,谁来完成我们的类的加载这个工作呢,这就是我们下面要说的,类加载器,类加载器其实没有什么神秘的,它就是一个对象,干的就是类加载的活。严格来说,类加载器分为:
1、引导类加载器(bootstrapLoader)负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如 rt.jar、charsets.jar等
2、扩展类加载器(ExtClassLoader)负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR 类包
3、应用程序类加载器(AppClassLoader)负责加载ClassPath路径下的类包,主要就是加载你自己写的那 些类
4、自定义加载器 开发者自己写的类加载器,可以指定加载路径
下面这个代码输出结果可以很好的体现这几种类加载器
public class Demo1 {
public static void main(String[] args) {
System.out.println(String.class.getClassLoader());
System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());
System.out.println(Demo1.class.getClassLoader().getClass().getName());
}
}
运行结果
null
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader
我们知道了类的加载过程,加载类的双亲委派机制,那么什么是双亲委派机制呢,其实双亲委派机制可以这么描述,加载某个类时会先委托父加载器寻找目标类,找不到再委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类。概括起来一句化,父加载器先加载,加载不到自己才去加载。下面的图会更详细体现这个过程
这里我们需要明确,类加载器的父类加载器并不是继承关系,比如应用程序类加载器的父加载器是扩展类加载器,但是二者并不是继承关系,而是应用程序类加载器的字段成员属性parent是引导类加载器。下面这个图展示的是从c++创建引导类加载器到初始化扩展类加载器和应用程序类加载器的过程,从Launcher类的源码可以看出,初始化先初始化了扩展类加载器,然后作为参数传入初始化应用程序类加载器当中
下面我们看一下实现双亲委派机制的源码,所有的类加载器都是继承ClassLoader类,该类的loadClass方法就是主要实现了双亲委派机制
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 先检查类是不是被加载过,保证类只加载一次
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果parent存在,由parent加载
if (parent != null) {
c = parent.loadClass(name, false);
// 扩展类加载器的parent为null 用引导类加载器加载
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
//父类没有加载到,自己加载
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
下面我们来探讨一下,为什么java要设计双亲委派机制:
1、避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性
2、沙箱安全机制,防止核心API库被随意篡改,这个这么理解呢,我们知道类的加载是根据全限定包名类名来确定的,也就是说我全限定包名类名相同,jvm便认为是一个类,那么如果我们自己写一个String类,全限定包名类名和jdk源码的包名类名一样,那是不是我们就可以篡改jdkString类的api了呢,通过双亲委派机制,尽管我们可已写这么一个类,但是加载类的时候,应用程序类加载器会先让父加载器扩展类加载器加载,而扩展类加载器会先让引导类加载器加载,引导类加载器加载的是jdk自己的String类,这就导致我们的string类不加载,这就保证了jdk的核心api不会被篡改。
那么是否可以打破双亲委派机制呢
我们可以通过自定义类加载器拉打破双亲委派机制。核心的是我们继承Classloader这个类,重写loadClass方法便可以实现按照自己意愿去加载类而不用遵循双亲委派机制Tomcat就是个很好的例子,TomCat为什么要打破双亲委派机制
我们思考一下:Tomcat是个web容器, 那么它要解决什么问题:
1. 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的 不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是 独立的,保证相互隔离。
2. 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程 序,那么要有10份相同的类库加载进虚拟机。
3. web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的 类库和程序的类库隔离开来。
4. web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中 运行,但程序运行后修改jsp已经是司空见惯的事情, web容器需要支持 jsp 修改后不用重启。
再看看我们的问题:Tomcat 如果使用默认的双亲委派类加载机制行不行? 答案是不行的。为什么?
第一个问题,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认 的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。 第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。 第三个问题和第一个问题一样。 我们再看第四个问题,我们想我们要怎么实现jsp文件的热加载,jsp 文件其实也就是class文 件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp 是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想 到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载 器。重新创建类加载器,重新加载jsp文件