这几天学习jvm一些知识,刚好在牛客网上刷题时遇到一道关于jvm底层编译顺序的题目,看到问题也都是就java代码而言讲的,也没有剖析原理。我趁着所学jvm还热着,赶紧分享一下我的拙见,不然过不了多久估计我也不会了。
在牛客看到题目是这样的:
public class Test {
static int x=10;
static {x+=5;}
public static void main(String[] args)
{
System.out.println("x="+x);
}
static{x/=3;};
}
上面代码能否运行,如果能够运行,那最后输出的结果是什么?
这里你先思考一下,再继续阅读
ps:防止大家不信我特意用idea编译运行一遍,把截图奉上以示自己正确性。
这一串代码是可以运行的,结果为 5,和你思考的一样吗?其实这不单纯是 x=10; 然后x+=5;x值变成15,在最后x/=3;x的值变成5那么简单的哦。
这里我先不分析原因,直接从jvm加载开始讲解,等到讲完大家也就知道原因了。
讲类加载过程其实还需要了解class类怎么在jvm中的存储的。
如果说class是java的类(java代码)
那么InstanceKlass就是jave类在jvm中的存在形式(c++代码)
首先大家要清楚一点就是jvm加载是懒加载(lazy loading)。懒加载通俗讲就是偷懒,用到谁加载谁,用不到就不加载。
类的生命周期是由7个阶段组成,但是类的加载说的是前5个阶段
大致讲解5个阶段功能:
这里补充一点:加载阶段主动使用子类父类也需要加载
我们看一下上面题目的字节码:
因为这一题,main方法肯定使用时在类加载过程的后面,所以到初始化的时候就会按顺序把static int x = 10; static {x+=5;} static{x/=3}执行。但其实这个题目比较简单,我们看几个复杂的例子,让大家彻底搞懂这块知识。
正如标题所说,我们直接来看代码
大家可以先看代码,然后自己去思考输出,然后再看我的解析会好点。
public class Test_1 {
public static void main(String[] args) {
System.out.printf(Test_1_B.str);
while (true);
}
}
class Test_1_A {
public static String str = "A str";
static {
System.out.println("A Static Block");
}
}
class Test_1_B extends Test_1_A {
static {
System.out.println("B Static Block");
}
}
结果是:
A Static Block
A str
差异吗?是不是很惊奇,待我慢慢讲来。
附加 证明static String str = “A str”;其实它是存储在Test_1_A 的InstanceMirrorKlass中的。这个证明我们需要用到HSDB(不懂得百度一个神器)
使用步骤就是:
先jps-l查看进程号
使用hsdb通过进程号查看进程的内存地址
通过内存地址搜索到存储的内容如下图(Test_1_A的jvm存储的信息,看到str被存在A中):
这里B就不演示搜索了,其实B中InstanceMirrorKlass是什么都没有的,也就能证明str这个静态String被存到A中。
public class Test_2 {
public static void main(String[] args) {
System.out.printf(Test_2_B.str);
}
}
class Test_2_A {
static {
System.out.println("A Static Block");
}
}
class Test_2_B extends Test_2_A {
public static String str = "B str";
static {
System.out.println("B Static Block");
}
}
结果是:
A Static Block
B Static Block
B str
踩了第一个坑之后,第二个就好点了吧。这个如果做错就是上面有一句话没有注意:加载阶段主动使用子类父类也需要加载,那就是你调用子类,父类会间接调用。第一题告诉我们调用父类,却不会调用子类。
仔细审视,这和第二题是有区别的。区别在于final。
public class Test_3{
public static void main(String[] args) {
System.out.println(Test_3_A.str);
}
}
class Test_3_A {
public static final String str = "A Str";
static {
System.out.println("Test_6_A Static Block");
}
}
准备:为静态变量分配内存,赋初值(ps:实例对象new时直接赋值在准备阶段没有赋初值一说。如果被final修饰,在编译的时候会给属性添加ConstantValue属性,准备阶段直接完成赋值,即没有赋初值这一步)
上面也有这句话,你理解其中含义没?其实final修饰的,在编译过程中将str=A Str写入Test_6_A的常量池中,所以不需要调用Test_6_A,直接在常量池中就能找到(真是有够懒加载的对不···)
可以使用javap -vervose classpath 查询常量池
三个例子够不够?不够那再来,码字不易记得多多点赞分享
public class Test_4 {
public static void main(String[] args) {
System.out.println(Test_4_A.uuid);
}
}
class Test_4_A {
public static final String uuid = UUID.randomUUID().toString();
static {
System.out.println("Test_4_A Static Block");
}
}
结果:
Test_4_A Static Block
38b41380-7111-4ce5-8079-5a2576fd4282
这个uuid虽说被finla修饰,但是右边是需要动态创建才能生成的,所以结果才会这样。
public class Test_5 {
public static void main(String[] args) {
Test_5_A obj = Test_5_A.getInstance();
System.out.println(Test_5_A.val1);
System.out.println(Test_5_A.val2);
}
}
class Test_5_A {
public static int val1;
public static int val2 = 1;
public static Test_5_A instance = new Test_5_A();
Test_5_A() {
val1++;
val2++;
}
public static Test_5_A getInstance() {
return instance;
}
}
结果是:1 2
这里我们一步一步分析,在前面的初始化阶段中:clinit方法中语句的先后顺序与代码的编写顺序相关,故val1=0;val2=1;
public static Test_5_A instance = new Test_5_A();这个会调用构造函数Test_5_A(),导致val1++,和val2++;
所以最后结果是1和2
public class Test_6 {
public static void main(String[] args) {
Test_6_A obj = Test_6_A.getInstance();
System.out.println(Test_6_A.val1);
System.out.println(Test_6_A.val2);
}
}
class Test_6_A {
public static int val1;
public static Test_6_A instance = new Test_6_A();
Test_6_A() {
val1++;
val2++;
}
public static int val2 = 1;
public static Test_6_A getInstance() {
return instance;
}
}
结果是:1 1
还是clinit方法中语句的先后顺序与代码的编写顺序相关,这次初始化顺序是val1=0;然后初始化public static Test_6_A instance = new Test_6_A();调用Test_6_A(),val1++,val2++;最后再次初始化val2=1;所以这里因为顺序关系导致覆盖。
感谢大家的观看,有什么错误之处欢迎大家指出来。