一切的一切都还要从那个夏天,啊不,那个面试题开始说起…
例一:请问运行以下代码,输出的结果会是什么?
class Parent {
public static String s = "is Parent";
static {
System.out.println("this is my Parent static code...");
}
}
class Child extends Parent {
static {
System.out.println("this is Child static code");
}
}
public class Test {
public static void main(String[] args) {
System.out.println(Child.s);
}
}
相信大家在面试求职的道路上,或多或少都会在笔试中遇到这样或类似这样的题目,那是因为这小小题目可蕴含着不少天机,各位看官且听我慢慢道来…
首先,我们先来看下答案:
this is my Parent static code...
is Parent
不知各位看官有没有答错的,答错的自己去面壁一会小黑屋~~~
“哎,不对啊,为什么我用子类调用父类的静态变量,父类的静态代码块执行了而子类的却不执行?大家不是讲使用static修饰的会被jvm优先加载吗?莫非是我学错了???”
如果你答错了并且还能有着如此疑问,很好,很不错,小伙子骨骼着实惊奇,相信未来的某天必定也是威震一方的人物(Q_Q)。如此,就让我们来一起探索一下问题到底出在了哪里吧…
首先,我们都知道我们通过IDE运行main方法的时候,IED会帮我们生成对应的.class文件并加载到JVM虚拟机中运行,那么JVM到底是怎么加载生成的.class文件的呢?重点来了:
在java代码中,类的加载指的是将类的.class文件中的二进制数据读取到内存中,将其放在运行时数据区的方法区中,然后再内存中创建一个java.lang.Class对象用来封装类在方法区的数据结构。
类的加载过程可以分为:加载、链接、初始化、使用、卸载几个步骤,而链接又可以分为:验证、准备和解析。
那么每个步骤又都是负责做什么的呢?别慌,这就来说:
- 加载:负责查找并加载类的.class二进制文件,可以从本地、网络、归档文件等加载也可以将java源文件动态编译为.class文件加载;
- 连接:
- 验证:验证被加载类的正确性,包括文件格式验证,元数据验证,字节码验证,符号引用验证;
- 准备:为类的静态变量分配内存,并为其赋默认值(注意:是默认值!默认值!默认值!重要的事情说三遍)
- 解析:将类中常量池内的符号引用转换为直接引用;
- 初始化:为类中的静态变量赋正确的初始值;(注意:是初始值!初始值!初始值!)
- 使用;
- 卸载;
看完是不是觉得原来JVM仅仅是加载一个.class文件竟然还有这么多的门门道道啊,那是当然…这才哪到哪,瞅着吧。
现在让我们再看一段代码:
class A{
static int n = 100;
}
看官大哥们:"???,大兄弟,你闹呢?你跟我看这个?你是在侮辱我的智商?信不信马上大哥们刀了你?"
害,看官大哥们先别急,小二想说的是看过上面的JVM加载过程之后再看这段代码是不是应该明白class A在链接完成后static int n = 0
而不是100呢?因为在连接中的准备阶段,JVM只是对class A中的静态变量分配内存空间并赋默认值,只有在JVM完成类的初始化之后才会给类中的静态变量赋初始值。(画重点,要考试的)
那什么时候JVM才会完成对类的初始化呢?
JVM规定了只有当对类的主动使用才会导致类的初始化,大概分为七种情况:
- 创建类的实例(new关键字);
- 访问某个类或接口的成员变量,或者对成员变量赋值;
- 调用类中的静态方法;
- 初始化一个类的子类;
- 反射(Class.forName());
- java虚拟机标注为启动类的类,如main或test类;
- JDK1.7之后开始提供的动态语言支持:java.lang.invoke.MethodHandle实例解析结果如果是REF_getStatic、REF_putStatic、REF_invokeStatic句柄对应的类没有初始化则初始化(开发中基本很少使用到,可忽略);
除了以上的其中情况其他的都可以看作是对类的被动使用,都不会导致类的初始化。
此时的大致流程如下:
好了,现在让我们回归一开始的那道面试题,看官大哥们能明白是什么原因了吗?小二相信肯定有聪明的小伙伴明白了为什么。没错,虽然我们是通过子类名来调用的父类中的静态成员变量,但是我们实际上还是访问的父类静态成员变量,而根据JVM对类的几种主动初始化时机中就包含了调用一个类中的静态成员变量或静态方法。
现在让我们来修改一下代码
例子二:
class Parent2 {
public static String s = "is Parent2";
static {
System.out.println("this is my Parent2 static code...");
}
}
class Child2 extends Parent2 {
public static String s2 = "is Child2";
static {
System.out.println("this is Child2 static code");
}
}
public class Test2 {
public static void main(String[] args) {
System.out.println(Child2.s2);
}
}
感兴趣的看官可以自己手动测试一遍,我这里就不说答案了,至于结果为什么会如此,看官大佬们也可以在JVM规定的几种类的七种主动初始化时机中找到答案,或者可以私信小二也可在下方留言。
考虑篇幅的原因,小二这次就先说这么多,如果喜欢的话,还请各位看官大佬点个关注,小二不胜感激。如有错误,还请看官大佬及时指正,以免小二误导他人。
JVM后事如何,且听小二下回细说风云~