什么是类加载
每个编写出的.java文件都存储着需执行的程序逻辑,经过Java编译器编译,
会为每个.java文件生成对应的.class字节码文件,.class文件中则记录着Java代码转换之后的虚拟机指令,每个.class文件开头都有特定的标识、魔数版本等信息。
当JVM需要用到某个类时,虚拟机会加载它的.class文件,加载了相关的字节码信息后,会为它创建对应的Class对象,而这个过程就被称为类加载。
类加载的过程
类加载过程被分为三个步骤,五个阶段,分别为加载、验证、准备、解析以及初始化。加载、验证、准备、初始化这四个阶段的顺序是确定的。但解析阶段不一定,为了支持Java语言的运行时绑定特性,在某些情况下可以在初始化阶段之后再开始(也称为动态绑定或晚期绑定)
加载
验证(验证被加载的Class的正确性)
准备:为类中的静态变量分配内存空间(static成员),并完成数值的初始化(例如static int i = 5;这里只会将i初始化为0)
解析:把类中对常量池内的符号引用转换为直接引用的过程
初始化:主要是对类的静态变量赋予正确的初始值,也就是在声明静态变量时指定的初始化值以及静态代码块中的赋值
JVM类加载器
- 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。
- 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
- 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
- 用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现
什么是双亲委派
当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类
双亲委派的作用
- ①防止加载同一个.class。通过委托去询问上级是否已经加载过该.class,如果加载过了,则不需要重新加载。保证了数据安全。
- ②保证核心.class不被篡改。通过委托的方式,保证核心.class不被篡改,即使被篡改也不会被加载,即使被加载也不会是同一个class对象,因为不同的加载器加载同一个.class也不是同一个Class对象。这样则保证了Class的执行安全
双亲委派的核心类——ClassLoader
在Java中,所有的类加载器都间接的继承自ClassLoader类,包括Ext、App类加载器(Bootstrap除外,因为它是C++实现的)
// ClassLoader类 → loadClass()方法
protected Class loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 加锁
synchronized (getClassLoadingLock(name)) {
// 先尝试通过全限定名从自己的命名空间中查找该Class对象
Class c = findLoadedClass(name);
// 如果找到了则不需要加载了,如果==null,开始类加载
if (c == null) {
long t0 = System.nanoTime();
try {
// 先将类加载任务委托自己的父类加载器完成
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 如果父类加载器为null,代表当前已经是ext加载器了
// 那么则将任务委托给Bootstrap加载器加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 处理异常,抛出异常
}
if (c == null) {
// 如果都没有找到,则通过自定义实现的findClass
// 去查找并加载
long t1 = System.nanoTime();
c = findClass(name);
// 这是记录类加载相关数据的(比如耗时、类加载数量等)
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
// 是否需要在加载时进行解析,如果是则触发解析操作
if (resolve) {
resolveClass(c);
}
// 返回加载后生成的Class对象
return c;
}
}
// ClassLoader类 → findClass()方法
protected Class findClass(String name)
throws ClassNotFoundException {
// 直接抛出异常(这个方法是留给子类重写的)
throw new ClassNotFoundException(name);
}
// ClassLoader类 → defineClass()方法
protected final Class defineClass(String name, byte[] b,
int off, int len) throws ClassFormatError
{
// 调用了defineClass方法,
// 将字节数组b的内容转换为一个Java类
return defineClass(name, b, off, len, null);
}
// ClassLoader类 → resolveClass()方法
protected final void resolveClass(Class c) {
// 调用本地(navite)方法,解析一个类
resolveClass0(c);
}
// ClassLoader类 → getParent()方法
@CallerSensitive
public final ClassLoader getParent() {
// 如果当前类加载器的父类加载器为空,则直接返回null
if (parent == null)
return null;
// 如果不为空则先获取安全管理器
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// 然后检查权限后返回当前classLoader的父类加载器
checkClassLoaderPermission(parent,
Reflection.getCallerClass());
}
return parent;
}
ClassLoader类的关键方法,具体作用如下:
loadClass(name,resolve):加载名称为name的类,加载后返回Class对象实例
findClass(name):查找名称为name的类,返回是一个Class对象实例(该方法是留给子类重写覆盖的,在loadClass中,在父类加载器加载失败的情况下会调用该方法完成类加载,这样可以保证自定义的类加载器也符合双亲委托模式)
defineClass(name,b,off,len):将字节流b转换为一个Class对象
resolveClass(c):使用该方法可以对加载完生成的Class对象同时进行解析操作
getParent():获取当前类加载器的父类加载器
如何破坏双亲委派
我们知道类的加载方式默认是双亲委派,如果我们有一个类想要通过自定义的类加载器来加载这个类,而不是通过系统默认的类加载器,说白了就是不走双亲委派那一套,而是走自定义的类加载器
我们知道双亲委派的机制是ClassLoader中的loadClass方法实现的,打破双亲委派,其实就是重写这个方法,来用我们自己的方式来实现即可
- 如果不想打破双亲委派模型,就重写ClassLoader类中的
findClass()
方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。- 而如果想打破双亲委派模型则需要重写ClassLoader类
loadClass()
方法(当然其中的坑也不会少)。典型的打破双亲委派模型的框架和中间件有tomcat