类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载 7个阶段。其中验证、准备、解析3个部分统称为连接。发生顺序如下:
对于加载,java 虚拟机规范中没有进行强制约束,交给虚拟机的具体实现来自由把握。但对于初始化阶段,虚拟机规范则是严格规定了有且只有5种 情况必须立即进行“初始化”(而加载、验证、准备自然在此之前开始):
1 ) 遇 到 new 、 getstatic 、 putstatic 或 invokestatic 这 4 条 字 节 码 指 令 时 , 如 果 类 没 有 进 行 过 初 始 化 , 则 需 要 先 触 发 其 初 始 化 。 生 成 这 4 条 指 令 的 最 常 见 的 Java 代 码 场 景 是 : 使 用 new 关 键 字 实 例 化 对 象 的 时 候 、 读 取 或 设 置 一 个 类 的 静 态 字 段 ( 被 . final 修 饰 、 已 在 编 译 期 把 结 果
放 人 常 量 池 的 静 态 字 段 除 外 ) 的 时 候 , 以 及 凋 用 一 个 类 的 静 态 方 法 的 时 候 。
2 ) 使 用 java-lang 、 reflect 包 的 方 法 对 类 进 行 反 射 调 用 的 时 候 , 如 果 类 没 有 进 行 过 初 始 化 , 则 需 要 先 触 发 其 初 始 化 。
3 ) 当 初 始 化 一 个 类 的 时 候 , 如 果 发 现 其 父 类 还 没 有 进 行 过 初 始 化 , 则 需 要 先 触 发 其 父 类 的 初 始 化 。
4 ) 当 虚 拟 机 启 动 时 , 用 户 需 要 指 定 一 个 要 执 行 的 主 类 ( 包 含 main() 方 法 的 那 个 类 ) , 虚 拟 机 会 先 初 始 化 这 个 主 类 。
5 ) 当 使 用 JDK 1.7 的 动 态 语 言 支 持 时 , 如 果 一 个 java.lang.invokeMethodHandle 实 例 最 后 的 解 析 结 果 REF-getStatic 、 REF_putStatic 、 REF invokeStatic 的 方 法 句 柄 , 并 且 这 个 方 法 句 柄 所 对 应 的 类 没 有 进 行 过 初 始 化 , 则 需 要 先 触 发 其 初 始 化
对于这5种会触发类进行初始化的场景,虚拟机规范中使用了一个很强烈的限定语:“有且只有”,这5个场景中的行为称为对一个类进行主动引用,除此之外,所有引用类的方式都不会触发初始化,称为被动引用。
例一:
- 1.通过子类引用父类的静态字段、不会导致子类初始化
父类:
package com.xnccs.cn.test;
public class SuperClass {
static{
System.out.println("SuperClass init!");
}
public static int value= 123;
}
子类:
package com.xnccs.cn.test;
public class SubClass extends SuperClass{
static{
System.out.println("SubClass init!");
}
}
测试类:
package com.xnccs.cn.test;
/**
* 非主动使用
* @author j_nan
*
* 可通过 -XX:+TraceClassLoading 参数观察到此操作会导致子类的加载
*
*/
public class NotInitialization {
public static void main(String[] args) {
System.out.print(SubClass.value);
}
}
输出:
SuperClass init!
123
结果并没有打印输出“SubClass init!” 说明通过子类引用父类的静态字段、不会导致子类初始化
例二:
- 通过数组定义来引用类,不会触发此类的初始化
修改下测试类
package com.xnccs.cn.test;
/**
* 非主动使用
* @author j_nan
*
* 可通过 -XX:+TraceClassLoading 参数观察到此操作会导致子类的加载
*
*/
public class NotInitialization {
public static void main(String[] args) {
SuperClass[] su = new SuperClass[10];
}
}
输出:
并没有输出SuperClass init! 说明 通过数组定义来引用类,不会触发此类的初始化
例三:
- 常量在编译阶段会存入类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
package com.xnccs.cn.test;
public class ConstClass {
static{
System.out.println("ConstClass init!");
}
public static final String HELLOWORlD = "hello world";
}
package com.xnccs.cn.test;
/**
* 非主动使用
* @author j_nan
*
* 可通过 -XX:+TraceClassLoading 参数观察到此操作会导致子类的加载
*
*/
public class NotInitialization {
public static void main(String[] args) {
System.out.println(ConstClass.HELLOWORLD);
}
}
输出:
hello world
并没有打印输出ConstClass init! 我们再用javap 命令看下编译后的class 文件
如上所述,常量在编译阶段会存入调用用的常量池中,也确实在Constant pool 中,也就是说实际上NotInitialization 的Class 文件之中并没有ConstClass类的符号引用入口,这两个类在编译成Class之后就不存在任何联系了。
下面再看一个试题,来便于初步理解类加载过程:
package com.xnccs.cn.share;
/**
* 1.加载的顺序:先父类的static成员变量 -> 子类的static成员变量 -> 父类的成员变量 -> 父类构造 -> 子类成员变量 -> 子类构造
2.static只会加载一次,所以通俗点讲第一次new的时候,所有的static都先会被全部载入(以后再有new都会忽略),进行默认初始化。在从上往下进行显示初始化。这里静态代码块和静态成员变量没有先后之分,谁在上,谁就先初始化
3.构造代码块是什么?把所有构造方法中相同的内容抽取出来,定义到构造代码块中,将来在调用构造方法的时候,会去自动调用构造代码块。构造代码快优先于构造方法。
* @author j_nan
*
*/
public class StaticTest {
public static int k = 0;
public static StaticTest t1 = new StaticTest("t1");
public static StaticTest t2 = new StaticTest("t2");
public static int i = print("i");
public static int n = 99;
public int j = print("j");
{
print("构造块");
}
static{
print("静态块");
}
public StaticTest(String str) {
System.out.println("StaticTest 构造方法: "+(++k) + ":" + str + " i=" + i + " n=" + n);
++n;
++i;
}
public static int print(String str) {
System.out.println("print 打印: "+(++k) + ":" + str + " i=" + i + " n=" + n);
++i;
return ++n;
}
public static void main(String[] args) {
StaticTest t = new StaticTest("init");
}
}
输出:
print 打印: 1:j i=0 n=0
print 打印: 2:构造块 i=1 n=1
StaticTest 构造方法: 3:t1 i=2 n=2
print 打印: 4:j i=3 n=3
print 打印: 5:构造块 i=4 n=4
StaticTest 构造方法: 6:t2 i=5 n=5
print 打印: 7:i i=6 n=6
print 打印: 8:静态块 i=7 n=99
print 打印: 9:j i=8 n=100
print 打印: 10:构造块 i=9 n=101
StaticTest 构造方法: 11:init i=10 n=102
在类加载的准备阶段是正式为类变量(静态变量)分配内存并设置初始值的阶段,这些变量所使用的内存将在方法区中进行分配。注意:
- 这个时候进行内存分配的仅包括类变量(被static 修饰的变量),而不包括实例变量,实例变量会在对象实例化时随着对象一起分配在Java堆中。
- 这里所说的初始值“通常情况”下是数据类型的零值。
“通常情况” 下初始值是零值,那特殊情况是指:如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量value 就会被初始化为ConstantValue属性所指的值,如
public static final int value = 123;
编译时javac 将会为value 生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置讲value赋值为123。