目录
类加载过程
包括以下 7 个阶段:
- 加载
- 验证
- 准备
- 解析
- 初始化
- 使用(Using)
- 卸载(Unloading)
加载
- 通过全限定类名定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构方法区的运行时数据结构
- 在内存中生成一个代表这个类的 java.lang.Class
验证
验证文件是否符合JVM规定,验证开头是否为 CAFEBABE
准备
给静态成员变量赋予默认值
解析
- 将类、方法、符号引用解析成直接引用
- 常量池中的各种符号引用解析成指针等内存地址的直接引用
初始化
- 调用类初始化代码,给静态变量赋予初始值
- 先进行赋予默认值(null / 0),再赋予初始值
动态链接
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么功态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
类加载器
启动类加载器
Bootstrap ClassLoader 负责加载$JAVA_HOME中 jre/lib/rt.jar 里所有的class或Xbootclassoath选项指定的jar包。由C++实现,不是ClassLoader子类。
扩展类加载器
Extension ClassLoader 负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar 或 -Djava.ext.dirs指定目录下的jar包。
系统类加载器
App ClassLoader 负责加载classpath中指定的jar包及 Djava.class.path 所指定目录下的类和jar包。
自定义类加载器
Custom ClassLoader 通过java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自身需要自定义的Class
为什么我们的类加载器要分层?
1.2版本的JVM中,只有一个类加载器,就是现在的“Bootstrap”类加载器。也就是根类加载器。但是这样会出现一个问题。假如用户调用他编写的java.lang.String类。理论上该类可以访问和改变java.lang包下其他类的默认访问修饰符的属性和方法的能力。也就是说,我们其他的类使用String时也会调用这个类,因为只有一个类加载器,我无法判定到底加载哪个。因为Java语言本身并没有阻止这种行为,所以会出现问题。
这个时候,我们就想到,可不可以使用不同级别的类加载器来对我们的信任级别做一个区分呢?
比如用三种基础的类加载器做为我们的三种不同的信任级别。最可信的级别是java核心API类。然后是安装的拓展类,最后才是在类路径中的类(属于你本机的类)。
所以,我们三种基础的类加载器由此而生。但是这是我们开发人员的视角。
JVM类加载机制
全盘委托机制
当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
(当ClassLoder(类加载器)加载一个类时,该类所依赖或引用的类也由该ClassLoder(类加载器)加载,除非指定使用另一个ClassLoder(类加载器))
什么是父类委托机制/双亲委托机制?
“双亲委派”是指子类加载器如果没有加载过该目标类,就先委托父类加载器加载该
目标类,只有在父类加载器找不到字节码文件的情况下才从自己的类路径中查找并装载目标类。
父类委托别名就叫双亲委派机制。
“双亲委派”机制加载Class的具体过程是:
1. ClassLoader先判断该Class是否已加载,如果已加载,则返回Class对象;如果没有则委托给父类加载器。
2. 父类加载器判断是否加载过该Class,如果已加载,则返回Class对象;如果没有则委托给祖父类加载器。
3. 依此类推,直到始祖类加载器(引用类加载器)。
4. 始祖类加载器判断是否加载过该Class,如果已加载,则返回Class对象;如果没有则尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则返回Class对象;如果载入失败,则委托给始祖类加载器的子类加载器。
5. 始祖类加载器的子类加载器尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则返回Class对象;如果载入失败,则委托给始祖类加载器的孙类加载器。
6. 依此类推,直到源ClassLoader。
7. 源ClassLoader尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则返回Class对象;如果载入失败,源ClassLoader不会再委托其子类加载器,而是抛出异常。
“双亲委派”机制只是Java推荐的机制,并不是强制的机制。
我们可以继承java.lang.ClassLoader类,实现自己的类加载器。如果想保持双亲委派模型,就应该重写findClass(name)方法;如果想破坏双亲委派模型,可以重写loadClass(name)方法。
实际上其原理就是在AppClassLoader.loadClass方法中进行执行的,主要流程:
1、检查该类是否已经被本类加载器加载,如果已经加载直接返回对象(被加载的对象)
2、如果没有被加载则判断是否存在父类加载器,存在即调用父类加载器的loadClass方法,若无父类加载器(说明该加载器为引导类加载器),调用bootstrap类加载器加载。
3、若到达bootstrap类加载器,并且bootstrap也未加载该类,则调用findClass方法尝试加载该类.
4、若加载到该类,直接返回。未加载该类则进行向下委派,由子类加载器调用findClass方法进行尝试加载。
双亲委派的原因
- 出于安全性考虑【防止程序员自己构建 Java.lang.Class 对象】
- 资源浪费问题【防止类重新进行加载】
双亲委派的作用
沙箱安全机制: 你自己写的同包名同类的java核心类时,双亲委派机制依旧会加载核心库中的类,而不会加载你所创建的类,从而保证核心类库不被随意篡改。
避免重复加载;当子类加载器加载过该类就无需向上委托。而当父类加载器加载了该类直接返回,而子类加载器无需加载,保证有且只有一个类加载器加载了该类,并且该类只被加载了一次。保证被加载类的唯一性
如何打破双亲委派机制?
1、自定义类加载器 ,重写loadclass方法。改变其 Parent 的引用。典型的打破双亲委派模型的框架和中间件有tomcat与osgi
2、SPI机制绕开loadclass 方法。当前线程设定关联类加载器
JDK 1.2 之前,自定义 ClassLoader 都必须要重写 LoadClass()
热启动、热部署: 直接将 ClassLoad 全部干掉,重新进行加载 ClassLoad
什么是SPI?
SPI(Service Provider Interface)是Java平台提供的一种服务发现机制。它允许应用程序定义一组接口或抽象类,并通过SPI机制动态加载实现这些接口或抽象类的具体实现类。
SPI的核心思想是将接口的实现与接口本身解耦,使得应用程序可以在运行时动态地替换或扩展实现类,而无需修改源代码。这种可插拔式的设计可以帮助实现组件化、可扩展和松耦合的架构。
SPI(Service provide interface),直译过来是服务提供接口,在这里指的是厂商负责定义一个接口但不负责提供实现类,定义完接口后厂商直接使用这个接口的方法,但是如果不给此接口提供实现肯定运行要报错的,所以谁要想用厂商这个接口,谁负责实现。
SPI使用场景
1、jdbc4(jdbc4是随着jdk1.6发布的,此版本才开始支持SPI)
2、springboot的自动话配置也是同样的原理
3、阿里的dubbo
4、其他 TOMCAT、热部署、OSGI
为什么SPI打破了双亲委派?
SPI机制通过反转类加载的委派方向,打破了双亲委派模型。在SPI中,接口或抽象类定义了一组服务接口,而具体的实现则是由各个组件自行提供并在运行时动态加载。在SPI中,类加载器会首先尝试加载应用程序自身提供的实现,而不是委派给父加载器。这样可以实现应用程序级别的扩展和定制。
引用: