类加载机制
java文件需要编译成字节码文件(.class文件),jvm是通过类加载机制,将.class文件加载进内存,经过验证连接->初始化直到使用该对象的过程就是类加载机制,当new对象的时候,jvm首先去常量池寻找该类的符号引用,找不到此引用,则执行类加载,简而言之就是jvm通过类加载器加载.class文件变成对象的过程就是类加载机制
三个重要的内置ClassLoader
- BootstrapClassLoader(启动类加载器(根)加载器) 负责加载\lib下的类库加载进内存,用来加载java的核心库
- ExtensionClassLoader (扩展类加载器) 负责加载lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类
- AppClassLoader (应用类加载器) 加载 Classpath 环境变量里定义的路径中的 jar 包和目录,继承自ClassLoader抽象类,所以自定义加载器也需要继承此接口,并重写findClass方法
protected Class<?> findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name);}
自定义类加载器
自定义加载器继承自ClassLoader,重写findClass方法
package com.alibaba.fescar.core.protocol.test;import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;public class MyDefineClassLoader extends ClassLoader {@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {try {// 读取字节数据Path path = Paths.get("D:\\601\\acm601\\cldm_springcloud\\wsd-common\\src\\main\\java\\com\\alibaba\\fescar\\core\\protocol\\test\\TestClass.class");byte[] classData = Files.readAllBytes(path);// 将字节码内容转换为Class对象return defineClass(name, classData, 0, classData.length);} catch (IOException e) {throw new ClassNotFoundException("Class not found: " + name, e);}}
}
定义测试类,并生成.class文件
package com.alibaba.fescar.core.protocol.test;public class TestClass {public void testClassLoader(){System.out.println("test my define classloader");}
}
自定义类加载器的使用
package com.alibaba.fescar.core.protocol.test;import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;public class Test {public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {MyDefineClassLoader myDefineClassLoader = new MyDefineClassLoader();// 加载测试类生成Class对象 一定要带包名Class<?> testClass = myDefineClassLoader.loadClass("com.alibaba.fescar.core.protocol.test.TestClass");// 使用反射获得对象Object o =testClass.getDeclaredConstructor().newInstance();Method testClassLoader = testClass.getMethod("testClassLoader");// 调用方法testClassLoader.invoke(o);}
}
运行结果如下
类加载的过程
- 加载阶段(将.class文件加载进内存)
- 验证、准备、解析阶段(验证.class文件的正确性 准备(为类变量(静态变量)分配内存和初始化默认值)解析(将常量池池中的符号引用转化为直接引用))
- 初始化阶段(执行类构造器init)
加载阶段
将.class字节码文件的二进制数据读入内存中,然后将这些数据翻译成类的元数据,元数据包括方法代码,变量名,方法名,访问权限与返回值,接着将元数据存入方法区,最后会在堆中创建一个Class对象
.class文件读入内存——>元数据放进方法区——>Class对象放进堆中
验证、准备、解析阶段
验证被加载类的正确性与安全性,看class文件是否正确,是否对会对虚拟机造成安全问题等,主要去验证文件格式与符号引用等
对整个类加载机制而言,验证阶段是一个很重要但是非必需的阶段,毕竟验证需要花费一定的的时间,可以使用-Xverfity:none来关闭大部分的验证
准备 在这个阶段中,主要是为类变量(静态变量)分配内存以及初始化默认值,因为静态变量全局只有一份,是跟着类走的,因此分配内存其实是在方法区上分配。
- 在准备阶段,虚拟机只为静态变量分配内存,实例变量要等到初始化阶段才开始分配内存
- 为静态变量初始化默认值,是初始化对应数据类型的默认值,不是自定义的值。
- 被final修饰的静态变量,如果值比较小,则在编译后直接内嵌到字节码中。如果值比较大,也是在编译后直接放入常量池中。准备阶段结束后,final类型的静态变量已经有了用户自定义的值,而不是默认值
解析阶段,主要是将class文件中常量池中的符号引用转化为直接引用
符号引用:可以直接理解为是一个字符串,用这个字符串来表示一个目标
直接引用:直接引用是一个指向目标的指针,能够通过直接引用定位到目标
Logger logger = new Logger();
我们可以通过引用变量logger直接定位到新创建出的Logger 对象实例,将符号引用转化为直接引用,就能将字符串logger转化为指向对象的指针
初始化阶段
初始化,就是虚拟机执行类构造器<clinit>方法的过程,<clinit>方法是由编译器自动去搜集类中的所有类变量与静态语句块合并产生的。可能存在多个线程同时执行某个类的<clinit>()方法,虚拟机此时会对该方法进行加锁,保证只有一个线程能执行
在此阶段类变量与类成员变量才会被赋予用户自定义的值,只有在初始化阶段完成后,类才能被正常使用
初始化顺序
父类的静态域->子类的静态域->父类的非静态域->子类的非静态域->父类的构造方法->子类的构造方法
静态域包括静态变量与静态代码块,静态变量和静态代码块的执行顺序由编码顺序决定
静态先于非静态,父类先于子类,构造方法在最后
双亲委派机制
java虚拟机中有多个类加载器,双亲委派机制的核心是解决一个类到底由谁加载的问题,针对的是类加载器(ClassLoader),避免了类的重复加载
当一个类加载器收到了一个类加载请求时,它自己不会先去尝试加载这个类,而是把这个请求转交给父类加载器,每一个层的类加载器都是如此,因此所有的类加载请求都应该传递到最顶层的启动类加载器中。只有当父类加载器在自己的加载范围内没有搜寻到该类时,并向子类反馈自己无法加载后,子类加载器才会尝试自己去加载
如果一个类重复出现在三个类加载器的加载位置,应该由启动类加载器(根加载器)加载,因为根据双亲委派机制,它的优先级是最高的
打破双亲委派
打破双亲委派机制的主要原因是为了满足一些特定的需求和场景:
- 实现类的热部署:在某些应用场景下,需要在运行时动态加载和替换类,以实现热部署的功能。而双亲委派机制会导致类的加载只发生一次,无法实现类的热替换
- 加载非标准的类文件:有些特殊的类文件,如动态生成的字节码、非标准的类文件格式等,无法通过标准的类加载器加载
- 实现类加载的动态控制:有些应用需要对类的加载进行特殊的控制,例如对特定的类进行加密、解密或验证等操作
打破双亲委派的方法
自定义类加载器 通过自定义ClassLoader的子类,重写findClass()方法,实现自定义的类加载逻辑,不委托给父类加载器,从而打破双亲委派机制
使用Java动态代理 Java动态代理机制可以在运行时生成代理类,并在代理类中实现特定的逻辑。通过使用动态代理,可以在类加载时动态生成代理类,从而打破双亲委派机制
线程上下文类加载器通过Thread类的setContextClassLoader()方法,可以设置线程的上下文类加载器,从而打破双亲委派机制
当然还有其他方法和框架打破双亲委派,比如OSGi框架、动态代理框架等