ClassLoader顾名思义就是类加载器,负责将Class加载到JVM中。事实上,ClassLoader除了能够将Class加载到JVM意外以外,还有一个重要的作用就是审查每个类应该由谁加载,它是一种父优先的等级加载机制。此外,ClassLoader除了上述的两个作用外还有一个任务就是将Class字节码重新解析成JVM统一要求的对象格式。
我们用到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还有重载方法,你统一可以决定在上面时候解析这个类。
双亲委派机制(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显式加载一个类时,这个类又引用了其他类,那么这些类就是隐式加载的。
下面分析如何将class文件加载到JVM中。ClassLoader加载一个class文件到JVM要经历如下阶段:
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文件加载到内存中。
类中包含的静态初始化器都被执行,在这一阶段末尾静态字段被初始化默认值。
在执行Java程序时经常会碰到ClassNotFoundException
和NoClassDefFoundError
两个异常,它们都与类加载有关,下面分析一下产生这些异常的原因:
这个异常通常发生在显示加载类的时候,例如,用如下方式调用加载一个类时就报了这个错:
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("Jack");
}
}
出现这个错误的原因是,JVM要加载指定的文件的字节码到内存时,并没有找到这个文件对应的字节码,也就是这个文件并不存在(在当前classpath目录下)。
获取classpath路径的方法:
this.getClass().getClassLoader().getResource("").toString()
这个异常在第一次使用命令执行Java类时很可能会碰到,出现这种异常的可能原因是使用new关键字、属性引用某个类、继承了某个接口或类,以及方法的某个参数引用类某个类,这时会触发JVM的隐时加载这些类时发现这些类不存在。解决这个错误的方法就是确保每个类的引用的类都在当前的classpath下面。
这个异常通常是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找不到对应的本机库文件出现。
这个错误比较常见,通常在程序中出现强制类型转换时出现这个错误。JVM在做类型转换时会按照如下规则进行检查:
如果不满足上面规则,JVM就会报这个错误。要避免这个错误有两种方式:
这个错误JVM规范中是这样定义的:
认值。
通过前面的分析,ClassLoader能够完成的事情无非以下几种情况: