文章目录
一. 问题背景
遇到一道面试题“简述java类加载机制”。今天了解一下类加载。
此笔记仅供自己参考,如有错误请指正
参考:java中级程序员必会的教程,解密JVM【黑马程序员出品】
二. 类的生命周期
类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期包括:加载;验证;准备;解析;初始化;使用;卸载。其中验证;准备;解析统称为链接。
发生的顺序如下:
加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后进行,这是为了支持Java的运行时绑定(即动态绑定 或者 晚期绑定)。这里按部就班地开始,非按部就班地进行 或者 完成。因为这些阶段通常是交叉运行的。通常会在一个阶段中调用或激活另外一个阶段。
三. 类加载的时机
虚拟机规范并没有强行约束类加载的加载阶段什么时候开始,而是由虚拟机自行把握的。但虚拟机规范严格规定初始化阶段有且仅有4种情况必须立即对类进行初始化(而加载、验证、准备自然要在此之前开始)
四. 类加载全过程(有哪些阶段)
我们编译的java代码都是以.java格式文件保存的,而它是不能被jvm执行。jvm需要将.java文件编译成.class文件(即字节码技术),jvm才能真正地执行我们编写的代码。
类加载分为3个大阶段:加载阶段;链接阶段;初始化阶段。而链接阶段分为:验证阶段;准备阶段;解析阶段。如下所示:
4.1 加载阶段
加载阶段是将类的字节码载入方法区中(如果该类的父类还没加载,则先加载父类),其内部采用C++的instanceKlass描述java类,该instanceKlass的重要fields有如下:
- _java_mirror即java类的镜像,如String类,就是String.class,作用是把klass暴露给java使用
- _super即父类
- _fields即成员变量
- _methods即成员方法
- _constants即常量池
- _class_loader即类加载器
- _vtable虚方发表
- _itable接口方法表
因为klass是采用C++写的,所以java不能直接访问klass,所以就有了java_mirror,其作用是充当了java与c++的桥梁,使得java通过java_mirror能访问到klass。
如下图:
注:instanceKlass的元数据是存储在方法区内(即jdk1.8的元空间),而java_mirror是存储在堆中的。
4.2 链接阶段
链接阶段分为3个:验证阶段;准备阶段;解析阶段
4.2.1 验证阶段
验证加载进来的类(实际上指字节码)是否符合jvm规范,这是一个安全性检查。
加入修改了类的字节码,比如它的魔数,运行的时候将会报出ClassFormatError,如下:
4.2.2 准备阶段
准备阶段是为static变量分配空间,设置默认值。
内容有:
-
static变量jdk1.7以前存储与instanceKlass的末尾,而jdk1.7及以后则存于java_mirror末尾(因为java_mirror在jdk1.7及以后是存储在堆中的,所以static变量也是存于堆中)
-
static变量分配空间和赋值是2个步骤:分配空间是在准备阶段完成的;赋值是在初始化阶段完成的
-
如果static变量是final的基本数据类型 以及 字符串常量,那么编译阶段值就确定了,所以赋值在准备阶段完成
-
如果static变量是final的引用类型,那么赋值在初始化阶段完成
如下:
4.2.3 解析阶段
将常量池的符号引用解析为直接引用(即将符号引用改为内存地址,直接去内存地址查找)
如下:
4.3 初始化阶段
初始化即调用<cinit>()V
,虚拟机会保证这个类的构造方法的线程安全。
类初始化是懒惰的,即当jvm用的某个类,才会去加载那个类。
4.3.1 导致类初始化的情况
-
main方法所在的类,总会被首先初始化
-
new会导致初始化
-
首次访问类的静态变量或静态方法时
-
子类初始化,如果父类还未初始化,则先初始化父类
-
子类访问父类的静态变量,只初始化父类
-
Class.forName触发初始化
4.3.2 不导致类初始化的情况
-
访问该类的static final静态常量(即基本数据类型 以及 字符串常量),因为它们的值在编译阶段就确定,然后static final变量在准备阶段会完成分配空间 以及 赋值 ,所以不会触发初始化
-
类对象.class不会触发初始化(因为类对象.class是在类加载阶段完成的,即字节码)
-
类加载器的loadClass方法
-
Class.forName的参数2为false时