论坛首页 Java企业应用论坛

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

浏览 5736 次
精华帖 (1) :: 良好帖 (2) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2009-11-08  
    最近在阅读 《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甚至从来都没有机会被应用服务器加载。
   发表时间:2009-11-09  
支持一下,加深了对jvm的理解..
0 请登录后投票
   发表时间:2009-11-09  
讲的很不错,谢谢分享。
0 请登录后投票
   发表时间:2009-11-09  
LZ,请问《Inside the JVM》在哪买的?我上次找遍了都没找到。
0 请登录后投票
   发表时间:2009-11-09  
treblesoftware 写道
LZ,请问《Inside the JVM》在哪买的?我上次找遍了都没找到。

绝版了,买二手书,或者下电子书。
0 请登录后投票
   发表时间:2009-11-09  
不错!学习了
《Inside the JVM》怎么不出新版本呀
0 请登录后投票
   发表时间:2009-11-10  
期待楼主更多的学习笔记!
0 请登录后投票
   发表时间:2009-11-10  
在接口中声明的变量,默认是static final的,学习了
0 请登录后投票
   发表时间:2009-11-10  
finux 写道
不错!学习了
《Inside the JVM》怎么不出新版本呀


我也希望出新版啊,JDK1.5/6 改变还是很大的,绝对有必要再出一版。
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics