类加载器是java语言的核心,容器,OSGI,一些web框架以及一些其他的工具如HotSwap都对类加载器重度依赖。这篇文章将对类加载器使用过程中的常见问题做一个分析。

类加载过程

每个编写的”.java”拓展名类文件都存储着需要执行的程序逻辑,这些”.java”文件经过Java编译器编译成拓展名为”.class”的文件,”.class”文件中保存着Java代码经转换后的虚拟机指令,当需要使用某个类时,虚拟机将会加载它的”.class”文件,并创建对应的class对象,将class文件加载到虚拟机的内存,这个过程称为类加载,这里我们需要了解一下类加载的过程,如下:
关于类加载器-你应该知道的事 - 图1

加载:类加载过程的一个阶段:通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个Class对象

验证:目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

准备:为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即0(如static int i=5;这里只将i初始化为0,至于5的值将在初始化时赋值),这里不包含用final修饰的static,因为final在编译的时候就会分配了,注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。

解析:主要将常量池中的符号引用替换为直接引用的过程。符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。有类或接口的解析,字段解析,类方法解析,接口方法解析(这里涉及到字节码变量的引用,如需更详细了解,可参考《深入Java虚拟机》)。

初始化:类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。

java.lang.ClassLoader

首先类加载器本身就是一个对象,它继承自java.lang.ClassLoader。所有的类都由类加载器加载到JVM。那么问题来了,类加载器也对应一个类,而所有的类都被累加载器加载,那么到底谁先被加载呢?咋看像个鸡生蛋的问题,其实并没有那么复杂,我们只需要了解类加载器的层次关系就回明白了。

首先,我们看下ClassLoader的API:

  1. package java.lang;
  2. public abstract class ClassLoader {
  3. public Class loadClass(String name);
  4. protected Class defineClass(byte[] b);
  5. public URL getResource(String name);
  6. public Enumeration getResources(String name);
  7. public ClassLoader getParent();
  8. protected Class<?> findClass(String name);
  9. }

loadClass的类加载逻辑

类何时被加载?
当一个类被我们代码使用时那么这个类肯定已经被加载好了。但是类的加载却可以通过lazy load来实现,一个类仅当它被引用时才会被加载,即调用类的构造方法(new 一个对象),引用类的static属性或者调用static方法时。这一点对理解类的加载过程很重要。

