1.类加载与生命周期_03_类的生命周期
链接: https://pan.baidu.com/s/1ImB-pBPAZtXJMyU2D5-Nvg?pwd=6x86 提取码: 6x86–来自百度网盘超级会员v6的分享类的生命周期分为以下几个阶段。
链接: https://pan.baidu.com/s/1ImB-pBPAZtXJMyU2D5-Nvg?pwd=6x86 提取码: 6x86
–来自百度网盘超级会员v6的分享
类的生命周期分为以下几个阶段

一.加载阶段
1.第一步
类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息
程序员可以使用Java代码拓展的不同的渠道

2.第二步
类加载器在加载完类之后, Java虚拟机会将字节码中的信息保存到方法区中,生成一个采用 C++ 数据结构的InstanceKlass对象来保存类的所有信息 ,这个类java是不能直接访问的
instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内)
它的重要 field 有
-
_java_mirror:表示当前java 的类镜像
因为java不能直接访问方法区中使用C++语言实现的类的数据结构instanceKlass ,因此java提供了一个转换机制,就是在堆内存中也提供了一个instanceKlass 的简化版的数据结构对象,这个堆中的类对象和方法区中的instanceKlass对象是相互持有对方的指针的,因此它们可以相互访问
因此java程序可以通过堆中的类对象访问方法区中的instanceKlass 对象,堆中的类对象的作用就是方法区中的 instanceKlass 暴露给 java 使用
例如对于String 来说,就是 String.class
-
_super:即表示当前类的父类
-
_fields :即表示当前类的成员变量
-
_methods :即表示当前类的方法
-
_constants :即表示当前类的常量池
-
_class_loader :即表示当前类的类加载器
-
_vtable :即表示当前类的虚方法表
-
_itable :即表示当前类的接口方法表

3.第三步
Java虚拟机还会在堆中生成一份简化版的instanceKlass 对象,即_java_mirror,作用是在Java代码中去获取类的信息以及存储静态字段的数据(JDK8及之后)
之所以需要这个堆中的对象,是因为方法区中的InstanceKlass对象是C++结构的,java代码不能直接访问,因此在堆中又创建了一个简化版的java.lang.Class对象 ,这个java.lang.Class对象 对象与方法区中的InstanceKlass对象互相持有对方的指针,因此它们是可以相互访问的

对于开发者来说,只需要访问堆中的Class对象而不需要访问方法区中所有信息
这样Java虚拟机就能很好地控制开发者访问数据的范围

可以通过前面介绍的 HSDB 工具查看

二.链接阶段
链接阶段可以细分为三个阶段
1.验证阶段
验证类是否符合 JVM规范,安全性检查,这个阶段一般不需要程序员参与
列举几个常规的验证
-
文件格式验证
比如文件是否以OxCAFEBABE开头,主次版本号是否满足当前Java虚拟机版本要求

-
元信息验证
例如类必须有父类(super不能为空)
-
验证程序执行指令的语义
比如方法内的指令执行中跳转到不正确的位置
-
符号引用验证
例如是否访问了其他类中private的方法等
2.准备阶段
2.1.准备阶段作用
为 static 变量分配空间,设置默认值

准备阶段只会给静态变量赋初始值,而每一种基本数据类型和引用数据类型都有其初始值

2.2.静态变量存储位置
- static 变量在 JDK 7 之前存储于 instanceKlass对象中
- 从 JDK 7 开始存储于堆中 _java_mirror 对象中
2.3.分配空间和赋值过程
变量分配空间和赋值是两个步骤
-
分配空间在准备阶段完成
【示例】测试项目: jvm001(JDK8)
package com.longdidi.jvm.load; /** * 演示 finally对静态变量的影响 */ public class LoadTest01 { static int a; }【字节码指令】
{ # 只是声明了静态变量,没有初始化 static int a; descriptor: I flags: ACC_STATIC public com.longdidi.jvm.load.LoadTest01(); 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 6: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/longdidi/jvm/load/LoadTest01; } -
赋值在初始化阶段完成
【示例代码】测试项目: jvm001(JDK8)
package com.longdidi.jvm.load; /** * 演示 finally对静态变量的影响 */ public class LoadTest02 { static int a; static int b = 2; }【字节码指令】
{ # 声明静态变量a static int a; descriptor: I flags: ACC_STATIC # 声明静态变量b static int b; descriptor: I flags: ACC_STATIC public com.longdidi.jvm.load.LoadTest02(); 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 6: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/longdidi/jvm/load/LoadTest02; # 这里会给静态变量b赋值,这个方法就是前面说过的类的构造方法,也就是类初始化时执行的方法 static {}; descriptor: ()V flags: ACC_STATIC Code: stack=1, locals=0, args_size=0 0: iconst_2 1: putstatic #7 // Field b:I 4: return LineNumberTable: line 8: 0 }
2.4.final变量的分配
(1).final的基本类型
如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
【示例代码】测试项目: jvm001(JDK8)
package com.longdidi.jvm.load;
/**
* 演示 finally对静态变量的影响
*/
public class LoadTest03 {
static int a;
static int b = 2;
static final int c = 10;
static final String d = "Hello World";
}
【字节码指令】
{
# 声明静态变量a
static int a;
descriptor: I
flags: ACC_STATIC
# 声明静态变量b
static int b;
descriptor: I
flags: ACC_STATIC
# 声明静态变量c,这里已经给常量c赋值为10了,因为这个值在编译阶段就已经确定好了,不会再变了,因此不用等到类初始化阶段了
static final int c;
descriptor: I
flags: ACC_STATIC, ACC_FINAL
ConstantValue: int 10
# 声明静态变量d,这里已经给常量d赋值为"Hello World"了
# 因为这个值在编译阶段就已经确定好了,不会再变了,因此不用等到类初始化阶段了
static final java.lang.String d;
descriptor: Ljava/lang/String;
flags: ACC_STATIC, ACC_FINAL
ConstantValue: String Hello World
public com.longdidi.jvm.load.LoadTest03();
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 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/longdidi/jvm/load/LoadTest03;
# 在类的构造方法中只需要给静态变量b赋值
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_2
1: putstatic #7 // Field b:I
4: return
LineNumberTable:
line 8: 0
}
(2).final的引用类型
如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
【示例代码】测试项目: jvm001(JDK8)
package com.longdidi.jvm.load;
/**
* 演示 finally对静态变量的影响
*/
public class LoadTest04 {
static final Object object = new Object();
}
【字节码指令】
{
static final java.lang.Object object;
descriptor: Ljava/lang/Object;
flags: ACC_STATIC, ACC_FINAL
public com.longdidi.jvm.load.LoadTest04();
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 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/longdidi/jvm/load/LoadTest04;
# 需要在类的构造方法中完成对象的创建和初始化
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: new #2 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: putstatic #7 // Field object:Ljava/lang/Object;
10: return
LineNumberTable:
line 7: 0
}
3.解析阶段
解析阶段主要是将常量池中的符号引用替换为直接引用
-
符号引用就是在字节码文件中使用编号来访问常量池中的内容

-
直接引用不在使用编号,而是使用内存中地址进行访问具体的数据

【示例代码】测试项目: jvm001(JDK8)
package com.longdidi.jvm.load;
import java.io.IOException;
/**
* 演示 解析的含义
*/
public class LoadTest05 {
public static void main(String[] args) throws ClassNotFoundException, IOException {
ClassLoader classloader = LoadTest05.class.getClassLoader();
// loadClass 方法不会导致类的解析和初始化
// 因此类C就不会解析和初始化,类D也就不会解析和初始化
Class<?> c = classloader.loadClass("com.longdidi.jvm.load.C");
// new C();
System.in.read();
}
}
class C {
D d = new D();
}
class D {
}
运行代码,程序会在"System.in.read();"处阻塞

此时使用jps指令查找进程ID

进入 JDK 安装目录,执行如下指令打开HSDB工具
java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB

进入图形界面,选择File --> Attach to HotSpot process

输入进程ID查询

连接结果

打开Tools--Class Browser

查询类C

查询类D

查看类C的信息发现有很多与类D有关的符号



断开连接,修改源代码
package com.longdidi.jvm.load;
import java.io.IOException;
/**
* 演示 解析的含义
*/
public class LoadTest05 {
public static void main(String[] args) throws ClassNotFoundException, IOException {
//ClassLoader classloader = LoadTest05.class.getClassLoader();
// loadClass 方法不会导致类的解析和初始化
// 因此类C就不会解析和初始化,类D也就不会解析和初始化
//Class<?> c = classloader.loadClass("com.longdidi.jvm.load.C");
new C();
System.in.read();
}
}
class C {
D d = new D();
}
class D {
}
重新启动程序
再次查看类C的内存结构

三.初始化阶段
1.类的初始化
1.1.<clinit>类的构造方法
编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法<clinit>()V,这个方法就是类的构造方法
clinit方法中的执行顺序与Java中编写的顺序是一致的
<clinit>()V 方法会在类加载的初始化阶段被调用,虚拟机会保证这个类的『构造方法』的线程安全

【示例代码】测试项目: jvm001(JDK8)
package com.longdidi.jvm.structure;
public class ConstructorTest01 {
static int i = 10;
static {
i = 20;
}
static {
i = 30;
}
}
字节码指令
# 将操作数10压入到操作数栈的栈顶
0 bipush 10
# 查找常量池中的7号、名字叫做i的常量,将操作数栈顶的数值赋值给该常量
2 putstatic #7 <com/longdidi/jvm/structure/ConstructorTest01.i : I>
# 将操作数20压入到操作数栈的栈顶
5 bipush 20
# 查找常量池中的7号、名字叫做i的常量,将操作数栈顶的数值赋值给该常量
7 putstatic #7 <com/longdidi/jvm/structure/ConstructorTest01.i : I>
# 将操作数30压入到操作数栈的栈顶
10 bipush 30
# 查找常量池中的7号、名字叫做i的常量,将操作数栈顶的数值赋值给该常量,因此i的值最后是30
12 putstatic #7 <com/longdidi/jvm/structure/ConstructorTest01.i : I>
15 return

1.2.类初始化的时机
概括得说,类初始化是【懒惰的】
(1).类不初始化的情况
不会导致类初始化的情况
-
访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
因为是在类的链接阶段完成的初始化,因此不会再在初始化阶段再初始化一次
-
类对象.class 不会触发初始化
实际上是在类加载时就会生成mirror对象,不是在初始化阶段完成的
-
创建该类的数组不会触发初始化类加载器的
-
loadClass() 方法
-
Class.forName() 的参数 2 为 false 时
-
无静态代码块且无静态变量赋值语句
-
有静态变量的声明,但是没有赋值语句
-
静态变量的定义使用final关键字,这类变量会在准备阶段直接进行初始化
【示例1】测试项目: jvm001(JDK8)
访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
package com.longdidi.jvm.load;
/*
* 不初始化方式1:
* 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化,因为是在类的链接阶段完成的初始化,因此不会再在初始化阶段再初始化一次
*/
public class ClassNotInitTest01 {
public static void main(String[] args) {
System.out.println("访问静态变量a:" + AA1.a);
System.out.println("访问静态变量b:" + AA1.b);
}
}
class AA1 {
public final static String a = "你好";
public final static double b = 5.0;
static {
System.out.println("AA1 init");
}
}

【示例2】测试项目: jvm001(JDK8)
类对象.class 不会触发初始化
package com.longdidi.jvm.load;
/*
* 不初始化方式2:
* 类对象.class 不会触发初始化
*/
public class ClassNotInitTest02 {
public static void main(String[] args) throws ClassNotFoundException {
// 类对象.class 不会触发初始化
System.out.println(AA2.class);
}
}
class AA2 {
static {
System.out.println("AA2 init");
}
}

【示例3】测试项目: jvm001(JDK8)
创建该类的数组不会触发初始化类加载器的
package com.longdidi.jvm.load;
/*
* 不初始化方式3:
* 创建该类的数组不会触发初始化类加载器的
*/
public class ClassNotInitTest03 {
public static void main(String[] args) {
// 创建该类的数组不会触发初始化
System.out.println(new AA3[0]);
}
}
class AA3 {
static int a = 0;
static {
System.out.println("AA3 init");
}
}

【示例4】测试项目: jvm001(JDK8)
loadClass方法
package com.longdidi.jvm.load;
/*
* 不初始化方式4:
* loadClass方法
*/
public class ClassNotInitTest04 {
public static void main(String[] args) throws ClassNotFoundException {
// 不会初始化类 AA4
ClassLoader cl = Thread.currentThread().getContextClassLoader();
cl.loadClass("com.longdidi.jvm.load.AA4");
}
}
class AA4 {
static int a = 0;
static {
System.out.println("AA4 init");
}
}

【示例5】测试项目: jvm001(JDK8)
Class.forName() 的参数 2 为 false 时
package com.longdidi.jvm.load;
/*
* 不初始化方式5:
* Class.forName() 的参数 2 为 false 时
*/
public class ClassNotInitTest05 {
public static void main(String[] args) throws ClassNotFoundException {
// 不会初始化类 AA5
ClassLoader c2 = Thread.currentThread().getContextClassLoader();
Class.forName("com.longdidi.jvm.load.AA5", false, c2);
}
}
class AA5 {
static int a = 0;
static {
System.out.println("AA5 init");
}
}
【示例6】测试项目: jvm001(JDK8)
无静态代码块且无静态变量赋值语句
package com.longdidi.jvm.load;
public class ClassNotInitTest06 {
public static void main(String[] args) {
}
}

【示例7】测试项目: jvm001(JDK8)
有静态变量的声明,但是没有赋值语句
package com.longdidi.jvm.load;
/*
* 不初始化方式7:
* 有静态变量的声明,但是没有赋值语句
*/
public class ClassNotInitTest07 {
public static Integer a;
public static void main(String[] args) {
}
}

【示例8】测试项目: jvm001(JDK8)
静态变量的定义使用final关键字,这类变量会在准备阶段直接进行初始化
package com.longdidi.jvm.load;
/*
* 不初始化方式8:
* 静态变量的定义使用final关键字,这类变量会在准备阶段直接进行初始化
*/
public class ClassNotInitTest08 {
public static final int a = 1;
public static void main(String[] args) {
}
}

(2).类初始化的情况
导致初始化的情况
-
访问这个类的静态变量或静态方法时
-
main 方法所在的类,总会被首先初始化
-
调用Class.forName(String className)方法
-
new 会导致初始化
-
子类初始化,如果父类还没初始化,会引发父类初始化
-
子类访问父类的静态变量,只会触发父类的初始化
【示例1】测试项目: jvm001(JDK8)
访问静态变量
package com.longdidi.jvm.load;
/**
* 初始化1 - 访问静态变量
*/
public class ClassInitTest01 {
public static void main(String[] args) {
// 访问一个类的静态变量时会导致该类初始化
int i = Demo2.i;
System.out.println(i);
}
}
class Demo2 {
static {
System.out.println("初始化了...");
}
public static int i = 0;
}

【示例2】测试项目: jvm001(JDK8)
main方法所在的类
package com.longdidi.jvm.load;
/**
* 初始化2 - main方法所在的类
*/
public class ClassInitTest02 {
static {
System.out.println("初始化了...");
}
public static void main(String[] args) {
System.out.printf("main方法所在的类初始化");
}
}

【示例3】测试项目: jvm001(JDK8)
调用Class.forName(“”)方法
package com.longdidi.jvm.load;
/**
* 初始化3 - Class.forName
*/
public class ClassInitTest03 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> clazz = Class.forName("com.longdidi.jvm.load.Demo3");
}
}
class Demo3 {
static {
System.out.println("初始化了...");
}
}

