深入分析ClassLoader工作机制
ClassLoader(类加载器)的作用
- 将Class加载到JVM中
- 审查每个类应该由谁加载,采用的是一种父优先的等级加载制度(双亲委派机制)
- 将Class字节码重新解析成JVM统一要求的对象格式
ClassLoader类结构分析
ClassLoader类是一个抽象类,较常用的方法有
- defineClass方法:用来将byte字节流解析成JVM能够识别的Class对象。通过这个方法我们不仅可以将class文件实例化为一个对象,还可以通过其他方式,例如网络获得一个类字节码实例成对象
- findClass方法:通过重写这个方法实现类的加载规则,从而获得要加载的类的字节码。获得字节码之后就可以调用上面的defineClass方法实例化
ClassLoader有很多子类,如果我们需要实现自己的ClassLoader,一般会继承URLClassLoader这个子类,因为这个类已经帮我们实现了大部分工作,只需要在适当的地方做些修改就好了,就像我们要实现Servlet时通常会直接继承HttpServlet
ClassLoader的等级加载机制
类加载器加载一个类,首先判断自己有没有加载过,如果已经加载则不再加载;如果没有加载则向上一级询问是否加载,如果上一级的类加载器加载过则将结果反馈给下一级,如果没有加载则向更高一级的询问,如果认为没有加载过,并且不应该自己加载,则最初的加载器会正式加载这个类。这个复杂的流程最要是为了保证加载类的安全问题
整个JVM平台提供了三层ClassLoader,自上而下分别为:
- Bootstrap ClassLoader,这个ClassLoader主要加载JVM自身需要的类,完全由JVM自己控制,需要访问哪个类也是由JVM控制的,别人无法访问到这个类,它仅仅是一个类的加载工具而已,既没有更高一级的父类加载器,也没有子类加载器
- ExtClassLoader,这个类比较特殊,它是JVM的一部分,但是它并不是JVM亲自实现的,有些类既不是JVM的内部类,又与普通的类有些区别的由它加载,它服务的目标在
System.getProperty("java.ext.dirs")
目录下的jar,一般是%JDK_HOME%/jre/lib/ext
中的jar包 - AppClassLoader,这个类就是加载普通的类,所有在
System.getPropery("java.class.path")
目录下的类都可以由这个类加载器加载,这个目录其实上就是我们经常说的classpath
如果我们要实现自己的类加载器,不管是直接实现抽象类ClassLoader,还是继承URLClassLoader类,或者其他的子类,它的父类都是AppClassLoader,因为不管调用哪个父类构造器,创造的对象都必须调用getSystemClassLoader()
作为父加载器,而getSystemClassLoader()
方法获取到的就是AppClassLoader
实际上Bootstrap ClassLoader并不属于JVM的类等级层级,因为Bootstrap ClassLoader并没有遵循类加载机制,并且也没有子类,ExtClassLoader的父类也不是Bootstrap ClassLoader,我们在应用中能获取到的顶级父类就是ExtClassLoader了
ExtClassLoader和AppClassLoader都位于sun.misc.Launcher
类中,它们是Launcher类的内部类,ExtClassLoader和AppClassLoader都继承URLClassLoader类,而URLClassLoader又实现了抽象的ClassLoader
除了System.getProperty("java.ext.dirs")
目录下的类由ExtClassLoader加载外,其余的类都由AppClassLoader加载
JVM加载class文件的两种方式
- 隐式加载:所谓隐式就是不通过代码中调用ClassLoader类加载需要的类,而是通过JVM自动将需要的类加载到内存中。例如:我们在一个类继承或引用了另外的类,JVM解析这个类发现引用的类不存在内存中,就是自动将这些类加载到内存中
- 显式加载:通过代码的方式加载类。例如:反射,或者我们自己实现的ClassLoader的findCLass()方法
- Class.forName()
- 对象.class
- 类ClassLoader中的loadClass()方法
- 类ClassLoader中的findSYstemClass()方法
如何加载class文件
加载class文件分为三个阶段:
- 第一阶段找到class文件并将或者文件包含的字节码加载到内存,至于如何找到class文件就是通过
findClass()
方法定义的,找到之后通过defineClass()
方法来创建类对象 - 第二阶段分为三个步骤(验证,准备,解析):字节码验证,Class类数据结构分析以及相应的内存分配,符号表链接
- 第三个阶段将类中的静态属性和初始化赋值,以及静态代码块的执行
常见加载类错误分析
- ClassNotFoundException:这个异常发生在显式加载类的时候,也就是JVM在加载指定的class文件的时候发现文件不存在,解决办法就是查找classpath下有没有对应的class文件,如果不知道当前的classpath路径,通过如下方法获取:
this.getClass().getClassLoader().getResource("").toString();
- NoClassDefFoundError:这个错误发生在隐式加载类的时候没有找到类,可能的方式是使用new关键字,属性引用某个类,继承某个接口或类,以及方法的某个参数中引入了某个类,解决这个错误的方法就是确保每个类引用的类都在当前的classpah下
NoClassDefFoundError和ClassNotFoundException的区别:
- 一个是Error,一个是Exception
- 前者一般由JVM隐式加载引发,后者由显式加载引发
- UnsatisfiedLinkError:这个错误不常见,通常是JVM启动时候,如果一个不小心将在JVM中的某个lib删除了,比如解析native标识的方法JVM找不到对应的本机库文件
- ClassCastException:类型转换异常,一般发生在不同类型之间转换时,无法转换引发。解决办法就是要么使用泛型,将运行时的类型错误在编译时避免;要么在转换前通过
instanceof
关键字检查
Tomcat使用的类加载器
Tomcat实现了类加载器用于加载自身以及web应用,有WebappClassLoader, StandardClassLoader。Tomcat中类加载的体系自顶向下:
- ExtClassLoader
- AppClassLoader
- StandardClassLoader
- WebappClassLoader(可以有多个实例)
Tomcat容器本身加载是通过StandardClassLoader加载,但实际上这个类是一个代理类,它会通过父类加载器,也就是AppClassLoader完成,依然遵循委派机制
我们一般不关心容器本身加载,而是在意Web应用由谁加载,Tomcat部署Web应用的不同使用的类加载器也是不同的。通过在serber.xml中配置<xontext/>
的方式(Tomcat显式部署)的方式是通过WebappClassLoader来加载web应用中类,而打成war放在webapp目录下(Tomcat隐式部署)则是通过StandardClassLoader直接加载
Tomcat仍然沿用了JVM的类加载机制,也就是委托式加载,保证核心类通过AppClassLoader来加载。但是Tomcat会优先检查WebappClassLoader已经加载的缓存,而不是JVM的findLoaderClass缓存
WebappClassLoader可以有多个实例,JSP修改后可以不重启服务器热部署,就是通过Tomcat发现JSP修改后(依赖实时编译),就会新创建一个WebClassLoader去加载新类,从而实现动态加载