• 类的加载机制(二)


    二、验证

    验证这里可能大家会疑问了?我们的类不是通过JVM编译成的字节码的吗,为什么这里还要验证加载类的正确性,难道通过Java虚拟机的javac编译器生成的字节码还会有错误不成?当然,javac编译出来的类都是正确的,但是如果是通过其他途径生成的字节码呢?是不是正确的呢?就比如你自己建一个文本文件,然后重命名该文件为Test.class,然后让JVM来运行这个类,显然是错误的。当然因为JDK的源码是开放的,所以JVM字节码的生成规则也是公开的,所以也有一些第三方的软件可以生成符合JVM规范的字节码文件,如CGlib。

    类的验证包括一下四个方面
    –类文件的结构检查
    –语义检查
    –字节码验证
    –二进制兼容性的验证
    JDK为了保证class文件的安全性,在加载完成之后又进行了一系列的验证,这些个验证很多在编译的时候已经做过了,但是我们前面已经提到了,很多class字节码文件不是通过javac实现的,例如eclipse就是通过JTA实现的,而我们也可以自己随便写一个.class的文件让他加载,因此为了安全期间,JVM又进行了一次校验!

    三、准备

    为类的静态变量分配内存,并将其初始化为默认值,这里我们一定要看清楚是为静态变量分配内存,而不是我们的实例变量,为什么我要强调静态变量,因为实例变量是什么时候产生的,是生成实例的时候产生的,而我们一般是在new一个对象的时候才对这个类进行实例化(前提是这个类已经被加载),而我们现在还没有加载完类,所以这个时候只能对静态变量分配内存空间(静态变量是属于这个类的而不属于某个对象),这个一定要分清楚。然后为该静态变量初始化为默认值(这个大家应该不陌生,int类型是0,boolean就是false,引用类型是null等)。

    四、解析

    把类中的符号引用转换为直接引用

    这里我们解释一下符号引用和直接引用,在Java语言里面我们说是没有指针的,但是我们看到上图中,car调用了car类定义的run方法,而worker类中是没有car的run的,因此在运行的时候JVM把这个有一个符号和银行替换为一个指针(这里指替换为真正的由C++底层实现的指针),而我们java里面看到的就是符号引用,实际C++执行的才是指针,我们称之为直接引用!

     五、初始化

    初始化:这个似乎与上面的初始化为默认值有点矛盾,我们再看一遍:为累的静态变量赋予正确的初始值,上面是赋予默认值,这里是赋予正确的初始值,什么是正确的初始值,就是用户给赋予的值。我们来看一个例子

    class Test{
    private static int a = 1;
    
    }

    我们知道,这个类加载好之后,a的值就是1,但实际是这样子的,类在加载的连接阶段,将a初始化为默认值0(int的默认值是0),然后在初始化阶段将a的值赋予为正确的初始值1. 我们看到最终a的值是等于1,但是实际的运行中是有一个将0赋予a的过程,这个过程放生在连接的准备阶段。类的初始化还有另外的一种形式,代码如下:

    复制代码
    class Test{
    
    private static int a ;
    
    static{
    
    a=1;
    
      }
    
    }
    复制代码

    这里强调一点,这个时候还是没有类的实例生成的,这点一定要注意!《深入java虚拟机第二版》里面有一个图阐述了对应的关系,如下:

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

    二、初始化步骤

    1.假如这个类还没有被加载和连接,那就先进行加载和连接。【加载和连接执行完成后不一定会执行初始化,要符合主动使用才会进行初始化,否则只会执行加载和连接】
    2.假如类存在直接的父类,并且这个父类还没有被初始化,那就先初始化直接的父类。
    3.假如类中存在初始化语句,那就依次执行这些初始化语句。

    三、主动使用和被动使用【初始化时机】

     Java程序对类的使用方式可分为2种,主动使用和被动使用。所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用时”才初始化他们。

    主动使用的六种情况:

    1.创建类的实例。

    new Test();

    2.访问某个类或接口的静态变量,或者对该静态变量赋值。

    int b = Test.a;
    Test.a = b;

    3.调用类的静态方法

    Test.doSomething();

    4.反射

    Class.forName(“com.mengdd.Test”);

    5.初始化一个类的子类

    复制代码
    class Parent{
    }
    class Child extends Parent{
          public static int a = 3;
    }
    Child.a = 4;
    复制代码

    6.Java虚拟机启动时被标明为启动类的类

    java com.mengdd.Test

    除了以上六种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化 (除了上述6种情况以外,都不会执行初始化,只会执行加载和连接)

    这个时候我们再来看第一节学习中那段比较诡异的代码:

    复制代码
    class Singleton{
    
       
    
        private static Singleton singleton=new Singleton();
    
        private static int counter1;
    
        private static int counter2 = 0;
    
       
    
        public Singleton() {
    
           counter1++;
    
           counter2++;
    
        }
    
        public static int getCounter1() {
    
           return counter1;
    
        }
    
        public static int getCounter2() {
    
           return counter2;
    
        }
    
    
        public static Singleton getInstance(){
    
           return singleton;
    
        }
    
       
    
    }
    
    
    public class ClassLoaderTest {
    
    
        @SuppressWarnings("static-access")
    
        public static void main(String[] args) {
    
           Singleton singleton=Singleton.getInstance();
    
           System.out.println("counter1:"+singleton.getCounter1());
    
           System.out.println("counter2:"+singleton.getCounter2());
    
           System.out.println(singleton.getClass().getClassLoader());
    
        }
    
    }
    复制代码

    我们调用Singleton singleton=Singleton.getInstance();调用Singleton的静态方法,相当于主动使用了类Singleton,因此Singleton被初始化!
    当我们看到显示的是:

    private static Singleton singleton=new Singleton();
    
        private static int counter1;
    
        private static int counter2 = 0;

    这样的时候,顺序执行,先赋予初始值,singleton为null,counter1为0,counter2为0,然后顺序对singleton赋予正确的值newSingleton(),执行构造函数,counter1增加变为1,然后counter2变为1,然后继续执行初始化,将counter2赋值为正确的值,将counter2修改为0,因此运行结果是1、0

    反过来

    private static int counter1;
    
        private static int counter2 = 0;
    
        private static Singleton singleton=new Singleton();

    先赋予初始值counter1为0,counter为0,singleton为null,然后对counter2赋值为正确的值,counter2为0,然后对singleton执行初始化赋予正确的值new Singleton(),执行构造函数,counter1为1,counter2为1,因此执行结果是1、1

    到此,是不是感觉前面的问题豁然开朗了呢?

  • 相关阅读:
    Qt 学习之路 2(39):遍历容器
    Qt 学习之路 2(38):存储容器
    JS 格式化日期
    springboot 核心注解
    Java 生成随机数 Random、SecurityRandom、ThreadLocalRandom、Math.random()
    验证码 easy_captcha
    读过的书籍
    typora 常用快捷键
    kafka 遇到的问题
    老男孩Linux 运维
  • 原文地址:https://www.cnblogs.com/huxipeng/p/8686541.html
Copyright © 2020-2023  润新知