Java 杂谈JVM · Java虚拟机原理 · JVM上语言·框架· 生态系统

深入理解JVM(一)类加载器部分:初始化规则、单例对象分析

2019-06-03  本文已影响0人  利伊奥克儿

单例对象

eg1:
public class MyTest6 {

    public static void main(String[] args) {
        Singleton singleton=Singleton.getInstance();
        System.out.println("Singleton3 counter1="+Singleton.counter1);
        System.out.println("Singleton3 counter2="+Singleton.counter2);
    }
}

class Singleton{
    public static int counter1=1;

    private static Singleton singleton = new Singleton();
    private Singleton(){
        System.out.println("Singleton1 counter1="+counter1);
        System.out.println("Singleton1 counter2="+counter2);
        counter1++;
        counter2++;
        System.out.println("Singleton2 counter1="+counter1);
        System.out.println("Singleton2 counter2="+counter2);
    }

    public static int counter2=2;
    public static  Singleton getInstance(){
        return  singleton;
    }

}
//输出:
Singleton1 counter1=1
Singleton1 counter2=0
Singleton2 counter1=2
Singleton2 counter2=1
Singleton3 counter1=2
Singleton3 counter2=2

过程分析:首先 Singleton singleton=Singleton.getInstance(); 调用了静态方法,是对Singleton的主动使用,会导致Singleton的初始化

在类的加载、连接、初始化中,在连接阶段有一个准备阶段,在此阶段会为静态变量赋一个默认值(并非指定的值),所以在连接阶段上述的静态变量的默认值如下

连接阶段的默认值:
public static int counter1=0;
private static Singleton singleton = null;
public static int counter2=0;

在初始化步骤,JVM会为静态变量赋指定的值,此时静态变量赋值顺序与定义的顺序一致,即赋值情况如下:

public static int counter1=1;
private static Singleton singleton = new Singleton();
//此时会调用Singleton的私有构造方法 private Singleton()
//在私有构造方法中,在执行++之前
//counter1 已经在前面被赋值为1(因为counter1 定义在singleton之前)
//此时counter2仍然是0(因为counter2 定义在singleton之后前,并未赋指定值)  
//所以第一组输出为:
> Singleton1 counter1=1
> Singleton1 counter2=0
//在执行++后,
counter1=2
counter2=1
//所以第二组输出为:
> Singleton2 counter1=2
> Singleton2 counter2=1
//经过上述步骤singleton赋值完毕,接下来对counter2赋值
//赋值后
 public static int counter2=2;
//经过上述步骤,初始化步骤完成,初始化后各个静态变量的值为
counter1=2;
singleton = Singleton的实例对象;
counter2=2;
//所以第三组输出为:
> Singleton3 counter1=2
> Singleton3 counter2=2

类加载到实例化对象过程

加载

类加载的最终产品是位于内存中的Class对象

Class对象封装了类在方法去内的数据结构,并向Java程序员提供了访问方法区内的数据结构借口

  1. Java虚拟机自带的类加载器

根类加载器(Bootstrap)

扩展类加载器(Extension)

系统(应用)类加载器(Sysytem)

  1. 用户自定义的类加载器

java.lang.ClassLoader的子类

用户可以定制类的加载方式

JVM规范允许类加载器在预料到某个类将要被使用是预先加载它,如果在预先加载过程中遇到.class文件缺失或者存在错误,类加载器必须在程序首次主动使用该类时才报错(LinkageError错误)

如果这个类一直没有被主动使用,那么类加载器就不会报告错误。

连接

连接:将已经读入内存的类的二进制数据合并到虚拟机的运行时环境中去,主要包含三个步骤:

验证内容:
类文件的结构检查
语义检查
字节码验证
二进制兼容性的验证

初始化

静态变量的声明语句、静态代码块都被看作类的初始化语句,Java虚拟机会按照初始化语句在类文件中的先后顺序来执行它们。

如果这个类还没有被加载和连接,那么就先进行加载和连接;

假如这个类存在直接父类,并且这个父类还没有被初始化,那就先初始化直接父类;

加入类中存在初始化语句,那就依次执行这些初始化语句

类初始化时机

当Java虚拟机初始化一个类时,要求它所有的父类都已经被初始化,但是这条规则不适用于接口

一个父接口不会因为它的子接口或者它的实现类的初始化而初始化,只有当程序首次使用定接口特的静态变量时,才会导致该接口的初始化。(特定的变量指的是的变异期间不确定,运行期间才确定的变量)

eg1:
public class MyTest5 {
    public static void main(String[] args) {
        System.out.println(MyChild5.b);
    }
}

interface MyParent5 {
    public static Thread thread = new Thread() {//匿名内部类
        {
            System.out.println("MyParent5 block");
        }
    };
}

class MyChild5 implements MyParent5 {
    public static int b = 6;
{
      System.out.println("MyChild5 static block");
    }
}
//输出结果:
[Loaded com.aaa.test.MyTest5 from file:/E:/IdeaProjects/AAAE2E2/target/scala-2.11/classes/]
[Loaded com.aaa.test.MyParent5 from file:/E:/IdeaProjects/AAAE2E2/target/scala-2.11/classes/]
[Loaded com.aaa.test.MyChild5 from file:/E:/IdeaProjects/AAAE2E2/target/scala-2.11/classes/]
[Loaded com.aaa.test.MyParent5$1 from file:/E:/IdeaProjects/AAAE2E2/target/scala-2.11/classes/]
MyChild5 static block
6

//注:MyParent5$1 为MyParent5中的匿名内部类

从上面的结果可以看出,当实现类初始化的时候,虽然父接口被加载,但是却没有初始化

eg2:
public class MyTest5 {
    public static void main(String[] args) {
        System.out.println(MyChild5.c);
    }
}

interface MyParent5 {
    public static Thread thread = new Thread() {//匿名内部类
        {
            System.out.println("MyParent5 block");
        }
    };
}

interface MyChild5 extends MyParent5 {
    public static int b = 6;
    public static String c = UUID.randomUUID().toString();
    public static Thread thread = new Thread() {//匿名内部类
        {
            System.out.println("MyChild5 block");
        }
    };
}
//输出结果:
[Loaded com.aaa.test.MyTest5 from file:/E:/IdeaProjects/AAAE2E2/target/scala-2.11/classes/]
[Loaded com.aaa.test.MyParent5 from file:/E:/IdeaProjects/AAAE2E2/target/scala-2.11/classes/]
[Loaded com.aaa.test.MyChild5 from file:/E:/IdeaProjects/AAAE2E2/target/scala-2.11/classes/]
[Loaded com.aaa.test.MyParent5$1 from file:/E:/IdeaProjects/AAAE2E2/target/scala-2.11/classes/]
[Loaded com.aaa.test.MyChild5$1 from file:/E:/IdeaProjects/AAAE2E2/target/scala-2.11/classes/]
MyChild5 block
4ebcde87-e2cc-4e47-81fa-eb1c557d5ca4

当子接口初始化的时候,如果没有调用父接口的非编译期间指定变量,父接口不会初始化

eg3:
public class MyTest5 {
    public static void main(String[] args) {
        System.out.println(MyChild5.a);
    }
}

interface MyParent5 {
    public static String a = UUID.randomUUID().toString();
    public static Thread thread = new Thread() {//匿名内部类
        {
            System.out.println("MyParent5 block");
        }
    };
}

interface MyChild5 extends MyParent5 {
    public static int b = 6;
    public static String c = UUID.randomUUID().toString();
    public static Thread thread = new Thread() {//匿名内部类
        {
            System.out.println("MyChild5 block");
        }
    };
}
//输出结果:
[Loaded com.aaa.test.MyTest5 from file:/E:/IdeaProjects/AAAE2E2/target/scala-2.11/classes/]
[Loaded com.aaa.test.MyParent5 from file:/E:/IdeaProjects/AAAE2E2/target/scala-2.11/classes/]
[Loaded com.aaa.test.MyChild5 from file:/E:/IdeaProjects/AAAE2E2/target/scala-2.11/classes/]
[Loaded com.aaa.test.MyParent5$1 from file:/E:/IdeaProjects/AAAE2E2/target/scala-2.11/classes/]
MyParent5 block
20e41ba5-939a-485f-a45c-0682963c88f1

通过子接口调用父接口的非编译期间可指定变量时,父接口,子接口都会被加载,但是只有父接口会被初始化

如果调用的是接口编译期间可指定的变量,此时该变量存在调用类的常量池中。父接口,实现类都不会加载与初始化。

类实例化

本文为学习张龙老师深入理解JVM的笔记与心得,转载请注明出处!!!

上一篇下一篇

猜你喜欢

热点阅读