1.该篇文章通过类加载的生命周期来讲述运行时数据区的概念
2.JVM主要分为加载和执行俩大块。其中类加载器负责class文件的寻址和加载,执行引擎负责字节码指令执行及内存管理等。下面是JVM的结构体系图:
一、类加载的生命周期
1.这里我们需要了解下class文件的结构,详细的请看这篇。。。有了对class文件结构的认识,可以有助我们理解类加载过程及运行时数据区。
1.加载:
1).获取字节流
2).将字节流的静态储存结构转化为方法区的运行时数据结构
3).在内存中生成一个代表这个类的java.lang.Class对象,来作为方法区这个类各种数据的访问入口
注:方法区是存放类信息,静态变量、 常量及即时编译后的代码等数据。类加载阶段会把将class文件解析成Class对象(类信息)放入方法区。
2.连接
1.验证:包括4个验证阶段
1).文件格式验证:
例如:判断魔数是否符合Class文件格式规范
CONSTANT_Utf8_info型的常量中是否有不符合utf8编码的数据
2).元数据验证(对类的元数据信息进行校验)
1).这个类是否有父类(除了java.lang.Object类之外,所有类都应该有父类)
2).这个类是否继承了fianl修饰的类
3).验证这个类的重载问题
3).字节码验证(通过数据流、控制流分析验证语义的合法、逻辑性:对类型、方法体的校验)
1).保证任意时刻操作数栈的数据类型都能和字节码指令匹配上(操作数栈与本地变量表的数据类型互相匹配)
2).保证跳转指令不会跳转到方法体以为的字节码指令上
注:jdk1.6之前字节码验证都是根据程序推导这些语义的合法性,
1.6之后给方法体新增了StackMapTable属性,用来记录本地变量表和操作栈应有的状态,这样在字节码验证时就只需验证StackMapTable属性中的记录的合法性就行了
4).符号引用验证(验证常量池中的符号引用)
1).符号引用中通过字符串描述的全限定名能否找到对应的类
2).在指定类中是否存在符合方法的字段描述及简单名称所描述的方法和字段
2.准备:准备阶段是将类变量分配到方法区(仅包含static变量)
1).初始值
1.public static int value = 123
准备阶段value的初始值是0也就是int的默认值,123是在初始化"<clinit>"的时候赋值的,也就是static {}
2.特殊情况
public static final int value = 123
这种情况下类字段value编译时会生成一个ConstantValue属性,准备阶段会将ConstantValue所值的值赋给value
3.解析:解析阶段是虚拟机将常量池中的符号引用直接转化为直接引用的过程
一、引用
1).符号引用:是存在Class文件常量池中,以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info类型存在
2).直接引用:是直接指向目标的指针、相对偏移量或直接能定位到目标的句柄,所谓目标是已经布局的内存
二、解析
1).类或接口的解析
2).字段解析
3).类方法解析
4).接口方法解析
注:
1). 准备阶段是为静态变量赋默认值,然后在方法区为静态变量划分内存。
2). 解析阶段是将class文件中的符号引用,并且把编译出的直接引用存放到方法区的运行时常量池中。
3.初始化:初始化阶段是执行类构造器的过程
一、<clinit>方法执行过程中一些会影响程序运行行为的特点和细节
1).<clinit>方法是自动收集类中所有类变量的赋值动作和static{}静态语句块合并产生的
2).优先初始化父类<clinit>
3).一个类中如果没有static{}也没有静态变量,则编译器不会为这个类生成<clinit>方法
4).一个接口中不能定义static{}静态语句块,可以静态变量赋值,因此编译器会为这个接口生成<clinit>方法,与类不同的是接口不需要先执行父接口的<clinit>方法
5).多个线程下执行一个类的<clinit>初始化操作,如果一个线程的正在初始化<clinit>,其他线程则会阻塞,并且<clinit>只会初始化一次,其他线程唤醒后就不会初始化<clinit>方法(同一个加载器下,<clinit>只会初始化一次)
注:初始化操作是赋值程序员自定义的静态变量值
4.对象的创建
1.当虚拟机执行一个new指令时,会根据new指令的参数到方法区运行常量池定位一个类的符号引用,并且检查这个符号引用是否被已被加载、解析及初始化过。如果没有被加载,则必须执行相应的类加载过程(即上述1,2,3步骤)
2.类加载器检查通过后会在Java堆中为新生对象分配内存
1).分配方式:
(1). 指针碰撞(内存规整): 每创建一个对象(分配内存)指针会往空闲的内存挪一段
(2). 空闲列表(内存不规整):虚拟机维护一个列表来存储哪块内存可用
分配方式的选择取决于虚拟机的垃圾收集器,
如在使用Serial、ParNew等带压缩过程的收集器时,系统采用的分配算法是指针碰撞,
而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表算法。
2).由于Java堆属于数据共享区域,因此在分配内存时会存在并发问题,虚拟机采用了2种方式
1). CAS:自旋锁,采用失败重试的方式保证更新操作的原子性。
2). LTAB(本地线程分配缓存):为每个线程预先分配一小块内存。可以通过
内存分配完成后,虚拟机需将分配到的内存空间都初始化为零(不包括对象头)。
3.接下来会调用invokespecial指令来执行<init>方法,目的是赋值程序员自定义的变量值
5. 对象布局
1.对象在Java堆中可分为3块区域:
1):对象头:分为2部分
(1).对象自身的运行时数据:哈希码(hashCode)、Gc分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,官方称它为“Mark Word”。
(2).指向该对象的类元数据指针,虚拟机通过这个指针来确定对象是哪个类的实例。
2):实例数据:在类中定义的各种类型的数据。
3):对齐填充:无特别含义,起着占位符的作用。
6.对象访问定位
1.如果使用句柄访问的话,Java堆划分一块内存作为句柄池,reference中存储的就是对象的句柄地址。如图(图片来自<<深入理解Java虚拟机>>书籍插图):
2.如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址,如图:
举案例贯穿整个jvm启动的过程(即执行java的main方法过程)
public class Test {
private int a = 1;
private static int b = 2;
public static void main(String[] args) {
new Test();
}
}
对应的字节码
public class com.struggle.middleware.rpc.consumer.Test {
// 构造函数
public com.struggle.middleware.rpc.consumer.Test();
Code:
// 将局部变量slot 0(即this指针)的元素入栈顶
0: aload_0
// 执行<init>方法, 程序员自定义的变量值
1: invokespecial #1 // Method java/lang/Object."<init>":()V
// 将局部变量slot 0(即this指针)的元素入栈顶
4: aload_0
// 将1这个常量加载到操作数栈顶
5: iconst_1
// 为实例a变量赋值
6: putfield #2 // Field a:I
9: return
// main方法
public static void main(java.lang.String[]);
Code:
// 创建一个对象,并将其压入栈顶(此时在Java堆中分配内存)
0: new #3 // class com/struggle/middleware/rpc/consumer/Test
3: dup
4: invokespecial #4 // Method "<init>":()V
7: pop
8: return
// 类变量(静态变量)
static {};
Code:
0: iconst_2
1: putstatic #5 // Field b:I
4: return
}
一个对象加载图解: