1、什么是类加载
在我们使用一个类之前,JVM需要将该类的字节码文件(.class文件)中的二进制数据从网络,磁盘或其他来源加载到内存中,将其放在运行时数据区的
方法区内,然后再
堆创建一个
java.lang.Class的对象。
![](https://i-blog.csdnimg.cn/blog_migrate/ffe383d60f41c4f8e79127bb3f69d479.png)
2、类加载过程
类从被加载到虚拟机内存开始,直到被卸载出内存为止,它的加载过程包括了
加载、验证、准备、解析、初始化、使用、卸载几个阶段,其中验证、准备、解析统称为连接,如下图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/28154398629fa46d51ce83ba070874ef.png)
其中
加载、
验证、
准备、
初始化这几个阶段发生的顺序是确定的,解析则不一定,为了支持Java的动态
绑定,它有可能在初始化之后开始。
绑定指的是把一个方法的调用和方法所在类(方法主体)关联起来,对java来说绑定分为
动态绑定和
静态绑定。
- 动态绑定:运行时绑定,在运行时根据具体的对象类型进行绑定,在java中几乎所有的方法都是动态绑定的。
- 静态绑定:前期绑定,在程序执行前方法已经被绑定。针对java,简单的可以理解为程序编译期的绑定。java当中的方法只有final,static,private和构造方法是前期绑定的。
加载
加载是类加载过程的第一个阶段,开发人员既可以通过系统提供的类加载器来完成加载,也可以自定义自己的类加载器来实现加载,在加载阶段,虚拟机需要完成以下三件事情:
- 通过一个类的全限定名来获取其定义的二进制字节流。
- 将这个二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区中这些数据的访问入口。
验证
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全,验证阶段大概会有四个阶段的检验步骤:
- 文件格式验证:验证字节流是否符合Class文件格式规范(是否以魔数0xCAFEBABE开头,主次版本号是否在虚拟机的处理范围之内,常量池中的常量是否有被不支持的类型)输入的字节流能够正确的解析并存在方法区之内。
- 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范要求;
- 字节码验证:通过数据流和控制流分析,确定程序的语义是合法是,是符合逻辑的(该阶段是最复杂的一个阶段)。
- 符号引用验证:虚拟机将符号引用转化为直接引用的时候,可以看做是对类自身以外的信息进行匹配性的校验。
准备
准备是正式为
类变量分配内存并设置
类变量初始值的阶段,这些内存都将在方法区中分配,该阶段有以下几点需要注意:
- 这时候进行内存分配的仅包括类变量(static),不包括实例变量,实例变量会在对象实例化的时候,随对象一起分配在堆内存中。
- 这里所设置的初始化的值通常情况下是数据类型默认的0值(如:0,0L,null,false等),而不是Java代码被显示赋予的值。假设一个变量被定义为:public staic int value = 7;那么变量value在准备阶段过后初始值仍然是0,不是3,因为这个时候尚未开始执行任何Java方法,把value赋值为3的putstatic指令是在程序编译后,存放于构造器<client>方法之中的,所以吧 value赋值为3的的动作在初始化阶段才会执行。
- 如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。 假设上面的类变量value被定义为: public static final int value = 3,编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。
基本数据类型的零值
数据类型
|
零值
|
int
|
0
|
short
|
(short)0
|
byte
|
(byte)0
|
long
|
0L
|
float
|
0.0f
|
double
|
0.0d
|
boolean
|
false
|
reference
|
null
|
char
|
‘\u0000'
|
解析
解析阶段是虚拟机将常量池内的
符号引用替换为
直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
符号引用:用一组符号表示描述目标,可以使任意字面量。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载在内存中。
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
直接引用:就是直接指向目标的指针,相对偏移量或者是一个间接定位到目标的句柄。如果有了直接引用,那么引用目标必定已经在内存中存在。
对同一个符号引用进行多次解析请求时很常见的事情,虚拟机实现可能会对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标示为已解析状态),从而避免解析动作重复进行。如果一个符号引用之前就已经解析成功,那么后续的引用解析请求就应当一直成功,如果第一次解析失败,其他指令对这个符号的解析也应该收到相同的异常。
解析动作主要针对
类或接口、
字段、
类方法、
接口方法四类引用符号进行。
初始化
当一个Java类第一次被真正用到的到时候,JVM会进行该类的初始化操作,类的初始化分为两步:
- 如果基类没有被初始化,初始化基类。
- 有类构造函数,则执行类构造函数。
JVM初始化步骤:
- 假如这个类还没有被加载和连接,则程序先加载并连接该类。
- 假如该类的直接父类还没有被初始化,则先初始化其直接父类。
- 假如类中有初始化语句,则系统依次执行这些初始化语句。
3.类加载机制
如下图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/4537fdeba2bfe94d7324a7a475350e1c.png)
运行后输出结果:
![](https://i-blog.csdnimg.cn/blog_migrate/1f6e0f7676ea089e7cec7ff6e5d8f846.png)
从结果我们可以看出,默认情况下,用户自定义的类使用
AppClassLoader
加载,
AppClassLoader
的父加载器为
ExtClassLoader
,但是
ExtClassLoader
的父加载器却显示为空,这是因为
Bootstrap Loader
是用C++语言写的,依java的观点来看,逻辑上并不存在
Bootstrap Loader
的类实体,所以在
java
程序代码里试图打印出其内容时,我们就会看到输出为
null
几种类加载器的层次关系如下图:
![](https://i-blog.csdnimg.cn/blog_migrate/3414e531e62afd8dd14e6482f400fc47.png)
注:这里父类加载器并不是通过继承关系来实现的,而是采用组合实现。
(附:classPatch指WEB-INF/classes,lib)
4.双亲委派模型
双亲委派模型工作过程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。
试想如果一个人写了一个恶意的基础类
(如
java.lang.String
)
并加载到
JVM
将会引起严重的后果,而有了双亲委派模型,黑客自定义的java.lang.String类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的java.lang.String类,最终自定义的类加载器无法加载java.lang.String类。
如何实现双亲委派模型:
每次通过先委托父类加载器加载,当父类加载器无法加载时,再自己加载 。
如何破坏双亲委派模型:
- 用户重写loadClass()方法实现自定义加载逻辑。
- Java的SPI机制。
- 热部署,热加载等技术。
http://www.codeceo.com/article/java-class-loader-learn.html
http://www.cnblogs.com/ityouknow/p/5603287.html
《深入理解JVM虚拟机》