美文网首页术业专攻
类的加载、连接、初始化

类的加载、连接、初始化

作者: zhh_happig | 来源:发表于2018-12-20 10:57 被阅读853次

    转载、引用请标明出处
    https://www.jianshu.com/p/853701433b3a
    本文出自zhh_happig的简书博客,谢谢

    以下内容,是本人学习笔记和工作中的总结,仅供大家参考,有误的地方还请指正

    java虚拟机

    执行一个java程序,都会启动一个java虚拟机的进程,进程里面包含一个主线程来执行程序,当程序执行完了之后,java虚拟机进程就消亡了。
    在如下几种情况下,java虚拟机将结束生命周期

    • 执行了System.exit()方法
    • 程序正常执行结束
    • 程序在执行过程中遇到了异常或错误而异常终止
    • 由于操作系统出现错误而导致java虚拟机进程终止

    一 类的加载、连接、初始化

    1 加载:查找并加载类的二进制数据

    • 类记载器ClassLoader将java的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区中
    • 加载后,虚拟机在堆区创建一个与该类对应java.lang.Class对象,不管类的对象有多少个,与此类对应的Class对象只有一个,Class对象用来封装类在方法区内的数据结构,所以类里面的内容都可以通过Class对象获取。Class对象是反射的入口。
    • 类的加载并不是你首次使用的时候去加载,而是预料到某个类要被使用的时候预先加载它。
    • 程序只有被加载到内存中,才能被执行

    2 连接

    • 验证:确保被加载的类的正确性
      • 通过javac生成的.class文件肯定是正确的,但是有的直接手动生成.calss可能是不符合java字节码规则的,这里要验证。
    • 准备:为类的静态变量分配内存,并将其初始化为默认值
      • 到这一步,类的加载、连接、初始化还没有完成,不会生成对象,所以所有的实例对象都不会分配内存,只有静态变量会被分配内存,初始化为默认值:int型初始化为0,boolean类型初始化为false,引用类型初始化为null。顺序:从上至下。
    • 解析:把类中的符号引用转换为直接引用
      • 在Worker类中调用car.run(),Car类的run()方法。car.run()---这是符号引用,在解析阶段java虚拟机会把这个符号引用替换为一个指针,该指针指向了Car类的run()方法在方法区内的内存位置,这个指针就是直接引用。

    3 初始化:为类的静态变量赋予正确的初始值

    • 正确的初始值是在程序中被赋的值,比如 int a = 3,这一步将3赋值给a;所以a经过了两次赋值:第一次是连接准备阶段,a先会被赋默认初始值0,第二就是初始化为程序中赋的值。
    • 静态变量的初始化有两种方式
      • 在静态变量的声明处进项初始化
      • 在静态代码块中进行初始化
    • 初始化步骤
      • 假如这个类还没有被加载和连接,先进行加载和连接
      • 假如这个类存在父类,父类还没有初始化,先初始化直接的父类
      • 初始化语句执行顺序:从上至下依次执行
    • 初始化时机
      • 类的主动使用
      • 当java虚拟机初始化类时,它所有的父类都已经被初始化了,但是这条规则不适用与接口
      • 在初始化一个类时,并不会先初始化它实现的接口
        • 初始化一个接口时,并不会初始化它的父接口
        • 只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化

    二 java程序对类的使用方式

    主动使用
    被动使用

    主动使用(6种),除了以下6种,其他类的使用全是被动使用

    • 创建类的实例
      • new出来一个类的实例对象,即为对这个类的主动使用
    • 访问某个类或接口的静态变量,或者对该静态变量赋值
      • int b = Test.a; 这个也是对Test类的主动使用
      • final修饰的静态变量要注意:如果这个变量在编译时就能确定它的值,就不会导致类被初始化,例如 public static final int a = 6/3
      • 如果这个变量要在运行时才能确定它的值,才会导致类被初始化,例如public static final int a = new Random().nextInt(100)
    • 调用类的静态方法
      • int b = Test.add()
      • 只有当访问的静态变量或静态方法确实在当前类或当前接口中定义时,才可以认为是对类和接口的主动使用,详见练习题5中例子。
    • 反射创建类的实例
    • 初始化一个类的子类
      • 初始化一个父类的子类,也是对这个父类的主动使用
    • Java虚拟机启动时被标注为启动的类
      • 启动程序的类,包含main方法的类

    所有的java虚拟机实现必须在每个类或接口被java程序 首次主动使用 时才初始化他们,换句话说就是以上6种情况,而且是第1次主动使用才会在初始化。其他情况,如被动使用,或第二次主动使用,都不会执行类的初始化。

    调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。调用ClassLoader类的loadClass方法只是执行了加载操作。

    二 示例演示——增加理解

    示例1

    class Singleton{
    
        private static Singleton singleton = new Singleton();
        public static int counter1;
        public static int counter2 = 0;
    
        private Singleton(){
            counter1 ++;
            counter2 ++;
        }
    
        public static Singleton getInstance(){
            return singleton;
        }
    
    }
    
    public class JVMTest {
    
        public static void main(String[] args){
            Singleton singleton = Singleton.getInstance();
            System.out.println("counter1 = " + singleton.counter1);
            System.out.println("counter2 = " + singleton.counter2);
        }
    
    }
    
    输出结果
    1
    0
    

    在类的连接--准备阶段,singleton被赋默认值null,counter1和counter2被赋默认值0;初始化的时候,先初始化singleton,创建实例,在Singleton 构造方法中counter1++,counter2++后,counter1和counter2变成了1,然后在再初始化counter1和counter2,counter1没有被赋值,所以还是1, counter2被赋值了0,所以counter2最终为0。

    如果将上面的部分代码顺序改一下:

    public static int counter1;
    public static int counter2 = 0;
    private static Singleton singleton = new Singleton();
    输出结果
    1
    1
    如果看懂了上面的代码,这个不难理解。
    

    示例2

    public class Test2 {
    
        public static void main(String[] args){
            System.out.println(FinalTest.x);
        }
    
    }
    
    class FinalTest{
    
        public final static int x = 1/3;
        static{
            System.out.println("FinalTest static block");
        }
    
    }
    
    输出结果
    0
    

    final修饰的静态变量要注意:如果这个变量在编译时就能确定它的值,就不会导致类被初始化,例如
    public static final int x = 1/3

    public class Test2 {
    
        public static void main(String[] args){
            System.out.println(FinalTest.x);
        }
    
    
    }
    
    class FinalTest{
    
        public final static int x = new Random().nextInt(100);
        static{
            System.out.println("FinalTest static block");
        }
    
    }
    
    输出结果
    FinalTest static block
    75
    

    如果这个变量要在运行时才能确定它的值,才会导致类被初始化,例如
    public static final int x = new Random().nextInt(100)

    示例3

    public class Test3 {
    
        static {
            System.out.println("Test3 static block");
        }
    
        public static void main(String[] args){
            System.out.println(Child.b);
        }
    
    }
    
    class Parent{
    
        static int a = 3;
        static{
            System.out.println("Parent static block");
        }
    
    }
    
    
    class Child extends Parent{
    
        static int b = 4;
        static{
            System.out.println("Child static block");
        }
    
    }
    
    输出结果?
    Test3 static block
    Parent static block
    Child static block
    4
    

    Test3是程序入口类,最先被加载初始化;先加载初始化父类,再子类。

    示例4

    public class Test4 {
    
        static {
            System.out.println("Test4 static block");
        }
    
        public static void main(String[] args){
            Parent2 parent2;
            System.out.println("-------------");
    
            parent2 = new Parent2();
            System.out.println(Parent2.a);
            System.out.println(Child2.b);
        }
    
    }
    
    class Parent2{
    
        static int a = 3;
        static{
            System.out.println("Parent2 static block");
        }
    
    }
    
    
    class Child2 extends Parent2{
    
        static int b = 4;
        static{
            System.out.println("Child2 static block");
        }
    
    }
    
    输出结果
    Test4 static block
    -------------
    Parent2 static block
    3
    Child2 static block
    4
    

    Child2、Parent2是由同一个类加载器加载的,所以Parent2初始化了, Child2初始化的时候就不会再去初始化父类了。
    PS: 如果有两个加载器:A类的加载器,B类的加载器,AB不是父子关系,即使A类的加载器已经初始化了Child2类,在B类的加载中还是可以再去初始化Child2类的。详解请看后续的 Java类加载器 文章

    为什么下面这一行代码不去初始化Parent2呢?

    ...
    Parent2 parent2;
    ...
    

    因为这只是声明了一个变量,并没有主动使用类,所以不会初始化。

    示例5

    public class Test5 {
    
        public static void main(String[] args){
            System.out.println(Child3.a);
            Child3.doSomething();
        }
    
    }
    
    class Parent3{
    
        static int a = 3;
        static{
            System.out.println("Parent3 static block");
        }
    
        static void doSomething(){
            System.out.println("doSomething");
        }
    
    }
    
    
    class Child3 extends Parent3{
    
        static{
            System.out.println("Child3 static block");
        }
    
    }
    
    输出结果?
    Parent3 static block
    3
    doSomething
    

    Child3.a,Child3.doSomething() 为什么Child3没有别初始化?
    只有当访问的静态变量或静态方法确实在当前类或当前接口中定义时,才可以认为是对类和接口的主动使用。而Child3.a,Child3.doSomething()调用的静态变量或静态方法不是在Child2类中定义的,而是在父类中定义的,所以不会对Child2进行初始化。

    示例6

    class C{
    
        static {
            System.out.println("class C");
        }
    
    }
    
    public class Test1 {
    
        public static void main(String[] args) throws Exception{
            ClassLoader loader = ClassLoader.getSystemClassLoader();
            Class<?> clazz = loader.loadClass("com.zhh.jvm.loadClass.C");//加载 C 类
            System.out.println("------------");
            clazz = Class.forName("com.zhh.jvm.loadClass.C");
        }
    
    }
    
    输出结果?
    ------------
    class C
    

    loader.loadClass("com.zhh.jvm.loadClass.C");
    调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。调用ClassLoader类的loadClass方法只是执行了加载操作。clazz = Class.forName("com.zhh.jvm.loadClass.C");是反射创建类的实例,是类的主动使用,所以导致类被初始化。

    以上内容,是本人学习笔记和工作中的总结,仅供大家参考,有误的地方还请指正

    转载、引用请标明出处
    https://www.jianshu.com/p/853701433b3a
    本文出自zhh_happig的简书博客,谢谢

    相关文章

      网友评论

        本文标题:类的加载、连接、初始化

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