【示例4】测试项目: jvm001(JDK8)
new会导致初始化
package com.longdidi.jvm.load;
/**
* 初始化4 - new初始化
*/
public class ClassInitTest04 {
static {
System.out.println("初始化了...");
}
public static void main(String[] args) throws ClassNotFoundException {
ClassInitTest04 cit = new ClassInitTest04();
}
}

【示例5】测试项目: jvm001(JDK8)
子类初始化,如果父类还没初始化,会引发父类初始化
package com.longdidi.jvm.load;
/**
* 初始化5 - 子类初始化,如果父类还没初始化,会引发父类初始化
*/
public class ClassInitTest05 {
public static void main(String[] args) throws ClassNotFoundException {
// 子类初始化,如果父类还没初始化,会引发
System.out.println(B2.c);
}
}
class A2 {
static int a = 0;
static {
System.out.println("a2 init");
}
}
class B2 extends A2 {
final static double b = 5.0;
static boolean c = false;
static {
System.out.println("b2 init");
}
}

【示例6】测试项目: jvm001(JDK8)
子类访问父类的静态变量,只会触发父类的初始化
package com.longdidi.jvm.load;
/**
* 初始化6 - 子类访问父类的静态变量,只会触发父类的初始化
*/
public class ClassInitTest06 {
public static void main(String[] args) throws ClassNotFoundException {
// 子类访问父类静态变量,只触发父类初始化
System.out.println(B3.a);
}
}
class A3 {
static int a = 0;
static {
System.out.println("a2 init");
}
}
class B3 extends A3 {
final static double b = 5.0;
static boolean c = false;
static {
System.out.println("b2 init");
}
}

