018、代码块和类加载

Par @Martin dans le
Tags :

1. 代码块

Java 中有一种语法, 它将一段代码直接用 { } (花括号)括起来, 这种语法被称做代码块, java 中的代码块根据它出现的位置共分三种:

  • 局部代码块
  • 构造代码块
  • 静态代码块

1.1 局部代码块

在方法中出现的 { } 就称为局部代码块, 又称普通代码块, 普通代码块和一般的语句执行顺序由他们在代码中出现的次序决定, 即“先出现先执行”.

1.2 构造代码块

直接在类中定义且没有加 static 关键字的代码块称为 { } 构造代码块, 构造代码块在创建对象时被调用, 每次创建对象都会被调用, 并且构造代码块的执行次序优先于类构造函数.

1.3 静态代码块

在类中使用 static 关键字声明的代码块称为 { } 静态代码块, 静态代码块在 jvm 加载类时被执行一次, 一般作用为类的属性初始化.
虽然它也有静态二字, 但静态代码块并不能直接访问静态实例变量和实例方法, 需要通过类的实例对象来访问.

2. 类加载

上面说到静态代码块会在 jvm 加载类时被执行一次, 那究竟什么时候 jvm 会去加载类呢?

2.1 首先, 何为类的加载?

一个 java 文件从被加载到被卸载这个生命过程, 总共要经历 5 个阶段:
装载 -> 链接 (验证 + 准备 + 解析) -> 初始化 -> 使用 (即实例化)-> 卸载

而细分下来, 共包括 装载 (Loading) 、验证 (Verification) 、准备 (Preparation) 、解析 (Resolution) 、初始化 (Initialization) 、使用 (Using) 、卸载 (Unloading) 等七个阶段, 而类的加载指的就是 装载 -> 链接 (验证 + 准备 + 解析) -> 初始化个阶段.

虚拟机规范对于何时进行加载这一阶段并没有强制约束, 但对于初始化阶段, 虚拟机规范是严格规定了有且只有种情况必须立即对类进行初始化:

  • 使用 new 关键字实例化对象、读取或设置一个类的静态字段以及调用一个类的静态方法的时候 (被 final 修饰的静态字段不属于这些场景).
  • 使用 java.lang.reflect 包的方法对类进行反射调用时, 如果类没有进行过初始化, 则需要先触发其初始化.
  • 当初始化一个类的时候, 如果发现其父类还没有进行过初始化, 则需要先触发其父类的初始化.
  • 当虚拟机启动时, 用户需要指定一个要执行的主类 (包含 main() 方法的类), 虚拟机会先初始化这个主类.

以上四种场景中的行为称为对一个类进行主动引用, 除此之外所有引用类的方式都不会触发初始化, 称为被动引用.

接口的加载过程与类加载过程最主要的区别在于第三点, 即当初始化一个接口时, 并不需要先初始化其父接口, 而是只有真正使用到父接口中的字段的时候才会初始化.

2.2 类加载阶段的简单说明

加载阶段
虚拟机需要完成三件事:通过一个类的全限定名来获取定义此类的二进制字节流, 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构, 在 java 堆中生成一个代表这个类的 java.lang.Class 对象, 作为方法区这些数据的访问入口.

验证阶段
不同虚拟机会进行不同类验证的实现, 但大致都会完成以下四个阶段的检验过程:文件格式验证 (验证字节流是否符合 Class 文件格式的规范, 并能被当前版本的虚拟机处理) , 元数据验证 (对字节码描述信息进行语义分析, 保证其描述信息符合 java 语言规范) , 字节码验证 (对类方法体进行数据流和控制流分析, 保证类的方法在运行时不会做出危害虚拟机的行为) 和符号引用验证 (发生在将符号引用转化为直接引用的时候, 在解析阶段中发生) .

准备阶段
正式为类成员变量 (注意, 不是实例成员变量, 实例变量会在对象实例化时随着对象一起分配在 java 堆上) 分配内存并设置类变量初始值 (通常情况下是数据类型的零值, 不进行赋值操作) 的阶段, 这些内存都将在方法区中进行分配.

例: public static int value=123; 则在准备阶段过后, value 的初始值为 0 而不是 123, 赋值指令是在初始化阶段通过构造方法来执行的.

解析阶段
虚拟机将常量池内的符号引用替换为直接引用的过程.符号引用与内存布局无关, 而直接引用的目标必定已经在内存中存在.解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行.

初始化阶段
真正开始执行类中定义的 java 程序代码 (字节码) .

所以我们可以看到, 静态代码块其实是在类初始化阶段被执行的.

2.3 手动加载类

Class.forName(xxx.xx.xx) 的作用是要求 JVM 查找并加载指定的类.

Class.forName(xxx.xx.xx) 是运用反射的原理创建对象.
Class.forName(xxx.xx.xx) 返回的是类.
Class.forName(xxx.xx.xx).newInstance() 返回的才是object.