文章目录
- 1、类的生命周期
- 2、加载阶段
- 3、查看内存中的对象:hsdb工具
- 4、连接阶段
- 5、初始化阶段
- 6、类生命周期的初始化阶段的触发条件
- 7、类生命周期的初始化阶段的跳过
- 8、练习题1
- 9、练习题2
- 10、数组的创建不会导致数组中元素的类进行初始化
- 11、final修饰的变量,等号右边不是常量,此时类生命周期中会有初始化阶段
1、类的生命周期
-
连接阶段又可分为验证、准备、解析这三步
-
使用阶段即根据类创建实例(对象):
Demo demo1 = new Demo();
Class<Demo> clazz = Demo.class;
Demo demo2 = clazz.newInstance();
- 类的卸载阶段和垃圾回收挂钩
2、加载阶段
STEP1:类加载器根据全类名通过不同的渠道以二进制流的方式获取字节码信息。
这里的不同渠道,指除了本地磁盘上的字节码文件被JVM加载外,还有程序运行过程中动态代理生成的类(内存中),以及网络传输的类(早期Applet技术)
STEP2:JVM将字节码中的信息保存到方法区中,生成一个InstanceKlass对象(c++),保存了类的所有信息
这个方法区只是一个概念层的东西,不同的JVM,甚至不同版本的HotSpot,设计方法区时都用到了不同的内存空间,比如早期的HotSpot,用的是永久带,而新版本中用的是元空间
注意上面InstanceKlass对象中保存的字节码信息,还多了一些信息,用于实现特定功能,比如虚方法表是多
态的实现基础
STEP3:JVM在堆区生成一个和上面InstanceKlass对象类似的java.lang.Class对象
这个类的Class对象,用于Java开发时获取类的信息,以及存储静态字段的数据。注意静态字段之前是存方法区,但JDK8及以后,存堆区。
为什么方法区和堆区都存字节码信息?只留方法区的InstanceKlass对象可行吗?
不可行,原因有二:
-
InstanceKlass对象是使用c++来编写的一个对象,Java代码一般不能直接去操作用c++编写的对象,所以在堆上面创建了一个java.lang.class这种用Java语言包装之后的对象,这个是可以在Java代码中操作的
-
堆区中Class对象包含的字段要少于方法区的InstanceKlass对象包含的字段,这也是出于安全考虑,很多字段是JVM底层使用的,开发者用不到,有了Class对象,JVM也就可以控制开发者访问数据的范围了
一句话,加载阶段是类加载器将类的信息加载到内存中,JVM在方法区和堆区各分配一个对象去保存类的信息。
3、查看内存中的对象:hsdb工具
JDK中带有hsdb工具,即HotSpot Debugger,位于SDK/lib/sa-jdi.jar,可查看JVM的内存信息
//JDK8下启动:
java -cp sa-jdi.jar sun.jvm.hotspot.HSDB
//和之前的java -jar不同,这个jar中的启动类有多个,需要指定某个启动类
JDK11中启动,在JDK的bin目录下cmd,然后执行:
jhsdb hsdb
写段测试代码并运行:
public class TestJvm{
//静态变量,一会儿查看其在哪个区
public static final int i = 0;
public static void main(String[] args ) throws IOException{
new TestJvm();
//不输入,让程序别退出运行
System.in.read();
}
}
打开HSDB页面:
输入PID,PID可执行jps查看:
点击Tools,查看对象直方图:
搜索刚才的TestJvm对象,双击查看对象在内存中的地址、对象的名字、类的信息:
可以看到确实有Klass对象以及Class对象,且定义的静态变量i在Class对象中,即堆区。(靠,最后一步卡的跟狗一样,截个网课里的图算了)
4、连接阶段
连接阶段又分为以下三步:
STEP1:验证
验证字节码是否满足JVM规范,校验内容包括:
- 文件格式验证(魔数)
- class文件里的主版本号是否适配当前JVM:JDK8中HotSpot虚拟机对版本号的校验源码如下:简言之就是首先判断字节码的主版本号小于JDK支持的最小值45,再判断其是否小于等于当前运行环境JDK版本(+42)。而当主版本号刚好等于当前JDK版本时,再往后判断字节码的副版本号不能超过运行环境的副版本号
- 基本信息验证,如必须有父类,super不能为空
- 验证执行指令的语义,如不能 go to跳转到不存在的一行
- 符号引用验证,例如是否访问了其他类中private的方法
STEP2:准备
该步骤是为静态变量(static)分配内存并设置初始值。
注意,此时给static变量赋的值是类型的默认值,不是源码里的值。(PS:JVM版本对应JDK8及以后)
但如果是final修饰的变量,那就是直接给源码中的值,因为其不可二次赋值,编译器认为,这个变量的值现在就可以确定了
STEP3:解析
该步骤是将常量池中的符号引用替换为直接引用。字节码中,是通过符号引用指向字节码常量池中的内容:
到这一步了,则直接使用内存中地址进行访问具体的数据。继续用前面的HSDB工具,输入PID后查看类浏览器:
可以看到此时superclass不再是之前的编号,而是内存地址。
5、初始化阶段
Java基础时,常说静态代码块在类加载时执行 ,更确切的说是类加载后的初始化阶段。初始化阶段做的事是执行静态代码块 + 为静态变量赋值。从字节码的角度来说,初始化阶段是执行clinit部分的字节码指令。
以上源码的字节码执行过程:
调整源码中静态代码块和静态变量的位置,发现字节码也跟着变:
结论:字节码的clinit方法中的执行顺序与Java中编写的顺序是一致的
6、类生命周期的初始化阶段的触发条件
- 访问一个类的静态变量或者静态方法,注意变量是final修饰且等号右边是常量时不会触发初始化(因为连接阶段就会给这种变量赋源码中的值)
- 调用Class.forName(String className)
- new一个该类的对象时
- 执行Main方法的当前类
代码演示:
也可添加JVM参数来查看加载并初始化的类:
-XX:+TraceClassLoading
同上的代码,再次运行:
同上的代码,改为final修饰且等号右边为常量:
再看Class.forName方法:
main方法执行,所在的类被加载并初始化。new对象,对应的类也被初始化:
7、类生命周期的初始化阶段的跳过
以下几种情况不进行初始化指令(clinit方法)执行:
- 无静态代码块且无静态变量赋值语句
- 有静态变量的声明,但是没有赋值语句
- 静态变量的定义使用final关键字,并且等号右边是常量,这类变量会在准备阶段直接进行初始化
8、练习题1
分析输出结果:
分析:Test1类中有main方法,其执行,首先是Test1类被加载,初始化阶段静态代码块执行,输出D,往下执行输出A,再new Test1的对象,从字节码可以看到,实例代码块被放到了init方法字节码指令里,因此,最终结果:DACBCB
注意这里虽然new对象了,但不会再触发初始化了,因为前面的main方法执行已经触发并完成初始化了。一个类被加载并初始化,一般只执行一次。
9、练习题2
子类的初始化clinit调用之前,会先调用父类的clinit初始化方法。因此,输出:2
调整,去掉new B02这一行:访问子类从其父类继承的静态变量,只会初始化父类,因此输出1
再变下,让父类A02中的静态代码块提前到静态变量前,此时输出0,原因前面已提到:字节码的clinit方法中的执行顺序与Java中编写的顺序是一致的
10、数组的创建不会导致数组中元素的类进行初始化
靠,有点混乱了,梳理下。加载是加载,初始化是初始化,加载后面不一定有初始化这步,而静态代码块的执行是在初始化这一步。
11、final修饰的变量,等号右边不是常量,此时类生命周期中会有初始化阶段
final修饰的变量如果赋值的内容需要执行指令才能得出结果,会执行clinit方法进行初始化:
====
总结: