什么叫做类加载?
类加载的定义: JVM把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终变成可以被JVM直接使用的Java类型(因为可以动态产生,这里的Class文件并不是具体存在磁盘中的文件,而是二进制数据流)
一个类型被加载到内存使用 到 结束卸载出内存,它的生命周期分为7个阶段: 加载->验证->准备->解析->初始化->使用->卸载
其中重要阶段一般的开始顺序: 加载->验证->准备->解析->初始化
验证,准备,解析合起来又称为连接所以也可以是加载->连接->初始化
注意这里的顺序是一般的开始顺序,并不一定是执行完某个阶段结束后才开始执行下一个阶段,也可以是执行到某个阶段的中途就开始执行下一个阶段
还有种特殊情况就是解析可能在初始化之后(因为Java运行时的动态绑定)
基本数据类型不需要加载,引用类型才需要被类加载
接下来将对这五个阶段进行详细介绍
Loading
注意
Verification
确保要加载的字节码符合规范,防止危害JVM安全
验证阶段是一个非常重要的阶段,但又不一定要执行(因为许多第三方的类,自己封装的类等都被反复"实验"过了)
在生产阶段可以考虑关闭 -Xverify:none以此来缩短类加载时间
Preparation
准备阶段为类变量(静态变量)分配内存并默认初始化
ConstantValue
属性Resolution
将常量池中的常量池中符号引用替换为直接引用(把符号引用代表的地址替换为真实地址)
可以是类加载时就对常量池的符号引用解析为直接引用
也可以在符号引用要使用的时候再去解析(动态调用时只能是这种情况)
当我们要访问一个未解析过的类时
IllegalAccessError
解析一个从未解析过的字段
NoSuchFieldError
异常IllegalAccessError
异常)确保JVM获得字段唯一解析结果
如果同名字段出现在父类,接口等中,编译器有时会更加严格,直接拒绝编译Class文件
解析一个从未解析过的方法
IncompatibleClassChangeError
异常AbstractMethodError
异常(自身找不到,父类中找不到,最后在接口中找到了,说明他是抽象类),找不到抛出NoSuchMethodError
异常IllegalAccessError
异常)解析一个从未解析过的接口方法
IncompatibleClassChangeError
异常NoSuchMethodError
Initializtion
执行类构造器<clinit>的过程
ConstantValue
属性的类变量初始化已经在准备阶段做过了,不会在这里初始化) public class TestJVM {
static class A{
static {
if (true){
System.out.println(Thread.currentThread().getName() + "<clinit> init");
while (true){
}
}
}
}
@Test
public void test(){
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "start");
A a = new A();
System.out.println(Thread.currentThread().getName() + "end");
}
};
new Thread(runnable,"1号线程").start();
new Thread(runnable,"2号线程").start();
}
}
/*
1号线程start
2号线程start
1号线程<clinit> init
*/
JVM规定6种情况下必须进行初始化(主动引用)
java.lan.reflect
包中方法对类型进行反射当访问静态字段时,只有真正声明这个字段的类才会被初始化
(子类访问父类静态变量)
public class TestMain {
static {
System.out.println("main方法所在的类初始化");
}
public static void main(String[] args) {
System.out.println(Sup.i);
}
}
class Sub{
static {
System.out.println("子类初始化");
}
}
class Sup{
static {
System.out.println("父类初始化");
}
static int i = 100;
}
/*
main方法所在的类初始化
父类初始化
100
*/
子类调用父类静态变量是在父类类加载初始化的时候赋值的,所以子类不会类加载
实例数组
public class TestArr {
static {
System.out.println("main方法所在的类初始化");
}
public static void main(String[] args) {
Arr[] arrs = new Arr[1];
}
}
class Arr{
static {
System.out.println("arr初始化");
}
}
/*
main方法所在的类初始化
*/
例子里包名为:org.fenixsoft.classloading。该例子没有触发类org.fenixsoft.classloading.Arr的初始化阶段,但触发了另外一个名为“[Lorg.fenixsoft.classloading.Arr”的类的初始化阶段,对于用户代码来说,这并不是一个合法的类名称,它是一个由虚拟机自动生成的、直接继承于Object的子类,创建动作由字节码指令anewarray触发. 这个类代表了一个元素类型为org.fenixsoft.classloading.Arr的一维数组,数组中应有的属性和方法(用户可直接使用的只有被修饰为public的length属性和clone()方法)都实现在这个类里。
创建数组时不会对数组中的类型对象(Arr)发生类加载
虚拟机自动生成的一个类,管理Arr的数组,会对这个类进行类加载
调用静态常量
public class TestConstant {
static {
System.out.println("main方法所在的类初始化");
}
public static void main(String[] args) {
System.out.println(Constant.NUM);
}
}
class Constant{
static {
System.out.println("Constant初始化");
}
static final int NUM = 555;
}
/*
main方法所在的类初始化
555
*/
我们在连接阶段的准备中说明过,如果静态变量字段表中有ConstantValue
(被final修饰)它在准备阶段就已经完成初始默认值了,不用进行初始化
调用classLoader类的loadClass()方法加载类不导致类初始化
方法区的垃圾回收主要有两部分: 不使用的常量和类
回收方法区性价比比较低,因为不使用的常量和类比较少
不使用的常量
没有任何地方引用常量池中的某常量,则该常量会在垃圾回收时,被收集器回收
不使用的类
成为不使用的类需要满足以下要求:
注意: 就算被允许回收也不一定会被回收, 一般只会回收自定义的类加载器加载的类
本篇文章围绕类加载阶段流程的加载-验证-准备-解析-初始化-卸载 详细展开每个阶段的细节
加载阶段主要是类加载器加载字节码流,将静态结构(静态常量池)转换为运行时常量池,生成class对象
验证阶段验证安全确保不会危害到JVM,主要验证文件格式,类的元数据信息、字节码、符号引用等
准备阶段为类变量分配内存并默认初始化零值
解析阶段将常量池的符号引用替换为直接引用
初始化阶段执行类构造器(类变量赋值与类代码块的合并)
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。