概述
new一个对象包括:类加载(未加载时)、为对象分配内存、内存空间初始化、对象头设置等步骤。
1. 如何判断类是否被加载
--示例代码
public class Demo {
public void tests(String a){
User user = new User();
}
public class User {
String name;
}
}
tests方法本地变量表
字节码常量池
- tests方法编译后在tests方法的本地变量表中有一个
user
变量,此时user
还只是一个符号引用,对应字节码常量池中全限定名为Lzzzliu/JVM/Demo$User; - 当虚拟机执行到
User user = new User();
时通过user的符号引用去运行时常量池表中找User.class,没有找到的话说明User尚未被加载;
2. 类生命周期
类生命周期.png- 类生命周期七个阶段中加载、验证、准备、初始化、卸载这五个阶段顺序时确定的;解析和初始化阶段由于动态绑定特性有可能先初始化,在运行时再进行解析;
3. 加载
3.1 通过类的全限定名在类加载器路径中获取此类的二进制字节流
- 例如
User user = new User();
就是在ClassPath路径下找User.class文件; - 也可以自定义加载器从其他途径加载.class文件(二进制字节流),例如:动态代理、jsp、数据库等
3.2 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 把二进制字节流中类文件结构,包括魔数与Class文件的版本、常量池、访问标志、类索引/父类索引/接口索引集合、字段表集合、方法表集合、属性表集合,转化为方法区的运行时数据结构
- 其中二进制字节流中常量池中字面量和符号引用转化成方法区的运行时常量池结构
注意:此时只是转化结构,尚未分配内存
例如:
Classfile /D:/privatefile/LeetCode/src/zzzliu/JVM/Demo.class
Last modified 2020-11-15; size 559 bytes
MD5 checksum a962546105384ba27fde2199fdba1a74
Compiled from "Demo.java"
public class zzzliu.JVM.Demo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#25 // java/lang/Object."<init>":()V
#2 = Class #26 // zzzliu/JVM/Demo$User
#3 = Methodref #2.#27 // zzzliu/JVM/Demo$User."<init>":(Lzzzliu/JVM/Demo;)V
#4 = Class #28 // zzzliu/JVM/Demo
#5 = Class #29 // java/lang/Object
#6 = Utf8 User
#7 = Utf8 InnerClasses
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Lzzzliu/JVM/Demo;
#15 = Utf8 tests
#16 = Utf8 (Ljava/lang/String;)V
#17 = Utf8 a
#18 = Utf8 Ljava/lang/String;
#19 = Utf8 b
#20 = Utf8 I
#21 = Utf8 user
#22 = Utf8 Lzzzliu/JVM/Demo$User;
#23 = Utf8 SourceFile
#24 = Utf8 Demo.java
#25 = NameAndType #8:#9 // "<init>":()V
#26 = Utf8 zzzliu/JVM/Demo$User
#27 = NameAndType #8:#30 // "<init>":(Lzzzliu/JVM/Demo;)V
#28 = Utf8 zzzliu/JVM/Demo
#29 = Utf8 java/lang/Object
#30 = Utf8 (Lzzzliu/JVM/Demo;)V
{
public zzzliu.JVM.Demo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lzzzliu/JVM/Demo;
public void tests(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=3, locals=4, args_size=2
0: iconst_1
1: istore_2
2: new #2 // class zzzliu/JVM/Demo$User
5: dup
6: aload_0
7: invokespecial #3 // Method zzzliu/JVM/Demo$User."<init>":(Lzzzliu/JVM/Demo;)V
10: astore_3
11: return
LineNumberTable:
line 6: 0
line 8: 2
line 9: 11
LocalVariableTable:
Start Length Slot Name Signature
0 12 0 this Lzzzliu/JVM/Demo;
0 12 1 a Ljava/lang/String;
2 10 2 b I
11 1 3 user Lzzzliu/JVM/Demo$User;
}
SourceFile: "Demo.java"
InnerClasses:
public #6= #2 of #4; //User=class zzzliu/JVM/Demo$User of class zzzliu/JVM/Demo
3.3 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
- 在方法区中分配内存存储生成的User.class对象,后续通过该class对象获取这个类的信息
4. 验证
- 文件格式验证:验证字节流是否符合Class文件格式规范,并且能被当前版本虚拟机处理;
例如
是否以魔术0xCAFFBABE开头等 - 元数据验证:对字节码描述的信息进行语义分析,保证其描述的信息符合《java语言规范的要求》;
例如
idea提醒的语法错误等 - 字节码验证:主要目的是通过数据流分析和控制流分析,确定程序语义是合法的,保证方法在运行时不会做出危害虚拟机安全的行为;
例如
保证跳转指令不会跳转到方法体以外的字节码指令上 - 符号引用验证:该校验发生在解析阶段符号引用转化成直接应用时候,校验该类是否缺少或被进制访问它依赖的某些外部类、方法、字段等资源;
例如
符号引用中通过字符串描述的全限定名是否能找到对应的类;
5. 准备
- 准备阶段仅仅为
static
的变量分配内存并且设置该类变量的初始值即零值,这里不包含用final
修饰的static
,final
的变量在准备阶段就会被初始化为ConstantValue属性所指定的初始值; - 类变量会分配在方法区中,而实例变量会随着对象一起分配到Java堆;
--示例代码
private static final int value = 100;
--编译时javac就会为value生成ConstantValue属性
private static final int value;
descriptor: I
flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL
ConstantValue: int 100
6. 解析
- 解析阶段是虚拟机将
常量池内
的符号引用替换为直接应用的过程; - 解析阶段并没有规定具体的执行时间,只要求在执行getfield/invokedynamic/invokespecial/new等指令需要使用直接引用之前解析完成就可以;
- 除invokedynamic指令外其余静态指令第一次解析之后结果可以缓存在常量池中,避免多次解析;
-
invokedynamic
指令为了支持动态调用,必须等到程序实际运行到这条指令时才解析;
注意:下面解析过程不针对invokedynamic指令相关的解析
6.1 类或接口解析
--常量池中类的符号引用
#38 = Utf8 zzzliu/JVM/Demo
#39 = Utf8 java/lang/Object
- 将符号应用传递给当前类的类加载器去加载该符号引用的类;加载过程中由于元数据验证、字节码验证的需要,有可能触发父类或接口的加载;
- 第一次解析后结果缓存在常量池中,之后直接获取;
- 解析完成之前对符号引用进行验证,确认是否具备该类的访问权限;
6.1 字段解析
private User user;
private zzzliu.JVM.Demo$User user;
descriptor: Lzzzliu/JVM/Demo$User;
flags: ACC_PRIVATE
先解析字段所属类或接口为C,然后按如下顺序解析字段
- C本身就包含了名称和字段描述都与目标匹配的字段,则返回这个字段的直接引用;
- 否则,如果C继承了接口,按照从下往上的继承关系往上搜索,查找到则直接返回;
- 否则,按照C的继承关系往上搜索,查找到则直接返回;
- 否则,都未查找到则抛出
java.lang.NoSuchFieldError
6.1 方法解析
同字段解析类似,先解析方法所属类或接口为C,然后按如下顺序解析字段
- C本身就包含了名称和字段描述都与目标匹配的方法,则返回这个方法的直接引用;
- 否则,按照C的继承关系往上搜索,查找到则直接返回;
- 否则,如果C继承了接口,按照从下往上的继承关系往上搜索,查找到则说明C是个接口,抛出
java.lang.AbstractMethodError
- 否则,都未查找到则抛出
java.lang.NoSuchMethodError
最后查找过程成功后检查权限,如果不具备访问权限,抛出java.lang.IllegalAccessError
7. 初始化
初始化阶段就是执行类构造器<clinit>()
方法的过程,<clinit>()是Javac编译器自动生成的;
- <clinit>()方法是由编译器收集类中所有static类变量的赋值动作和静态语句块的语句生成的,收集的顺序是由源文件定义顺序决定的;
-
<clinit>()
方法与实例构造器<init>
方法不同,这里面不用显示的调用父类的<clinit>方法,父类的<clinit>方法会自动先执行于子类的<clinit>方法。即父类定义的静态语句块和静态字段都要优先子类的变量赋值操作 - 虚拟机保证<clinit>()方法在多线程环境中能够被正确的加锁同步,多个线程并发执行<clinit>()时会阻塞;
--------over---------
- 参考《深入理解Java虚拟机》
网友评论