从类的生命周期而言 一个类包括如下阶段:
JVM类加载过程 - 图1
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的 类的加载过程必须按照这种顺序进行
而解析阶段则不一定 它在某些情况下可能在初始化阶段后在开始 因为java支持运行时绑定

类加载时机

加载(loading)阶段 java虚拟机规范中没有进行约束**

类加载方式

这里的类加载不是指类的加载阶段,而是指整个类加载过程,即类的加载阶段到初始化完成
1)隐式加载
创建类对象
使用类的静态域
创建子类对象
使用子类的静态域
在JVM启动时 BootStrapLoader会加载一些JVM自身运行所需的class
在JVM启动时 ExtClassLoader会加载指定目录下一些特殊的class
在JVM启动时 AppClassLoader会加载classpath路径下的class 以及main函数所在的类的class文件
2)显式加载
ClassLoader.loadClass(className);
只加载和连接 不会进行初始化
Class.forName(String name, boolean initialize,ClassLoader loader);
使用loader进行加载和连接 根据参数initialize决定是否初始化

1 加载阶段

加载是类加载过程中的一个阶段 不要将这2个概念混淆了
在加载阶段 虚拟机需要完成以下3件事情:
1)通过一个类的全限定名来获取定义此类的二进制字节流
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3)在内存中生成一个代表这个类的java.lang.Class对象 作为方法区这个类的各种数据的访问入口
也就是把.class文件根据字节流来创建代表这个类的java.lang.Class对象 然后会把.class文件装载到内存

加载.class文件的方式:
1)从本地系统中直接加载
2)通过网络下载.class文件
3)从zip,jar等归档文件中加载.class文件
4)从专有数据库中提取.class文件
5)将Java源文件动态编译为.class文件
相对于类生命周期的其他阶段而言 加载阶段(准确地说 是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段
因为开发人员既可以使用系统提供的类加载器来完成加载 也可以自定义自己的类加载器来完成加载

2 连接阶段

2.1 验证 确保被加载的类的正确性
确保Class文件的字节流中包含的信息符合当前虚拟机的要求 并且不会危害虚拟机自身的安全
1)文件格式验证:验证字节流是否符合Class文件格式的规范
如:是否以模数0xCAFEBABE开头、主次版本号是否在当前虚拟机处理范围内等等
2)元数据验证:对字节码描述的信息进行语义分析 以保证其描述的信息符合Java语言规范的要求
如:这个类是否有父类 是否实现了父类的抽象方法 是否重写了父类的final方法 是否继承了被final修饰的类等等
3)字节码验证:通过数据流和控制流分析 确定程序语义是合法的、符合逻辑的
如:操作数栈的数据类型与指令代码序列能配合工作 保证方法中的类型转换有效等等
4)符号引用验证:确保解析动作能正确执行
如:通过符合引用能找到对应的类和方法 符号引用中类、属性、方法的访问性是否能被当前类访问等等
验证阶段是非常重要的 但不是必须的 可以采用-Xverify:none参数来关闭大部分的类验证措施
2.2 准备:为类的静态变量分配内存 并将其赋默认值
为类变量分配内存并设置类变量初始值 这些内存都将在方法区中分配
对于该阶段有以下几点需要注意:
1)只对static修饰的静态变量进行内存分配、赋默认值(如0、null、false等)
2)对final的静态字面值常量直接赋初值 赋初值是程序员给定的初值
如果不是字面值静态常量 那么会和静态变量一样赋默认值
2.3 解析:将常量池中的符号引用替换为直接引用(内存地址)的过程
Java在编译器Java类并不知道所引用类的实际地址,只能用符号引用来代替
1)符号引用就是一组符号来描述目标 可以是任何字面量

属于编译原理方面的概念如:包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符
2)直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
如指向方法区某个类的一个指针
假设:一个类有一个静态变量 该静态变量是一个自定义的类型
那么经过解析后 该静态变量将是一个指针 指向该类在方法区的内存地址
例子:test() 方法解析后就会把test变成一个引用 通过引用可以找到该方法

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

赋初值两种方式:
1)定义静态变量时指定初始值
如 private static String x=”123”;
2)在静态代码块里为静态变量赋值
如 static{ x=”123”; }
注意:只有对类的主动使用才会导致类的初始化
初始化阶段 java虚拟机严格规定了有且只有如下5种情况必须立即进行初始化
(初始化前 又必须经过加载、验证、准备阶段)
1)使用new实例化对象时、读取和设置类的静态变量、静态非字面值常量(静态字面值常量除外)时、调用静态方法时
2)对内进行反射调用时
3)当初始化一个类时,如果父类没有进行初始化,需要先初始化父类
4)启动程序所使用的main方法所在类
5)当使用1.7的动态语音支持时
如上5种场景又被称为主动引用 除此之外的引用称为被动引用
被动引用有如下几种常见情况
1)通过子类引用父类的静态属性 只会触发父类的初始化 而不会触发子类的初始化
2)定义对象数组和集合 不会触发该类的初始化
如 Person[] persons = new Person[10];
3)类A引用类B的static final常量不会导致类B初始化
注意静态常量必须是字面值常量 否则还是会触发B的初始化
也就是final修饰的静态常量会在常量池中找 不会经过初始化
4)通过类名获取Class对象 不会触发类的初始化 如System.out.println(Person.class);
5)通过Class.forName加载指定类时 如果指定参数initialize为false时 也不会触发类初始化
6)通过ClassLoader默认的loadClass方法 也不会触发初始化动作
注意:被动引用不会导致类初始化 但不代表类不会经历加载、验证、准备阶段

  1. public class TestMain{
  2. //主函数中使用了类中静态变量 触发该类初始化
  3. //初始化前 先经过加载、验证、准备阶段
  4. //在准备阶段 会对静态变量进行内存分配、赋默认值
  5. //此时 ts = null; x = 0; y = 0;
  6. //然后在初始化阶段 为静态变量赋初值
  7. //先执行ts = new TestMain(); 使得此时 x = 1; y = 1;
  8. //然后再执行 x = 0;
  9. //所以最终结果 x = 0; y = 1;
  10. //若调换了ts与x的顺序 那么结果会变为 x = 1; y = 1;
  11. private static TestMain ts = new TestMain();
  12. public static int x = 0;
  13. public static int y;
  14. private TestMain() {
  15. x++;
  16. y++;
  17. }
  18. public static void main(String[] args){
  19. System.out.println("x="+TestMain.x+";"+"y="+TestMain.y);
  20. //结果为x=0;y=1
  21. }
  22. }

此外需注意 静态代码块和静态变量会先与静态方法执行
而静态变量和静态代码块会看代码书写顺序 谁在前谁先执行

  1. public class TestMain{
  2. //static int i=4;
  3. //若静态变量放在这个位置
  4. //结果为 静态代码块 静态方法 2
  5. static {
  6. i=2;
  7. System.out.println("静态代码块");
  8. }
  9. public static void method(){
  10. System.out.println("静态方法");
  11. System.out.println(i);
  12. }
  13. static int i=4;
  14. //若静态变量放在这个位置
  15. //结果为 静态代码块 静态方法 4
  16. public static void main(String[] args) {
  17. TestMain.method();
  18. }
  19. }