类加载过程
类加载子系统
- 负责从文件获取网络加载
Class
字节流- 由此得知,加载一个字节码文件到内存,可以从本地,也可以从网络
- 负责读取字节码中的信息,加载到
JVM
运行时内存区域(方法区) - 任何字节码文件被加载到
JVM
之前,都要符合JVM
字节码规范
类加载过程图示
graph LR;
start(开始加载) --> classLoader[类加载器]
classLoader --> hasLoad{是否加载过}
hasLoad --否--> step1[第一步Loading加载阶段加载字节码到内存中]
hasLoad --是--> step2[第二步链接阶段/验证/准备/解析]
step1 --> step2
step2 --> step3[第三步initialzation初始化执行类构造器clinit]
step3 --> endLoad(加载完成)
- 验证(Verification):在验证阶段,Java虚拟机会对加载的字节码进行各种验证,以确保字节码的正确性和安全性。验证的内容包括语法检查、字节码验证、符号引用验证等。
- 准备(Preparation):在准备阶段,Java虚拟机会为类的静态变量分配内存,并设置默认初始值。这些静态变量会被存储在方法区中。
- 解析(Resolution):在解析阶段,Java虚拟机会将类、接口、字段和方法的符号引用解析为直接引用。解析的过程可以在编译期间进行静态解析,也可以在运行期间进行动态解析。
QA:为什么验证字节码不放在第一阶段执行?
因为第一步的时候字节码还不存在,如果在这个时候验证可能会出现各种问题,例如网络断开,文件读取失败等。
1.加载阶段
- 读取字节码二进制流
- 解析二进制流的静态数据,转换为
JVM
运行时方法区的数据 - 生成一个类的
java.lang.Class
对象,放入堆内存中,作为方法区的访问入口 - 检测是否存在父类,如果有执行父类的类加载,重新经过一次类加载流程
假设有两个类:Base类、User类,User继承Base,那么加载User的时候就会是以下流程:
// 获取user的class对象实例 Class clazz = User.class; // 通过class对象获取方法区的信息 Field age = clazz.getField("age");
堆内存存放的Class对象实例是一个访问的入口,可以通过对象实例访问方法区的数据。
Class实例何时会创建
-
使用
new
关键字User user = new User()
-
使用反射
Class clazz = Class.forName("cn.yufire.User")
-
子类创建时,父类自动加载
public class User extents Base
-
JVM
启动时,包含入口main
方法的主类 -
jdk1.7
后动态语言类型支持
2.Linking阶段
链接阶段分为3个小阶段:
验证阶段(Verify
):确保字节码符合虚拟机要求,不通虚拟机版本要求可能不一致。
- 文件格式校验
CAFEBABE
- 版本号
- ...
- 元数据匹配,语义分析
- 是否有父类
- 是否继承了final修饰的类
- ...
- 字节码验证,数据流与控制流分析
- 类型转换是否有效
- 无法执行到return?
- ...
- 符号引用
- 通过全限定名是否能找到类
ClassNotFoundException
- 通过全限定名是否能找到方法
NoSuchMethodError
- 通过全限定名是否能找到类
准备阶段(Prepare
):为字段赋予初始值。
- 为类的
static
字段赋予初始值public static int a = 100;
- 在准备阶段时,字段的值还是初始值,即
a=0
- 初始化阶段时才会使
a=100
- 初始化阶段时才会使
解析阶段(Resolve
):字节码内的符号引用#6
转换为直接引用。
-
其实就是将
#6
这些符号引用,转换成JVM
内存中实实在在的内存指针关联 -
例如:字节码中类描述信息中:
Super Class:cn.yufire.Base
-
解析后将描述信息的字符串转换为指针信息,指向内存中的
Base.class
的Class
实例 -
像这些符号引用转为内存指针引用主要针对4种类型的引用,这些东西在字节码中都是以符号引用的方式进行存储,都是静态的字符串 -> 转换为动态指针关联。
- 类解析
- 字段解析
- 方法解析
- 接口解析
3.初始化阶段
初始化阶段(Initialzation
)
- 初始化阶段是执行类构造器方法
()
的过程 - 类的初始化构造方法
()
不需要显式声明,Java
编译器会自动生成1。- 这个阶段和其它两个阶段的区别是:其它两个阶段是虚拟机主导的,和我们的代码无关,而这个阶段会使用我们的代码创建
方法,并完成类的初始化。
- 这个阶段和其它两个阶段的区别是:其它两个阶段是虚拟机主导的,和我们的代码无关,而这个阶段会使用我们的代码创建
初始化知识点
- 对类的变量进行赋值,和执行
static
代码块 - 子类初始化过程会优先执行父类的
方法
- 没有类变量以及
static{}
就不会产生 - 使用
-XX:+TraceClassLoading
查看类加载过程 方法默认会增加同步锁,确保只会执行一次。
案例
类中的所有static
属性和static{}
代码块如下
@Slf4j // lombok的注解,相当于生成一个static的log对象
public static final ThreadLocal MY_BATIS_METHOD_NAME = new ThreadLocal();
public static Integer NUM = 10;
static {
NUM = 20;
System.out.println("num已赋值");
}
我们类里的静态变量和static{}
代码自动生成的字节码如下:
Java 类加载器
类加载就是将.class
文件通过IO
读入内存,并生成一个Class对象的过程。
类加载通常由JVM
提供默认的类加载器,这些被称之为系统加载器,还可以通过继承ClassLoader
来实现自定义加载器。
一个类被加载到JVM
中后,通常不会被加载第二次,因为通过双亲委派模型可以很好的避免重复加载的问题。
一、类加载器种类
在Java
中默认的类加载器有三种,分别为:
- BootStrap ClassLoader (启动类加载器)
- 执行优先级最高的类加载器
- 用于加载
JRE
核心类库的.jar
文件,如rt.jar
- 使用
C
语言实现,双亲委派顶级祖宗加载器
- Extensions ClassLoader 也称 (Ext ClassLoader) (扩展类加载器)
- 用于加载
JRE
扩展目录下的.jar
文件 - 使用
Java
实现
- 用于加载
- Application ClassLoader (应用程序类加载器)
- 用于加载用户自己编写的类文件,和项目中引用其他
.jar
包中的类文件
- 用于加载用户自己编写的类文件,和项目中引用其他
- Custom ClassLoader (自定义加载器)
- 用于加载指定目录下的类,通常用于自定义实现
- 关系类图如下
二、双亲委派模型
1. 双亲委派模型其表达的意思是,类加载器与父亲加载器嵌套,当需要加载类的时候回去现寻找父亲加载器,如果有父亲加载器的话就优先使用父亲加载器,如果父亲加载器无法完成加载的话,再使用自己进行加载。
说人话:现在有一个苹果(Class),你不吃你给你妈妈吃,你妈妈不吃又给你奶奶吃,你奶奶不吃有给你妈妈吃,你妈妈不吃又给你吃。
2. 使用双亲委派模型有以下好处
- 可以避免重复加载类
- 虽然有多个类加载器,但是因为每个类都是唯一的,且都会通过父亲加载器加载,所以可以避免类加载重复。
- 可以避免
Java
核心类被用户篡改- 如果用户自定义了一个类为
java.lang.String
,那么会一层一层向上加载,直到BootStrap ClassLoader
祖宗类加载器(入口加载器),但是在这个加载器里已经将JDK
包下的java.lang.String
加载过了,所以会直接跳过加载用户编写的String
类。 - 但是此处有个
BUG
,就是双亲委派可以被破坏!
- 如果用户自定义了一个类为
3. 双亲委派模型图
三、破坏双亲委派
双亲委派有两个弊端:
- 不可以不委派
- 不可以向下委派
但是这两个弊端都可以被打破
- 通过自定义类加载器可以破坏:不可以不委派
- 通过
SPI
机制可以破坏:不能向下委派
源码中可以得知,双亲委派机制是由此处进行调用的
// AppClassLoader || ExtClassLoader
public Class loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
int i = name.lastIndexOf('.');
// 权限验证
if (i != -1) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPackageAccess(name.substring(0, i));
}
}
// 判断类在classPath中是否存在
if (ucp.knownToNotExist(name)) {
// The class of the given name is not found in the parent
// class loader as well as its local URLClassPath.
// Check if this class has already been defined dynamically;
// if so, return the loaded class; otherwise, skip the parent
// delegation and findClass.
// 查找此类是否已经被加载过了,如果是的话则返回Class对象,否则返回null,则调用父类loadClass()方法进行加载
Class c = findLoadedClass(name);
if (c != null) {
if (resolve) {
resolveClass(c);
}
return c;
}
throw new ClassNotFoundException(name);
}
// 调用父类的loadClass,内部实现了双亲委派
return (super.loadClass(name, resolve));
}
父类的loadClass()
protected Class loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 获得加载锁
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 查询是否被加载过了,如果加载过了,直接跳过
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 查找当前加载器是否有父亲加载器,(双亲委派机制)
if (parent != null) {
// 调用父亲加载器进行加载
c = parent.loadClass(name, false);
} else {
// 如果父亲加载器等于null的话,则代表执行当前方法的加载器为BootStrap ClassLoader,并使用此加载器进行加载
// native方法,由C实现
c = findBootstrapClassOrNull(name);
}
// 父亲加载器无法加载
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 如果父亲加载器无法加载的话,则自己在进行加载
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
1. 不委派父亲加载器
从上述源码中可以得知,双亲委派机制是由ClassLoader -> loadClass()
方法进行实现的,而且此方法非final
修饰,则代表此方法是可以被我们的自定义加载器重写的。
双亲委派的核心源码为:
// 查找当前加载器是否有父亲加载器,(双亲委派机制)
if (parent != null) {
// 调用父亲加载器进行加载
c = parent.loadClass(name, false);
}
如果我们将loadClass()
方法进行重写,且不执行这段逻辑的话,则双亲委派机制就会被打破,从而遭到破坏!
2. 为什么建议重写findClass()
方法
因为是出于历史原因,且JDK
是向上兼容的,所以该方法并不能随便进行更改,但是JDK
开发者也意识到了这个问题,所以在ClassLoader
抽象类中新增了findClass()
方法,供开发者进行重写,和实现自定义类加载器。
在JDK
源码中的loadClass()
方法上有一段注释说明了此方法不建议被重写:
/*加载具有指定二进制名称的类。该方法的默认实现按照以下顺序搜索类:调用findLoadedClass(String)检查类是否已经加载。在父类装入器上调用loadClass方法。如果父类为空,则使用虚拟机内置的类装入器。调用findClass(String)方法来查找类。如果使用上述步骤找到了类,并且resolve标志为真,则该方法将对生成的class对象调用resolveClass(class)方法。ClassLoader的子类被鼓励重写findClass(String),而不是这个方法。除非被重写,否则这个方法在整个类加载过程中同步getClassLoadingLock方法的结果。*/
protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException {...}
2. 向下委派子加载器
在Java
中通过SPI
机制可以实现向下委派子加载器,至于为什么这么做也是因为一些历史原因
在
JDK
中引入了SPI
机制后,例如JDBC
有个驱动管理类DriverManager
,他是在rt.jar
下的,由BootStrap ClassLoader
进行加载,但是由于这个类里需要加载所有的Driver
驱动,但是,像是MySQL
、Oracle
等数据库驱动都是由各家数据库厂商实现的,是以外部依赖的模式进行引入的,此时如果SPI
机制想要顺利的加载驱动,就不能使用BootStrap ClassLoader
因为各个数据库驱动并不在rt.jar
下,所以无法加载,因此JDK
搞了一个线程上下文类加载器,和setContextClassLoader()
来手动设置类加载器,才解决了这个问题,此时其实已经是调用了线程上下文类加载器,一般为Application ClassLoader
,进行加载驱动的动作了。
四、自定义Java内部
如果在自己的工程目录中自定义了一个和JDK
中同包,同类名的一个类,例如:java.lang.String
,会有什么样的结果?
首先我们了解类加载器和类加载器的双亲委派机制,因此得知,要加载这个类,可以使用Application ClassLoader
或Custom ClassLoader
进行加载,当其中一个类加载器加载时,会调用父亲的loadClass()
方法,最终调用到BootStrap ClassLoader
中去,但是由于java.lang.String
为JDK
核心类,并且已经被祖宗加载器加载过了,所以会跳过自己工程下的java.lang.String
。从而避免了核心类篡改和重复加载的问题。
此时如果调用自定义java.lang.String
类的方法,会报**NoSuchMethodError
**异常。因为自定义的类并没有被加载。
五、附
类加载图解
Footnotes
编译器自动生成的Class init
方法,并非默认的构造方法,不写类也会自动生成 ↩