类加载的过程

2018/12/13 Java JVM

类加载的过程

image

  1. 类加载的过程包括了加载验证准备解析初始化五个阶段。
  2. 解析阶段可能在初始化阶段前开始,也可能在初始化之后开始。这是为了支持Java语言的运行时绑定(动态绑定)
  3. 这几个阶段是按顺序开始,不是按顺序进行或完成的。

加载

  1. 通过一个类的全限定名来获取其定义的二进制字节流

  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

  3. 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

验证

目的:

  1. 确保Class文件中的字节流包含的信息符合当前虚拟机的要求。
  2. 确保字节流不会危害虚拟机自身的安全。

流程:

  1. 文件格式验证

验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
目的是保证输入的字节流能正确地解析并存储于方法区之内。

经过该阶段的验证后,字节流才会进入内存的方法区中进行存储,后面的三个验证都是基于方法区的存储结构进行的。

  1. 元数据验证

对类的元数据信息进行语义校验(其实就是对类中的各数据类型进行语法校验)。 保证不存在不符合Java语法规范的元数据信息。

  1. 字节码验证

进行数据流和控制流分析,对类的方法体进行校验分析。
保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。

  1. 符号引用验证

这是最后一个阶段的验证,它发生在虚拟机将符号引用转化为直接引用的时候(解析阶段中发生该转化),主要是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。

准备

准备阶段是正式为类变量(static变量)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中分配。

  1. 只为类中static变量设置默认初始值。
  2. 设置的值为默认的零值,而不是被Java代码中赋予的值。
数据类型 默认零值
int 0
long 0
short (short)0
char ‘\u0000’
byte (byte)0
boolean false
float 0.0f
double 0.0d
reference null
  1. 对于类变量(类中static变量),如果不显式赋值,系统会附默认零值。对于局部变量不显式附值编译报错。
  2. 如果数组初始化时未赋值,其中的元素都将被附零值。
  3. 对于static final修饰的常量,必须在声明时就显式的赋值,否则编译不通过。
    1. 只有final修饰的常量,既可以在声明时赋值,也可以在初始化时赋值,不能不赋值。系统不会为final值附默认值。
    2. 在准备阶段(编译期)虚拟机就会将static常量的值放入常量池中。

解析

解析阶段是虚拟机将常量池中的符号引用转化为直接引用的过程

(常量池存在于方法区中)

解析阶段可能开始于初始化前,也可能开始于初始化后,虚拟机会根据需要判断到底是在类加载器加载时就对常量池中的符号引用进行解析,还是在使用该符号引用时才去解析它。

符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。

直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。

  1. 类或接口的解析
    假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,有以下3个步骤:
    1. 如果C不是一个数组类型,虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C。
    2. 如果C是一个数组类型,并且数组的元素类型为对象,也就是N的描述符是类似”[Ljava/lang/Integer]”的形式,那将会按照第1点的规则加载数组元素类型。接着由虚拟机生成一个代表数组维度和元素的数组对象。
    3. 解析完成后进行符号引用验证,确认D是否具备对C的访问权限。
  2. 字段解析 (父类有则用父类,没有则继续往上找) 先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,则查找结束;如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系从上往下递归搜索其父类,直至查找结束。
  3. 类方法解析 对类方法解析与对字段解析步骤差不多。只是多了对接口的判断。先搜索父类、再搜索接口
  4. 接口方法解析 递归向上搜索父接口。

初始化

