前置知识
class文件的的结构
概述
前端编译器会将HelloWorld.java
文件编译为HelloWorld.class
文件,让后jvm就可以读取HelloWorld.class
文件进入内存,但是进入内存之前的操作,称之为类的加载。
进行类的加载的时候,是jvm程序已经启动的时候,这个时候程序已经运行了。
在加载阶段,会进行加载,连接,初始化
三个大体步骤,也就是说,java是在运行时才进行连接的,这样比起一些编译的时候进行连接的语言要多一些开销,但是这却增加了java的可扩展性和灵活性,使得java天生就可以动态扩展。
动态扩展的例子:
根据规范,下面这个才是规范的创建一个list,list中的类型是Object类型,即等号右边也要写Object类型
List<Object> list = new ArrayList<Object>();
但是,我们通常只写等号左边的object类型,即:
List<Object> list = new ArrayList<>();
也就是说,我们右边写了object类型后,在加载的时候,会自己的给我们补充成在这里插入代码片new ArrayList<Object>();
的样子
类的加载阶段
概述
一个类从加载到虚拟机的内存开始,到结束,总共经历了五个阶段,其中链接阶段分为3个小阶段,七个阶段的顺序如图所示:
注意:大多数时候下,类的生命周期是顺序开始执行的,但是,在某些情况下,解析可能会在初始化之后进行
开始执行的意思是:它们的开始顺序固定,但是,在开始之后,没结束之前,下一阶段也可能开始
加载
在加载阶段,jvm需要完成三件事情(摘至深入理解Java虚拟机第三版
P267):
- 通过一个类的全限定名(包名+类名)来获得定义此类的二进制字节流---->获得二进制字节流
- 将这个字节流所代表的静态存储结构转换为方法区(jdk8后叫元空间)的运行时数据结构----> 生成类模板
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口----> 生成对应的class对象
总结:将.class的字节码文件加载到机器的内存中,并生成对应的类模板对象和Class实例
什么是类模板对象
类模板对象是java类在jvm中存入的一个快照,jvm将字节码文件中解析出的常量池、类字段、类的方法等信息存入类模板中,这样jvm在运行期间便能通过类模板获得java类的任意信息,可以对java类的成员变量进行访问和方法进行调用,反射也就是用了这个原理。
而且在堆中存储的某一个具体对象的对象头
中,是包指向含类模板对象的信息
Class实例和类模板所在的位置
class实例,是一个具体的对象,放入堆中
类模板:因为几乎不会被回收,所以放入方法区(元空间)
关于class实例:
class类的构造方法是私有的,只有jvm能够创建class类。class类是对类模板的一个访问接口
,只有通过class类,才可以放入类模板
数组的加载
数组本身不是通过类加载器来进行加载的,它是由jvm虚拟机直接在内存中动态的构造出来的,但是数组的类型也是需要靠类加载器进行加载,创建数组大致如下过程(摘自深入理解java虚拟机第三版
P268):
- 如果数组的组件类型(数组去掉一个维度的类性,如Object[] 去掉一个维度为Object)是引用类型,那么让对应的加载器去加载此类,并将该数组和该加载器关联起来
- 若不上引用类型,那么将该数组和
引导类加载器
关联 - 数组类的可访问性和组件类型的访问性一致,若不是引用类型,则为public。
关于验证
前面说过,每个阶段的开始顺序大致是概述中的图示
,但是可能在加载的时候,也就开始了,也就是说,加载和验证阶段可以同时进行,但是加载的开始一定是在验证的前面
链接
验证
目的:
它的目的是保证加载的字节码是合法、合理并符合规范的
验证类型
大致分为如下的类型进行验证,
文件的格式验证–> 语义验证 -> 字节码验证 -> 符号引用验证
对验证的说明
在字节码验证中,若验证没通过,一定不安全,若通过,不一定安全。(参考停机问题)
符号引用验证,是保证解析阶段可以正常执行,发生在将符号引用转换为直接引用的时候
符号引用转换为直接引用:将class文件中的常量池中的数据(符号引用)转换为内存中某个具体位置的数据(直接引用)
关于加载
加载的时候说过,要将数据存入内存中的方法区,但是前提是要在文件格式验证阶段结束后,通过了验证,才可以放入内存中方法区存储,放入方法区后,后面的三种验证都是在方法区进行
验证中的参数以及错误
-XX:-UseSplitVerifier
:jdk6之后,关闭javac编译器的对验证进行的优化-XX:+FailOverToOldVerifier
: 在类型校验失败后,退回到旧的类型推导方式,对应jdk6后的版本来说,不允许退回原来的类型推导的校验方式,但是虚拟机中仍保留着推导校验的代码-Xverify:none
: 关闭大部分类的验证措施
错误类性:
验证失败会抛出一个java.lang.IncompatibleClassChangeError
的子类异常,如:java.lang.IllegalAccessError
,java.lang.NoSuchFieldError
,java.lang.NoSuchMethodError
准备
目的
为类中的静态变量分配内存空间并设置默认值,并对类中的静态常量分配空间并赋值
下面的代码,在准备阶段分配空间并设置值后分别是后面的情况
public static int value = 13; ===>>> public static int value = 0;
public static final int value = 13; ===>>> public static final int value = 13;
因为int类型的默认值就是0;
各种基本类型的默认值:
解析
目的
将符号引用转换为直接引用,也就是得到类、字段、方法在内存中的偏移量。
不过,由于java支持运行时绑定(晚期绑定),也就是字节码中支持了的invokedynamic
指令,所以有些时候解析在初始化后面才开始,如lambda表达式。
初始化
jvm规定,有且仅有主动引用的情况下,才会对类进行初始化。
主动引用
- 创建一个对象,如new、反射、序列化、克隆
- 调用静态方法
- 读取或修改一个类型的静态字段(strict final是属于常量字段)
- 父类没有进行初始化,则要初始化父类
- jvm启动的时候,用户执行的主线程的类,即
main方法
的类 - jdk8加的默认方法,如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,该接口要在其之前被初始化。
- 当使用java.lang.reflect包中的方法反射类的方法时。比如:Class.forName(“com.xing.java.Test”)
- 当初次调用
java.lang.invoke.MethodHandle
实例时,初始化该 MethodHandle 指向的方法所在的类。(涉及解析REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄对应的类)
如果针对代码,设置参数-XX:+TraceClassLoading,可以追踪类的加载信息并打印出来。
除了主动使用以外,其他的都是被动使用,不会进行初始化,也就不会调用<clinit>()
方法
()方法
<clinit>()
方法不是程序员写的代码,他是编译器自动生成的,初始化阶段也就是执行这个方法。
深入理解Java虚拟性第三版
中说到(P277):他是编译器自动生成物,但是我们非常有必要了解这个方法是如何具体产生的,以及<clinit>()
方法执行过程中各种有可能影响程序运行行为的细节。
1. 收集静态变量和静态语句块
()方法是由编译器自动收集类中的所有类变量的赋值和静态代码块,并对它们进行合并产生的。编译器收集的顺序由语句在源文件中出现的顺序决定的。
静态语句块中,如果要访问在源文件中还在该语句块后出现的数据的时候,会报非法前向引用,但是可以修改该变量
如图,在赋值的时候idea没报错,但是在访问的时候,idea报错
2. 父类的()
()和构造方法()不同,它不需要显示的调用父类构造器,jvm会保证父类的()以及别调用过了,在调用子类的();
// 父类
public class Father {
public static int id = 1;
public static int number;
static {
number = 2;
System.out.println("father static{}");
}
}
// 子类
public class SubInitialization extends Father {
static{
number = 4;//number属性必须提前已经加载:一定会先加载父类。
System.out.println("son static{}");
}
public static void main(String[] args) {
System.out.println(number);
}
}
如果上述的结论正确,那么这里输出的答案就是4,并且father先输出
输出结果:
father static{}
son static{}
4
3. 在主动使用的前提下无()方法
类:<clinit>()
方法的主要作用就是对静态代码块和静态变量进行操作,如果类中没有静态代码块和静态变量的赋值操作,那么也就不需要这个方法了。
接口:接口不能使用静态代码块,但是可以使用静态变量初始化进行赋值,也可以生成()方法,但是接口执行的时候,父接口不用先执行(),只有父接口在使用的时候,才会执行(),同样,接口是实现类在初始化 的时候也不会执行接口的初始化方法
4. ()的死锁问题
如果多个线程进行初始化的时候,若其中一个线程初始化的 时间长,其他的线程就会处于阻塞状态
或者两个类都使用反射进行初始化,但是两者都没有初始化完成,但是都卡住了对方进行初始化。
A进行初始化,但是会暂停一会,然后调用反射去加载 另外一个类
class StaticA {
static {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
try {
Class.forName("com.xxx.jvm.StaticB");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println("StaticA init OK");
}
}
class StaticB {
static {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
try {
Class.forName("com.xxx.jvm.StaticA");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println("StaticB init OK");
}
}
public static void main(String[] args) {
new Thread(()->{
try {
Class.forName("com.xxx.jvm.StaticA");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
},"a").start();
new Thread(()->{
try {
Class.forName("com.xxx.jvm.StaticB");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
},"b").start();
}
两个线程利用反射调用两个类,但是两个类初始化的时候,也利用反射调用另外一个类,但是另外一个类还在初始化中,所以产生了死锁。
()中的static和static final
一般来说,static定义的变量是在初始化阶段进行赋值的,static final 定义的常量是在准备阶段进行赋值的,但是也有例外的
static修饰的静态变量,都是在初始化阶段进行赋值。
static final修饰的常量,要分情况:
- 对象:即左边的类型是一个对象,左边的类型是new或者是调用静态方法产生的;
public static final Object XXX = Object;
因为涉及到对象,所以在左边阶段无法完成static final的初始化,需要在初始化阶段完成。
- string字符串
字符串分两种情况
- 直接设置字符串常量
public static final String _s0 _= “helloworld0”; //在链接阶段的准备环节赋值
- new 一个对象设置字符串常量
public static final String _s1 _= new String(“helloworld1”); //在初始化阶段赋值
关于两种不同的字符串情况,要理解字符串在jvm内存中 的结构的时候才好理解
总结: 除了直接设置字符串外,其他的引用类型的常量都要在初始化的时候才可以完成赋值,直接设置字符串和基本类型一样,可以在准备阶段赋值
类的使用
在类经历过前三个步骤后,就可以正常的使用了,调用静态方法,new 一个对象。
类的卸载
在加载的时候,说过一句类加载器要和此类相关联,所以在了解类的卸载之前,先处理类、类加载器、实例对象直接的关系:
可以看到,class实例和加载器是双向关联的关系,若要卸载类,则要卸载 类的模板,卸载类的模板,则要卸载Class类的实例,要去除Class类的实例,就要让xxx对象的实例为null,并且将类加载器也卸载,但是通常类加载器是加载很多的类,所以无法因为一个类要 类型就将其卸载。
所以,类加载到内存中后,基本是无法卸载的,只能卸载类的实例对象
参考资料
宋红康老师的jvm课程
周志明老师的《深入理解java虚拟机–第三版》