目录
【JVM类加载机制】学习视频来自B站寒食君
类加载机制
类加载流程的目的:把一份被javac编译过的class文本文件,通过加载生成某种形式的Class数据结构进入内存,程序可以调用这个数据结构来构造出Object。
这个过程是在运行时进行的,所以这也是Java动态拓展性的根基。

来看《深入理解JVM》中的一张图:

-
这张图其实展示了一个类的生命周期,在最开始加上javac编译阶段显得更完整。而 【类加载】只包括加载、连接、初始化这3个过程。
-
区分“类加载”与“加载”:“加载”只是“类加载”的第一个环节
-
“解析”部分是灵活的,它可以在初始化环节之后再进行,实现所谓的“后期绑定”,其它环节的顺序不可改变。
加载Loading
Java是一种具有动态性的解释型语言,类Class只有被加载到JVM后才能运行。
加载是类型的加载,就是读取Class文件,将其转化为某种静态数据结构存储在方法区内,并在堆中生成一个便于用户调用的Class类型的对象的过程。
- Class文件泛指各种来源的二进制流,比如说来自于网络、数据库、甚至是即时生成的Class文件。动态代理技术就是使用了即时计算出来的CLass,然后实例化代理对象。
- 最终在堆中生成Class对象,注意不是目标类对象,该对象封装了类在方法区中的数据结构,并且想用户提供了访问方法区数据结构的接口,即Java反射的接口
通过什么来进行加载?类加载器
连接Linking
连接阶段负责把类的二进制数据合并到JRE中,即把Java类的二进制代码合并到JVM的运行状态之中。
验证Verification
验证主要是为了确保类符合JVM规范,没有安全方面的问题。
验证动作有很多个步骤,分散在不同的阶段内:

首先是对文件格式的验证:发生在加载阶段,如果通过,那么才能顺利加载;
顺利加载之后,此时方法区内虽然已经存在了该class的静态结构,堆中也存在了该class类型的对象,但是这并不代表JVM已经完全认可了这个类。
程序要想使用这个类就必须进行连接,连接的第一步就是对这个类进行进一步验证,主要是对class静态结构进行语法和语义上的分析,保证其不会产生危害JVM的行为。
如果这一步验证也通过,那么JVM会姑且认为该class是安全的,但是后面在解析阶段还有一道符号引用的验证。
准备Preparation
在通过元数据字节码验证之后,JVM会姑且认为该class是安全的,这时进入准备阶段:为该类型中的静态变量赋类型0值。

解析Resolution
该阶段主要做的事情就是将符号引用替换为直接引用。

-
符号引用:
假如A类中引用了B类,在编译阶段,A不知道B是否被编译了,而且此时B也一定没有被加载,所以A不知道B的实际地址,那么在A的class文件中,将使用一个字符串S来代替B的地址。S就是符号引用。
-
直接引用:
在运行时,如果A发生了类加载,到了解析阶段会发现B还未被加载,那么就会触发B的类加载,将B加载到虚拟机中,此时A中B的符号引用就会被替换为B的实际地址,这被称作为直接引用,此时A就能真正地调用到B。
-
补充
Java通过后期绑定的方式来实现多态,那么后期绑定是如何实现的呢?其实就是这里的动态解析。
-
静态解析:
如果A调用的B是一个具体的实现类,那么就称为静态解析。
-
动态解析:
假如上层Java代码使用了多态,这里的B可能是一个抽象类或者接口,它有两个具体的实现类C和D。此时B的具体实现并不明确,所以也就不知道使用哪个具体类的直接引用来进行替换。既然不知道,那就只能等到运行过程中发生了调用,此时JVM调用栈中将会得到具体的类型信息,这时候再进行解析,就能用明确的直接引用来替换符号引用了。
这也是为什么“解析”有时候会发生在“初始化”之后,这就是动态解析,用它来实现上层的后期绑定和多态,底层对应了
invokevirtual
指令来实现。
-
-
解析步骤完成,意味着整个连接部分的完成,这也就是说外部加载的Java类已经成功地引用到了程序中。
初始化Initialization
此时会判断代码中是否存在主动的资源初始化操作,如果有则执行。
这里的主动初始化操作不是构造函数,而是class层面的,比如成员变量、静态变量的赋值动作以及静态代码块的逻辑。
只有显示调用new指令,才会调用构造函数进行对象实例化,这是对象层面的。
类加载总结

类加载器
加载过程是由类加载器完成的,具体来说,是由 ClassLoader
及其子类来实现的。
类加载器根据类的全限定名将class文件加载到JVM内存,转为Class对象。
什么时候需要加载类
- 隐式加载:new(),会隐式地调用类加载器去加载类,获取对应Class后会自动进行实例化
- 显示加载:直接调用loadClass()和Class.forName()
类加载器分类

其它几种ClassLoader都只能从本地文件中获取字节码来进行加载,而 User ClassLoader
能够让用户获取任何来源的字节码,这也就印证了 “在类加载机制中,允许用户从各个渠道获取class文件的二进制流来进行加载” 的这个结论。
双亲委派机制
ClassLoader命名空间
类加载器的命名空间 是由类加载器本身以及所有父加载器所加载出来的binary name(full class name)组成
JVM规范:每个ClassLoader都有属于自己的命名空间,命名空间相互之间无法感知。
这也就是说,用不同的ClassLoader即使去加载同一个限定名的类,JVM也会认为它们是完全不同的类。
关于命名空间的一些规则:
- 在同一个命名空间里,不允许出现二个完全一样的binary name。
- 在不同的命名空间中,可以出现二个相同的binary name。此时二者对应的Class对象是相互不能感知到的,也就是说Class对象的类型是不一样的。
- 子加载器的命名空间中的binary name对应的类中可以访问父加载器命名空间中binary name对应的类,反之不行。
出现问题:一个类如果被不同的加载器加载,或者说不同的加载器加载了【限定名一样、但内容不一样】的类,这样会给程序带来混乱。
所以目前的需求是:默认情况下,一个限定名的类只会被一个ClassLoader加载并解析使用,这样在程序中,它就是唯一的,不会产生歧义。
如何实现这种需求?引入双亲委派模型
类加载器的层级结构

区别于JDK中的类加载继承关系:ClassLoader <– SecureClassLoader <– URLClassLoader <– ExtClassLoader/AppClassLoader
所以双亲委派机制就是:需要加载某个类时,会自底向上地把这个请求委派给父加载器来完成,当父加载器无法加载时,子加载器会自顶向下的尝试加载。
如何判断自己无法加载?根据类的限定名,ClassLoader没有在自己负责的加载路径中找到该类则为无法加载(即ClassNotFoundException
)。
双亲委派的实现体现在
ClassLoader.loadClass()
里,后面会在源码中具体分析、
采用双亲委派就能避免重复加载,比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的 Bootstrap ClassLoader
进行加载,这样就保证了使用不同的ClassLoader最终得到的都是同样一个Object对象。
相关源码
-
ClassLoader介绍
-
loadClass():根据限定名去加载指定的类
-
ClassLoader. getSystemClassLoader():获取系统类加载器
Launcher的创建通过
Launcher.getLauncher()
来实现:可以看到在这个方法中初始化了
ExtClassLoader
和AppClassLoader
:-
ExtClassLoader的创建:
-
AppClassLoader的创建:
-
破坏双亲委派
继承ClassLoader类,重写loadCLass和findClass方法