1 首先复习下主动使用的7中情况
- 主动使用
- 实例化一个类对象
- 访问类/接口的静态变量,或者对其进行赋值操作
- 调用类/接口的静态方法
- 反射–Class.forName(“com.test.Test”)
- 实例化该类的子类
- JVM标记为启动类的类(包含main方法的类)
- JDK1.7 后新增对动态语言的支持,java.lang.invoke.MethodHandle实例解析结果为REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则需要初始化。
2 访问类中的静态变量
看第一段代码
package com.cj.jvm.classloader;
public class MyTest1 {
public static void main(String[] args) {
System.out.println(MyChild1.str2);
}
}
class MyParent1{
public static String str = "hello world";
static {
System.out.println("MyParent1 static block");
}
}
class MyChild1 extends MyParent1{
public static String str2 = "welcome";
static {
System.out.println("MyChild1 static block");
}
}
情况1: main函数中打印 MyChlid1.str
结果:
分析: 符合情况2。访问类/接口的静态变量,或者对其进行赋值操作,该类会进行初始化操作。由于MyChild的str变量实际是继承于MyParent1。所以对MyParent1类进行初始化。
情况2: main函数中打印 MyChlid1.str2
结果:
分析: 符合情况2和5。首先是访问str2,这是MyChild1中定义的静态变量,访问时需要对MyChild1进行初始化。又由于MyChild1是MyParent1的子类,对MyChild1进行初始化之前需要先让MyParent1初始化。
结论: 对一个静态字段来说,只有直接定义了该字段的类才会被初始化,当一哥类在初始化时,要求其父类全部已经初始化完毕了
3 常量–编译期可知与编译期不可知
看另一个例子
package com.cj.jvm.classloader;
public class MyTest2 {
public static void main(String[] args) {
System.out.println(MyChild2.str2);
}
}
class MyParent2{
public static final String str = "hello world";
static {
System.out.println("MyParent1 static block");
}
}
class MyChild2 extends MyParent2{
public static final String str2 = "welcome";
static {
System.out.println("MyChild1 static block");
}
}
分别在main函数中,打印MyChild2.str和MyChild2.str2,结果如下
分析:由于str和str2是字面常量,即在编译的时候已经将他们的信息放在了字节码中。但是这有三个类,放在那个类的字节码中呢?
记住下面这句话:常量在编译阶段,会存入到,调用这个常量的方法,所在的类的,常量池中。
所以在本例子中,str(或者str2)在第一次编译后,存到了MyTest2的常量池中。
查看MyTest2的字节码文件,#4号位置就是了
上面说的是编译期可知的常量,下面说下,运行时可知的常量,看例子
package com.cj.jvm.classloader;
import java.util.UUID;
public class MyTest3 {
public static void main(String[] args) {
System.out.println(MyParent3.str);
}
}
class MyParent3{
public static final String str = UUID.randomUUID().toString();
static {
System.out.println("MyParent3 static block");
}
}
输出结果:
分析:依旧是常量,只不过是变成了运行时才可知的常量,这就导致了该常量所在的类被初始化。
结论: 当一个常量的值是在编译期无法确定的,那个它的值就不会被放到调用类的常量池中,在程序运行时,会导致主动使用这个常量所在的类,然后导致该类的初始化
4 类的数组是否会导致类的初始化? – 不会
看个例子
package com.cj.jvm.classloader;
public class MyTest4 {
public static void main(String[] args) {
MyParent4[] myParent4s = new MyParent4[1];
System.out.println(myParent4s.getClass());
System.out.println(myParent4s.getClass().getSuperclass());
MyChild4[] myChild4s = new MyChild4[1];
System.out.println(myChild4s.getClass());
System.out.println(myChild4s.getClass().getSuperclass());
int[] ints = new int[1];
System.out.println(ints.getClass());
System.out.println(ints.getClass().getSuperclass());
}
}
class MyParent4{
static {
System.out.println("MyParent4 static block");
}
}
class MyChild4 extends MyParent4{
static {
System.out.println("MyChild static block");
}
}
运行结果:
分析:是的,对于数组实例来说,其类型是有JVM在运行期间动态生成的。表示为[Lcom.cj.jvm.classloader.MyParent4这种形式。其父类是Object。
(这是为什么呢?书上这样写的 emm)
5 类和接口初始化的区别
这个暂时没有比较好的代码例子,只好直接上结论了。
结论: 当一个接口在初始化时,并不要求其父接口完成初始化;只有真正使用到父接口时(如引用接口中所定义的常量时),才会初始化。
//2020-1-31 更新
看例子,
package com.cj.jvm.classloader;
// 接口初始化相关
public class MyTest5 {
public static void main(String[] args) {
System.out.println(MyParent5.str);
}
}
interface MyGranParent5{
Thread t = new Thread(){
{
System.out.println("MyGranParent is here ");
}
};
}
class MyParent5 implements MyGranParent5{
static {
System.out.println("MyParent5 is here");
}
public static String str = "gggg";
}
结果:
分析:初始化一个类时,不会是初始化其实现的接口,
修改下代码,打印t变量
public class MyTest5 {
public static void main(String[] args) {
System.out.println(MyParent5.t);
}
}
结果:
只初始化了MyGranparent接口。
验证,子接口初始化,不会引起父接口的初始化,
package com.cj.jvm.classloader;
// 接口初始化相关
import java.util.UUID;
public class MyTest5 {
public static void main(String[] args) {
System.out.println(MyChild5.id);
}
}
interface MyGranParent5{
Thread t = new Thread(){
{
System.out.println("MyGranParent is here ");
}
};
}
interface MyChild5 extends MyGranParent5{
Thread t1 = new Thread(){
{
System.out.println("MyChild5 is here ");
}
};
public static final String id = UUID.randomUUID().toString();
}
结果:
分析:
MyChild5的父接口MyGranparent5并没有初始化
6 初始化的顺序
看代码:
package com.cj.jvm.classloader;
public class MyTest6 {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println("count1 = "+ Singleton.count1);
System.out.println("count2 = "+ Singleton.count2);
}
}
class Singleton{
public static int count1;
public static int count2=0;
private static Singleton singleton = new Singleton();
private Singleton(){
count1++;
count2++;
}
public static Singleton getInstance(){
return singleton;
}
}
结果是:
分析:显然,得出结果,count1和count2在连接阶段的准备过程中被初始化为默认值0。在构造函数中都增加1,所以结果都为1。
我们修改下几个静态变量的和构造函数的顺序,
package com.cj.jvm.classloader;
public class MyTest6 {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println("count1 = "+ Singleton.count1);
System.out.println("count2 = "+ Singleton.count2);
}
}
class Singleton{
public static int count1;
private static Singleton singleton = new Singleton();
private Singleton() {
count1++;
count2++;
}
public static int count2=0;
public static Singleton getInstance(){
return singleton;
}
}
结果:
分析:
调整顺序之后,结果发生了改变,说明,程序执行的顺序应该是:
准备阶段,默认初始化为0–》构造函数对变量进行修改–》类初始化,给变量赋予正确的值。
再修改下程序,
package com.cj.jvm.classloader;
public class MyTest6 {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println("count1 = "+ Singleton.count1);
System.out.println("count2 = "+ Singleton.count2);
}
}
class Singleton{
private static Singleton singleton = new Singleton();
private Singleton() {
System.out.println(count1);
System.out.println(count1);
count1++;
count2++;
System.out.println(count1);
System.out.println(count1);
}
public static int count2=0;
public static int count1=2;
public static Singleton getInstance(){
return singleton;
}
}
结果:
7 反射导致的类初始化
看例子(20-1-31更新)
package com.cj.jvm.classloader;
class CJ{
static {
System.out.println("class CJ static block");
}
}
public class MyTest8 {
public static void main(String[] args) throws ClassNotFoundException {
//获取系统类加载器
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
//使用系统类加载器去加载CJ类
Class<?> clazz = classLoader.loadClass("com.cj.jvm.classloader.CJ");
System.out.println(clazz);
System.out.println("--------------");
Class<?> clazz2 = Class.forName("com.cj.jvm.classloader.CJ");
System.out.println(clazz2);
}
}
结果:
分析:系统类加载器加载CJ类,不会导致CJ的初始化。反射会。