美文网首页Java 杂谈JVM · Java虚拟机原理 · JVM上语言·框架· 生态系统
深入理解JVM(一)类加载器部分:初始化规则、单例对象分析

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

作者: 利伊奥克儿 | 来源:发表于2019-06-03 17:42 被阅读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
    

    类加载到实例化对象过程

    加载

    • 加载:就是把二进制的java类型读入java虚拟机中(.class)

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

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

    • 有两种类加载器
    1. Java虚拟机自带的类加载器

    根类加载器(Bootstrap)

    扩展类加载器(Extension)

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

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

    java.lang.ClassLoader的子类

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

    • 类加载器并不需要等到某个类被”首次主动使用“是再加载它

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

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

    连接

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

    • 验证: 验证.class文件的正确性

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

    • 准备: 为类变量(静态变量)分配内存,设置默认值,直到初始化之前,类变量都没有被赋值成真正的初始值(我们指定的初始值,参考单例模式部分案例)
    • 解析:在类型的常量池中寻找类、接口、字段和方法的符号引用,并将这些符号引用替换成直接引用的过程

    初始化

    • 初始化:为类变量赋值为真正的初始值(我们指定的,如果没有指定则使用默认的初始值)

    静态变量的声明语句、静态代码块都被看作类的初始化语句,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
    

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

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

    类实例化

    • 为新的对象分配内存(堆)
    • 为实例变量赋默认值(类似类变量先赋默认值,非指定)
    • 为实例变量赋正确的初始值(类似类变量先赋指定值定)
    • java编译器会为它编译的每一个类都至少生成一个实例初始化方法(即构造函数),在java的.class文件中,这个实例化初始化方法被称为<init>。针对源代码中每一个类的构造方法,编译器都产生一个<init>方法

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

    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/zkchxctx.html