JVM原理学习笔记(三) —— 类的初始化

最近在阅读 《Inside the JVM》 这本书,结合一些日常工作学习中的感想,随便写一些东西,蜻蜓点水,不必有章法。

类的初始化工作,主要是将静态变量、常量初始化为“正确”的值(也就是程序员希望设定的特定值而非其类型的默认值),以及其它一些需要在初始化类的时候需要做的工作(如读取配置文件等)。通常我们可以这样做:

class A extends B {
public static int intVal = 30;
public static String strVal;

static {
strVal = readConfig("ItemA");
}

private static String readConfig(String key) {
....
}
}

当一个类被加载,它将顺序经历四个过程:验证、准备、解析、初始化(一个类被加载以前,如果其父类尚未初始化,那么JVM会先去用以上四个过程对这个父类进行初始化)。验证只是检查class文件是否符合java语义并且不会损害JVM的完整性,这里不多赘述。准备阶段是为类成员分配内存,同时根据该成员的类型赋给它相应的默认值,对于上面的示例类A,经过准备阶段后状态是这样的:
intVal = 0;
strVal = null;
解析是把符号引用转为直接引用的过程,比如,原来JVM只知道类A有一个成员叫"intVal",通过解析则知道了该成员的地址是0xBBEDF,以后再使用这个成员的时候,就直接去这个内存地址去找了。同时在这个阶段,类的方法比如上面的readConfig()也会被映射到某个内存地址以待调用。
初始化则是利用类定义的JAVA代码确定成员变量的初值(如果对某个成员没有相应的java代码对其进行初始赋值操作,那么它就沿用在准备阶段获得的该类型默认值)。在这个阶段,所有的静态代码都会被执行,事实上,类在编译时编译器是会把这些静态代码封装到一个<clinit>方法中去的,在使用这个类的时候,初始化过程就会调用这个<clinit>方法。这个方法程序员是不能调用的,它只能被JVM调用。

以上对JVM初始化一个类的过程做了一些讲解,但是JVM究竟什么时候才会初始化一个类呢?总的来说,JVM会在“主动”使用一个类的时候将该类初始化。所谓“主动”,大致有6种已知的行为,对我们比较常见的是:1)试图创建该类的一个新实例;2)调用该类声明的一个静态方法;3)使用类中声明的非常量静态字段。考虑下面的例子:

public interface Angry {
String greeting = "Grrrr!";
int angerLevel = Dog.getAngerLevel();
}

public class Dog implements Angry {
public static final String greeting = "Wong, Wong, Wong!";

static {
System.out.println("Dog was initialized.");
}

public static int getAngerLevel() {
System.out.println("Angry was initialized.");
return 1;
}
}

public class Main {
public static void main(String[] args) throws Exception {
testClassInit();
}

public static void testClassInit() throws Exception {

//passive use of Angry
System.out.println(Angry.greeting);

//passive use of Dog
System.out.println(Dog.greeting);
}
}

如果可以的话,看官可以先猜一猜输出的结果是什么。
其实注释里已经提示得很清楚了,两个输出语句都是用被动方式调用的Angry和Dog的成员,因此无论是"Dog was initialized."还是"Angry was initialized."都不会被打印。换句话说,Angry和Dog都没有被初始化。
为什么类没有被初始化但它的静态final成员还是可以正确打印呢?实际上,像声明为“public static final String”的静态常量(注意,在接口里声明String效果是一样的,它实际上也是个final常量),它在编译的时候已经在使用它的所有地方用这个常量值直接替换了,也就是说,经过编译,实际上主运行类变成了这样:

public class Main {
public static void main(String[] args) throws Exception {
testClassInit();
}

public static void testClassInit() throws Exception {

//passive use of Angry
System.out.println("Grrrr!");

//passive use of Dog
System.out.println("Wong, Wong, Wong!");
}
}

自然地,它打印结果的时候就无需初始化甚至无需加载相关的类了。

实际中我的项目也有很多活生生的例子。比如,很多时候我们想要用一个类来管理项目中通用的常量,避免“魔数”的代码臭味,比如:

class ConstManager {
public static final String SIGN_DASH = "-";
public static final String SIGN_SLASH = "/";
public static final String SIGN_COMMA = ",";
......
}

然后我们会在项目的某处会以ConstManager.SIGN_COMMA 来代替逗号。这对我们管理项目是有益的,当需要修改一个符号的时候,只需要在一处修改再编译就行了。但实际上,编译后的class文件,在用到这些常量的代码块的位置,这些符号早就被替换成真实的","之类的值了。在项目运行的过程中,ConstManager甚至从来都没有机会被应用服务器加载。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值