1.3.初始化父类
- 子类的初始化clinit调用之前,会先调用父类的clinit初始化方法
- 直接访问父类的静态变量,不会触发子类的初始化
【示例1】测试项目: jvm001(JDK8)
子类的初始化clinit调用之前,会先调用父类的clinit初始化方法
package com.longdidi.jvm.load;
/*子类的初始化clinit调用之前,会先调用父类的clinit初始化方法*/
public class ParentInit01 {
public static void main(String[] args) {
new PB01();
//System.out.println(PB01.a);
}
}
class PA01 {
static int a = 0;
static {
a = 1;
System.out.println("PA01 static init");
}
}
class PB01 extends PA01 {
static {
a = 2;
System.out.println("PB01 static init");
}
}

【示例2】测试项目: jvm001(JDK8)
直接访问父类的静态变量,不会触发子类的初始化
package com.longdidi.jvm.load;
/*直接访问父类的静态变量,不会触发子类的初始化*/
public class ParentInit02 {
public static void main(String[] args) {
System.out.println(PB02.a);
}
}
class PA02 {
static int a = 0;
static {
a = 1;
System.out.println("PA02 static init");
}
}
class PB02 extends PA02 {
static {
a = 2;
System.out.println("PB02 static init");
}
}

2.对象的初始化
编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法
原始构造方法内的代码总是在合并后的构造方法的最后面
【示例代码】测试项目: jvm001(JDK8)
package com.longdidi.jvm.structure;
public class ConstructorTest02 {
private String a = "s1";
{
b = 20;
}
private int b = 10;
{
a = "s2";
}
public ConstructorTest02(String a, int b) {
this.a = a;
this.b = b;
}
public static void main(String[] args) {
ConstructorTest02 d = new ConstructorTest02("s3", 30);
System.out.println(d.a);
System.out.println(d.b);
}
}
字节码指令
# 将局部变量表的0号槽位上的this引用压入操作数栈的栈顶
0 aload_0
# 调用父类的构造方法初始化
1 invokespecial #1 <java/lang/Object.<init> : ()V>
# 将局部变量表的0号槽位上的this引用压入操作数栈的栈顶
4 aload_0
# 常量池中的2号常量,也就是s1常量压入操作数栈的栈顶
5 ldc #2 <s1>
# putfield指令用栈顶的值为指定的类的实例域赋值
# 也就是将s1的值赋值给3号常量a,也就是this.a=s1
7 putfield #3 <com/longdidi/jvm/structure/ConstructorTest02.a : Ljava/lang/String;>
# 将局部变量表的0号槽位上的this引用压入操作数栈的栈顶
10 aload_0
# 在操作数栈栈顶压入一个数字20
11 bipush 20
# 将20的值赋值给4号常量b,也就是this.b=20
13 putfield #4 <com/longdidi/jvm/structure/ConstructorTest02.b : I>
# 将局部变量表的0号槽位上的this引用压入操作数栈的栈顶
16 aload_0
# 在操作数栈栈顶压入一个数字10
17 bipush 10
# 将10的值赋值给4号常量b,也就是this.b=10
19 putfield #4 <com/longdidi/jvm/structure/ConstructorTest02.b : I>
# 将局部变量表的0号槽位上的this引用压入操作数栈的栈顶
22 aload_0
# 常量池中的5号常量,也就是s2常量压入操作数栈的栈顶
23 ldc #5 <s2>
# 将栈顶的s2赋值给3号常量a,也就是this.a=s2
25 putfield #3 <com/longdidi/jvm/structure/ConstructorTest02.a : Ljava/lang/String;>
#######################################从这里开始是原有的构造方法##################################333
# 将局部变量表的0号槽位上的this引用压入操作数栈的栈顶
28 aload_0
# 这里是构造方法中的第一个参数s3
29 aload_1
# this.a=s3
30 putfield #3 <com/longdidi/jvm/structure/ConstructorTest02.a : Ljava/lang/String;>
# 将局部变量表的0号槽位上的this引用压入操作数栈的栈顶
33 aload_0
# 这里是构造方法的第二个参数30
34 iload_2
# 也就是this.b=30
35 putfield #4 <com/longdidi/jvm/structure/ConstructorTest02.b : I>
38 return

整体流程图

更多推荐



所有评论(0)