深入分析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文件分为三个阶段:

  1. 第一阶段找到class文件并将或者文件包含的字节码加载到内存,至于如何找到class文件就是通过findClass()方法定义的,找到之后通过defineClass()方法来创建类对象
  2. 第二阶段分为三个步骤(验证,准备,解析):字节码验证,Class类数据结构分析以及相应的内存分配,符号表链接
  3. 第三个阶段将类中的静态属性和初始化赋值,以及静态代码块的执行

常见加载类错误分析

  • ClassNotFoundException:这个异常发生在显式加载类的时候,也就是JVM在加载指定的class文件的时候发现文件不存在,解决办法就是查找classpath下有没有对应的class文件,如果不知道当前的classpath路径,通过如下方法获取:
this.getClass().getClassLoader().getResource("").toString();
  • NoClassDefFoundError:这个错误发生在隐式加载类的时候没有找到类,可能的方式是使用new关键字,属性引用某个类,继承某个接口或类,以及方法的某个参数中引入了某个类,解决这个错误的方法就是确保每个类引用的类都在当前的classpah下

NoClassDefFoundError和ClassNotFoundException的区别:

  1. 一个是Error,一个是Exception
  2. 前者一般由JVM隐式加载引发,后者由显式加载引发
  • UnsatisfiedLinkError:这个错误不常见,通常是JVM启动时候,如果一个不小心将在JVM中的某个lib删除了,比如解析native标识的方法JVM找不到对应的本机库文件
  • ClassCastException:类型转换异常,一般发生在不同类型之间转换时,无法转换引发。解决办法就是要么使用泛型,将运行时的类型错误在编译时避免;要么在转换前通过instanceof关键字检查

Tomcat使用的类加载器

Tomcat实现了类加载器用于加载自身以及web应用,有WebappClassLoader, StandardClassLoader。Tomcat中类加载的体系自顶向下:

  1. ExtClassLoader
  2. AppClassLoader
  3. StandardClassLoader
  4. 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去加载新类,从而实现动态加载

posted @ 2020-11-07 15:52  OverZeal  阅读(234)  评论(0编辑  收藏  举报