虚拟机类加载-静态块顺序

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载验证准备解析初始化使用卸载 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

在类加载的准备阶段是正式为类变量(静态变量)分配内存并设置初始值的阶段,这些变量所使用的内存将在方法区中进行分配。注意:

  1. 这个时候进行内存分配的仅包括类变量(被static 修饰的变量),而不包括实例变量,实例变量会在对象实例化时随着对象一起分配在Java堆中。
  2. 这里所说的初始值“通常情况”下是数据类型的零值。

这里写图片描述

“通常情况” 下初始值是零值,那特殊情况是指:如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量value 就会被初始化为ConstantValue属性所指的值,如
public static final int value = 123;
编译时javac 将会为value 生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置讲value赋值为123。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值