类的加载机制(二)

二、验证

验证这里可能大家会疑问了?我们的类不是通过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

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

posted @ 2018-04-01 14:35  Diligent_Watermelon  阅读(212)  评论(0编辑  收藏  举报