文章目录
- 一、ClassLoader简介
- 1. 概念
- 2. ClassLoader类结构分析
- 二、ClassLoader的双亲委派机制
- 三、Class文件的加载流程
- 1. 简介
- 2. 加载字节码到内存
- 3. 验证与解析
- 4. 初始化Class对象
- 四、常见加载类错误分析
- 1. ClassNotFoundException
- 2. NoClassDefFoundError
- 3. UnsatisfiledLinkError
- 4. ClassCastException
- 5. ExceptionInInitializerError
- 五、自定义ClassLoader的优势
一、ClassLoader简介
1. 概念
ClassLoader顾名思义就是类加载器,负责将Class加载到JVM中。事实上,ClassLoader除了能够将Class加载到JVM意外以外,还有一个重要的作用就是审查每个类应该由谁加载,它是一种父优先的等级加载机制。此外,ClassLoader除了上述的两个作用外还有一个任务就是将Class字节码重新解析成JVM统一要求的对象格式。
2. ClassLoader类结构分析
我们用到ClassLoader时常用下面的几个方法,以及它们的重载方法:
public abstract class ClassLoader {ClassLoader;Class<?> defineClass(byte[],int,int);Class<?> findClass(String);Class<?> loadClass(String);void resolveClass(Class<?>);
}
defineClass
方法用来将byte字节流解析成JVM能够识别的Class对象,有了这个方法我们不仅仅可以通过class文件实例化对象,还可以通过其他方式如我们通过网络接收一个类的字节码,拿这个字节码流直接创建类的Class对象形式实例化对象。defineClass
通常是和findClass
方法一起使用的,我们通过直接覆盖ClassLoader父类的findClass
方法来实现类的加载机制,从而取得想要加载类的字节码。然后调用defineClass
方法生成类的Class对象,如果你想在类被加载到JVM中时就被链接,那么可以调用另一个resolveClass
方法,当然你也可以选择让JVM来解决什么时候才链接到这个类。
如果你不想重新定义加载类的规则,只想在运行时能够加载自己指定的一个类而已,那么你可以用this.getClass().getClassLoader().loadClass("class")
调用ClassLoader的loadclass方法可以获取这个类的Class对象,这个loadClass还有重载方法,你统一可以决定在上面时候解析这个类。
二、ClassLoader的双亲委派机制
双亲委派机制(Parent Delegation Mechanism)是Java中的一种类加载机制。在Java中,类加载器负责加载类的字节码并创建对应的Class对象。双亲委派机制是指当一个类加载器收到类加载请求时,它会先将该请求委派给它的父类加载器去尝试加载。只有当父类加载器无法加载该类时,子类加载器才会尝试加载。
这种机制的设计目的是为了保证类的加载是有序的,避免重复加载同一个类。Java中的类加载器形成了一个层次结构,根加载器(Bootstrap ClassLoader)位于最顶层,它负责加载Java核心类库。其他加载器如扩展类加载器(Extension ClassLoader)和应用程序类加载器(Application ClassLoader)都有各自的加载范围和职责。通过双亲委派机制,可以确保类在被加载时,先从上层的加载器开始查找,逐级向下,直到找到所需的类或者无法找到为止。
这种机制的好处是可以避免类的重复加载,提高了类加载的效率和安全性。同时,它也为Java提供了一种扩展机制,允许开发人员自定义类加载器,实现特定的加载策略。
其实Bootstrap ClassLoader并不属于JVM的类等级层次,因为BootStrap ClassLoader并没有遵守ClassLoader的加载规则,另外它并没有子类,ExtClassLoader的父类也不是Bootstrap ClassLoader,我们应用中能取到的顶层父类时ExtClassLoader。
ExtClassLoader和AppClassLoader都位于sun.misc.Launcher
类中,它们是Loucher类的内部类。ExtClassLoader和AppClassLoader都继承了URLClassLoader,而URLClassLoader又实现了抽象类ClassLoader,在创建Launcher对象时会首先创建ExtClassLoader,然后将ExtClassLoader作为父加载器创建AppClassLoader对象,而通过Launcher.getClassLoade()方法获取的ClassLoader就是AppClassLoader对象。所以如果Java应用中没有定义其他ClassLoader,那么除了System.getProperty("java.ext.dirs")
目录下的类是由ExtClassLoader加载为,其它类都由AppClassLoader来加载。
JVM加载class文件到内存中有两种方式:
- 隐式加载:所谓隐式加载是不通过在代码里面调用ClassLoader来加载所需要的类,而是铜鼓oJVM来自动加载所需的类到内存的方式。例如:当我们在类中继承或者引用某个类是,JVM在解析当前这个类时发现引用不在内存中,那么自动将这些类加载到内存中。
- 显式加载:相反的显式加载就是我们在代码中使用ClassLoader类加载一个类的方式
其实这两种方式是混合使用的,例如我们通过自定义的ClassLoader显式加载一个类时,这个类又引用了其他类,那么这些类就是隐式加载的。
三、Class文件的加载流程
1. 简介
下面分析如何将class文件加载到JVM中。ClassLoader加载一个class文件到JVM要经历如下阶段:
- 首先找到class文件并把这个文件包含的字节码加载到内存中
- 链接阶段分为三个步骤,分别是字节码验证、Class类数据结构分析及相应的内存分配和最后符号表的链接
- 最后是类中静态数据和初始化赋值,以及静态块的执行
2. 加载字节码到内存
findClass()的方法是在ClassLoader实现类中实现的,例如URLClassLoader就实现了该方法,URLClassLoader类通过一个URLClassPath类的帮助取得要加载的class文件字节流,而这个URLClassPath定义了到哪里去找这个class文件,如果找到了这个class文件,再读取它的byte字节流通过调用defineClass()方法创建类对象。
private final URLClassPath ucp;
再看其构造函数,要指定一个URL数据才能创建URLClassLoader对象,也就是必须要指定这个ClassLoader默认到哪个目录中去查找class文件
public URLClassLoader(URL[] urls, ClassLoader parent) {super(parent);SecurityManager security = System.getSecurityManager();if (security != null) {security.checkCreateClassLoader();}this.acc = AccessController.getContext();ucp = new URLClassPath(urls, acc);}
在创建URLClassLoader对象时就根据传过来的URL数组中的路径来判断是文件还是jar包,根据路径不同分别创建FileLoader或者JarLoader,或者使用默认的加载器,当JVM调用findClass时由这几个加载器来将class文件加载到内存中。
3. 验证与解析
- 字节码验证,类装入器对于类的字节码要做许多检测,以确保格式正确、行为争取
- 类准备,这个阶段准备代表的每个类中定义的字段、方法和实现接口所必需的数据结构
- 解析,在这个阶段类装入器装入类所引用的其他类。可以用许多方式引用类,如超类、结构、字段、方法签名、方法中使用的本地变量
4. 初始化Class对象
类中包含的静态初始化器都被执行,在这一阶段末尾静态字段被初始化默认值。
四、常见加载类错误分析
在执行Java程序时经常会碰到ClassNotFoundException
和NoClassDefFoundError
两个异常,它们都与类加载有关,下面分析一下产生这些异常的原因:
1. ClassNotFoundException
这个异常通常发生在显示加载类的时候,例如,用如下方式调用加载一个类时就报了这个错:
public class Main {public static void main(String[] args) throws ClassNotFoundException {Class.forName("Jack");}
}
出现这个错误的原因是,JVM要加载指定的文件的字节码到内存时,并没有找到这个文件对应的字节码,也就是这个文件并不存在(在当前classpath目录下)。
获取classpath路径的方法:
this.getClass().getClassLoader().getResource("").toString()
2. NoClassDefFoundError
这个异常在第一次使用命令执行Java类时很可能会碰到,出现这种异常的可能原因是使用new关键字、属性引用某个类、继承了某个接口或类,以及方法的某个参数引用类某个类,这时会触发JVM的隐时加载这些类时发现这些类不存在。解决这个错误的方法就是确保每个类的引用的类都在当前的classpath下面。
3. UnsatisfiledLinkError
这个异常通常是JVM启动时,如果一不小心将JVM中的某个lib删除了,就可能会报这个错误。
public class Main {public native void nativeMethod();static {System.loadLibrary("Nolib");}public static void main(String[] args) throws ClassNotFoundException {new Main().nativeMethod();}
}
上面就是在解析native标识的方法时JVM找不到对应的本机库文件出现。
4. ClassCastException
这个错误比较常见,通常在程序中出现强制类型转换时出现这个错误。JVM在做类型转换时会按照如下规则进行检查:
- 对于普通对象,对象必须是目标类的实例或目标类的子类实例。如果目标类是一个接口,那么会把它当作实现该接口的一个字类。
- 对于数组类型,目标类必须是数组类型或java.lang.Objecgt、java.lang.CLoneable、java.io.Serializable
如果不满足上面规则,JVM就会报这个错误。要避免这个错误有两种方式:
- 在容器类型中显示地指明这个容器所包含的对象类型
- 先通过instanceof检查是不是目标类型,然后再进行强制类型转换
5. ExceptionInInitializerError
这个错误JVM规范中是这样定义的:
- 如果Java虚拟机试图创建类ExceptionInInitializerError的新实例,但是因为出现Out-Of-Memory-Error而无法创建新实例,那么就会抛出OutOfMemoryError对象作为代替
- 如果初始化器抛出一些Exception,而且Exception类不是Error或者它的某个子类,那么就会创建ExceptionInInitializerError类的一个新实例,并用Exception作为参数,用这个实例代替Exception
认值。
五、自定义ClassLoader的优势
通过前面的分析,ClassLoader能够完成的事情无非以下几种情况:
- 在自定义路径下查找自定义的class文件,也许我们需要的class文件并不总是在已经设置好的ClassPath下面,那么我们必须想办法找到这个类,在这种情况下我们需要自己实现一个ClassLoader
- 对我们自己的要加载的类做特殊处理,如保证通过网络传输的类的安全性,可以将类经过加密后再传输,在加载到JVM之前需要对类的字节码再解密,这个过程就可以在自定义的ClassLoader中实现
- 可以定义类的实效机制,如果我们可以检查已经加载的class文件是否修改,如果修改类可以重新加载这个类,从而实现类的热部署。