前言
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序交叉混合进行
,而解析阶段则不一定,它在某些情况下可能在初始化阶段后在开始,因为java支持运行时绑定。
正文
流程图
装载(Load)
查找和导入class文件
原理
- 通过一个类的全限定名获取定义此类的二进制字节流(此处有多种方式,不知道有没有人当作面试题,下文介绍)。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区中这些数据的访问入口。
加载.class文件方式
- 从本地系统加载
- 从网络下载后加载:例如小程序应用?
- 从jar,war中加载
- 从专有数据库中加载
- 将java源文件动态编译成.class文件然后加载:例如动态代理
- 从加密文件中获取后加载
存储空间
- 方法区:类信息,静态变量,常量。
- 堆:被加载类的java.lang.Class对象。
连接(Link)
加载阶段未完成,连接阶段已经开始。两者之间会交叉运行。
验证(Verify)
为了确保Class文件中的字节流包含的信息完全符合当前虚拟机的要求,
并且要求我们的代码不会危害虚拟机自身的安全,导致虚拟机崩溃。
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
准备(Prepare)
为类变量(静态变量)分配内存并且设置该变量的默认初始值;(PS:ConstantValue在这里就会被直接赋值)
这里不包括被final修饰的static变量,因为final修饰的在编译时就会分配内存,在准备阶段会显示初始化;
也不会被实例变量(没有static修饰的成员变量)分配内存,类变量是存储在方法区中,而实例变量是存储在java堆中。
解析(Resolve)
把类的符号引用转化为直接引用
初始化 (Initialize)
初始化阶段是执行类构造器()方法的过程
简单来说
在准备阶段是赋予类变量初始值,到这里就是赋予程序员定义的值,也包括其他资源
类变量进行初始值设定的两种方式
- 直接声明类变量的指定初始值
- 使用静态代码块为类变量指定初始值
初始化步骤
- 假如这个类还没有被加载和连接,则程序先加载并连接该类
- 假如该类的直接父类还没有被初始化,则先初始化其直接父类
- 假如类中有初始化语句,则系统依次执行这些初始化语句
使用
注意:只有对类的主动使用才会导致类的初始化。
主动使用
- new
- 访问类/接口的静态变量,或者对该静态变量进行赋值操作
- 调用类的静态方法
- 反射
- 初始化某个类的子类,则其父类也会被初始化
- 直接运行某个类,即main()
被动使用
- 使用其父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化。
- 定义类数组,不会导致类的初始化。
Object[] o = new Object[6];
- 引用类的static final常量,不用引起类的初始化(如果只有static修饰,还是会引起该类的初始化)。
卸载
在方法区中清空类信息
在类使用完之后,如果满足下面的情况,类就会被卸载:
- 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
- 加载该类的ClassLoader已经被回收。
- 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
类加载机制
- 全盘负责:当一个类加载器加载一个class对象时,其依赖的,引用的class对像也会被这个类加载器加载,除非显示的使用另一个类加载器加载。
- 父类委托:当一个类加载器加载一个class对象时,会先交给父类加载器进行加载,只有在父类加载器检索不到对应的字节码文件时,才在自身的类路径去查找,并加载。
- 缓存机制:将所有加载过的class对象全部缓存到内存中,当类加载器需要使用时都会在内存中去查找对应的class对象,如果没有则会去查找到对应的二进制数据,将其转化为class对象,将其存入缓存中。
如何破坏双亲委派机制
- SPI: 如java.sql.Driver是定义在rt.jar中的接口,按照双亲委派规则会由bootstrap classLoader进行加载,但是Driver可以由用户自己实现,所以会被线程上下文加载器(ThreadContextClassLoader)即系统类加载器(System ClassLoader)进行加载。
- 自己实现classLoader: 继承抽象类ClassLoader,并重写loadClass方法。显示的使用自己定义的类加载器去加载指定的类,并且需要将这个类移出当前目录,防止还会被之前的类加载器找到并加载。