loadClass方法,是类加载的入口,也就是一个类在确定要被加载时会进入到这个方法

  1. protected Class<?> loadClass(String name, boolean resolve)
  2. throws ClassNotFoundException
  3. {
  4. synchronized (getClassLoadingLock(name)) {
  5. // 第一步,检查类是不是已经被当前类加载器加载过了 findLoadedClass是一个静态方法
  6. // 会在SystemDictionary中查找当前类是不是已经被加载过了
  7. // SystemDictionary的结构如下:
  8. // ClassName|ClassLoader|ClassObject
  9. Class<?> c = findLoadedClass(name);
  10. //如果类之前没有被当前类加载器加载过,这委托给父类加载器加载
  11. if (c == null) {
  12. long t0 = System.nanoTime();
  13. try {
  14. if (parent != null) {
  15. //通过父类加载器加载类,这是一个递归的过程
  16. c = parent.loadClass(name, false);
  17. } else {
  18. //如果parent为null则看BootstrapClassLoader中是否已经加载,
  19. //注意:这个方法只是查找是否被加载,并不涉及加载过程
  20. c = findBootstrapClassOrNull(name);
  21. }
  22. } catch (ClassNotFoundException e) {
  23. // haha 原来jdk的源码也在用异常控制流程
  24. }
  25. if (c == null) {
  26. // 如果还是没有找到这个类这通过findClass方法来加载类
  27. long t1 = System.nanoTime();
  28. c = findClass(name);
  29. sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
  30. sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
  31. sun.misc.PerfCounter.getFindClasses().increment();
  32. }
  33. }
  34. if (resolve) {
  35. resolveClass(c);
  36. }
  37. return c;
  38. }

从loadClass方法中我们可以看到类加载器的委托模型,如果有类需要加载,首先从子类到父类由下而上去查找类看类是否已经被加载过(这个过程是通过递归控制),如果没有被加载,则从父类到子类从上往下去加载类直到加载成功(这个流程是通过异常控制的)。

类加载器层次结构

通过对loadClass方法的分析后我们通过一张图来明确下类加载器的层次结构:

关于类加载器-你应该知道的事 - 图2

Bootstrap ClassLoader 是唯一没有父亲的类加载器,换句话说他是其他所有类加载器的父亲,到此我们之前抛的鸡生蛋的问题这里就有解答了,就如同女娲造人一样 BootstrapClassLoader 就是女娲,负责将 java.lang包下的类以及一些其他运行时需要的类进行加载。

Extension classloader 负责加载 java.ext.dir 路径下的 .jar文件中的类。

System classpath classloader 负责加载 CLASSPATH 中的类,正常情况下我们引入的包已经我们自己的代码都是由它及它的儿子来加载。

Custom ClassLoader 除了系统提供的类加载器,我们还可以自定义类加载器,来实现特定的加载行为,这也是类加载器里最好玩的地方,更多内容后面会聊到。

findClass与自定义类加载器

之前聊到,类加载器真正由自己加载类而非委托父类加载,这过程是在findClass中实现的,ClassLoader类
并未实现findClass过程而是交由其子类去实现自己的加载逻辑。

  1. public abstract class ClassLoader{
  2. ...
  3. protected Class<?> findClass(String name) throws ClassNotFoundException {
  4. throw new ClassNotFoundException(name);
  5. }
  6. ...
  7. }

我们先看下URLClassLoader中的findClass方法

  1. protected Class<?> findClass(final String name)
  2. throws ClassNotFoundException
  3. {
  4. final Class<?> result;
  5. try {
  6. result = AccessController.doPrivileged(
  7. new PrivilegedExceptionAction<Class<?>>() {
  8. public Class<?> run() throws ClassNotFoundException {
  9. String path = name.replace('.', '/').concat(".class");
  10. // 找到对应的类
  11. Resource res = ucp.getResource(path, false);
  12. if (res != null) {
  13. try {
  14. //获取类的字节码 并调用native方法defineClass来定义一个类
  15. return defineClass(name, res);
  16. } catch (IOException e) {
  17. //如果找不到类或者说不具备加载该类的能力则抛出ClassNotFoundException
  18. throw new ClassNotFoundException(name, e);
  19. }
  20. } else {
  21. return null;
  22. }
  23. }
  24. }, acc);
  25. } catch (java.security.PrivilegedActionException pae) {
  26. throw (ClassNotFoundException) pae.getException();
  27. }
  28. if (result == null) {
  29. throw new ClassNotFoundException(name);
  30. }
  31. return result;
  32. }

总结来说,如果自定义的类加载器如果需要重写findClass方法 我们需要分三步:

  1. 根据类名定位类
  2. 没找到类或没能力加载类则抛出ClassNotFoundException
  3. 找到类后调用native方法defineClass进行类的定义并返回Class对象

破坏委托模型的自定义类加载器

重写findClass方法能达到的效果仅仅是改变类加载的方式,使我们class字节码可以从文件系统加载,通过网络加载或者从内存中加载。但是由loadClass方法保证的委派机制并没有被改变。所以如果需要破坏委派机制我们需要重写loadClass方法,按照自己需要的加载逻辑去实现。

类加载常见报错原因分析及解决办法

首先得明确的一点是:编译和运行是两回事,编译成功不一定正常运行。举个例子通过maven编译时引入的依赖中加了provider那么打包时就不会被打进去,运行时则会找不到类。

错误名称 产生原因
NoClassDefFoundError 找不到类的定义。意味着编译时能找到该类,但是运行时却找不到 常见于new一个对象时
NoSuchMethodError 找不到方法。类存在但是类加载的版本不对,导致方法找不到。常见于多个版本的jar包冲突,相同类名的类只会被加载一次,导致运行时使用的类版本不对。也常见于依赖的模块方法变更后,jar包中的class没有更新。
ClassCastException 类型转换异常。对一个类的对象进行强制类型转换时抛出,常见于同一个类文件被不同类加载器加载时。
LinkageError 关系错误。和ClassCastException唯一的区别就是未进行强制类型转换
IllegalAccessError 访问权限错误。A B 两个类在同一包下 B中的有一个方法是包内可见,A中本该可以这场调用B的这个方法但是如果A ,B由不同的类加载器加载,那么发生调用时就会报这个错

定位类加载问题常用的方法有几个:

  1. find *.jar -exec jar -tf ‘{}’; | grep MyClass 查询对应的类是否真的被打进了jar包
  2. ClassLoader.getResource() 获取当前类加载器加载的类的路径
  3. JVM启动参数 -verbose:class 打印类加载日志
  4. javap -private MyClass 配合2使用 查看类的版本是否是最近版本