类初始化阶段是类加载过程的最后一步。在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,虚拟机执行类构造器<clinit>()方法,搜集和执行类静态变量的赋值动作静态语句块(static{}块)中的语句。

  1. 编译器堆类变量赋值和静态语句块的收集顺序是由语句在源文件中出现的顺序决定。
    //静态语句块中只能访问到定义在静态语句块之前的变量,
    //定义在之后的变量,只能赋值、不能访问
    public class Test{
     static {
         i = 0; //可以正常编译通过
         System.out.print(i); //非法前向引用
     }
     static int i = 1;
    }
    
  2. 虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。 ```java static class Parent{ public static int A = 1; static { A = 2; } }

static class Sub extends Parent{ public static int B = A; }

public static void main(String[] args){ Sytem.out.println(Sub.B); //2 }


3. 接口中不能使用静态语句块,但依然有赋值操作,因此也有`<clinit>()`方法。但是执行接口的`<clinit>()`方法不需要执行父接口的`<clinit>()`方法。只有当父接口中定义的变量使用时,父接口才会初始化。接口的实现类在初始化时也不会执行接口的`<clinit>()`方法。
4. 虚拟机会保证`<clinit>()`方法的多线程安全,如果多个线程同时初始化一个类,只会有一个线程去执行这个类的`<clinit>()`方法。

#### <clinit>和<init>的区别

<clinit>:在jvm第一次加载class文件时调用,包括静态变量初始化语句和静态块的执行

<init>:在实例创建出来的时候调用,包括调用new操作符;调用Class或java.lang.reflect.Constructor对象的newInstance()方法;调用任何现有对象的clone()方法;通过java.io.ObjectInputStream类的getObject()方法反序列化。

#### static{}语句块

static块会在类被加载的时候执行且只会被执行一次。一般用来初始化静态变量和调用静态方法。
1. 当一个类中有多个static块时,按照static块的定义顺序,从前往后执行。
2. 先执行完static块的内容,才会执行调用语句(main)。
3. 静态变量赋值操作也是在类加载的时候完成的,当一个类中既有static块又有static变量时,t同样遵循先定义先执行原则。

#### 静态绑定和动态绑定

绑定指的是把一个方法的调用与方法所在的类(方法主体)关联起来,对java来说,绑定分为静态绑定和动态绑定:

1. 静态绑定:即前期绑定。在程序执行前方法已经被绑定,此时由编译器或其它连接程序实现。针对java,简单的可以理解为程序编译期的绑定。
    java当中的方法只有**final**,**static**,**private**和**构造方法**是前期绑定的。

    1. private: 不能被继承,没办法通过子类对象调用,因此是静态绑定。
    2. final方法:可以被继承,但不能被重写。‘
    3. 构造方法:不能被继承。
    4. static方法:静态方法属于类而不属于对象。static方法可以被子类继承、隐藏而不能被重写。当将子类对象转型为父类对象时,不论子类中有没有定这个静态方法,该对象都会使用父类中的静态方法。

2. 动态绑定:即晚期绑定,也叫运行时绑定。在运行时根据具体对象的类型进行绑定。除final、static、private和构造方法外都是动态绑定。
    1. 虚拟机提取对象的实际类型的方法表;
    2. 虚拟机搜索方法签名;
    3. 调用方法。

3. 静态绑定 VS 动态绑定

    1. 静态绑定是发生在编译阶段;而动态绑定是在运行阶段;
    2. private, final and static方法和变量使用静态绑定,而虚函数(virtual methods)则会根据运行时的具体对象进行绑定(注:在Java语言中, 所有的方法默认都是”虚函数”。只有以关键字 final 标记的方法才是非虚函数。)
    3. 静态绑定使用的是类信息,而动态绑定使用的是对象信息
    4. 重载方法(overloaded methods)使用的是静态绑定,而重写方法(overridden methods)使用的是动态绑定。



refs:
1. https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html
2. http://note.youdao.com/noteshare?id=b2d27668822ed6f794b2a64760f7fb43
3. https://blog.csdn.net/ns_code/article/details/17881581

静态绑定VS动态绑定
4. https://blog.csdn.net/luckykapok918/article/details/50556047
5. https://www.jianshu.com/p/4277bbcab647
 

本文地址:https://cheng-dp.github.io/2018/12/13/class-load-process/ ```

Search

    Table of Contents