snake-hand

JVM类加载过程学习总结

先不说JVM类加载的原理,先看实例:

NormalTest类,包含了一个静态代码块,执行的任务就是打印一句话。

/**
 * 在正常类加载条件下,看静态代码块是否会执行
 * @author jianying.wcj
 * @date 2013-6-21
 */
public class NormalTest {

    static {
        System.out.println("hello world!");
    }
}

TestStatic类, 有三行代码,其中两行被注释,测试过程是,在执行其中任意一行代码的时候,注释掉其余两行。

public class TestStatic {

    public static void main(String[] args) throws ClassNotFoundException {
        /**
         * 实验1
         */
        Class.forName("NormalTest");
        /**
         * 实验2
         */
        //NormalTest nt = new NormalTest();
        /**
         * 实验3
         */
        //TestStatic.class.getClassLoader().loadClass("NormalTest");
    }
}

测试的输出的结果是: 在执行 Class.forName("NormalTest")的时候,输出了“Hello world!”,在执行NormalTest nt = new NormalTest();的时候也输出了“Hello world!” 但是在执行代码TestStatic.class.getClassLoader().loadClass("NormalTest");却没有输出“Hello world!” 下面分析一下原因或者说看看这三行代码的内部实现的异同。 以上三行代码其实在执行的时候都会去加载NormalTest.class,这里可以不准确的说以上三行代码是三种加载类的方式。从实验的输出来看,可以确定实验1 和实验2 在加载NormalTest的时候执行了静态代码块,而实验3 直接调用ClassLoader来loadclass的时候没有执行静态代码块。执行静态代码块的过程其实就是初始化类的过程,话说到这,说白了,前两种方式加载类的时候对类进行了初始化,而第三种没有,那么看看部分代码的实现。

 public static Class<?> forName(String className)  throws ClassNotFoundException {
     return forName0(className, true, ClassLoader.getCallerClassLoader());
}

上面这段代码是Class.forName()的定义,实现里直接调用了forName0(),forName0的方法签名是:

 private static native Class forName0(String name, boolean initialize,
                                ClassLoader loader)throws ClassNotFoundException;

这是个本地方法,本地方法的C++实现先不研究(抛砖引玉一下,谁有好的研究可以分享一下),这个本地方法第二个参数是initialize,这个参数的true或false就是告诉虚拟机,在根据类的全限定名name加载类的时候,要对类进行初始化。在forName调用forName0的时候,看以看到initialize设置成了true,所以我们的类的静态代码块就被执行了(类被初始化了)。 实验2在new一个对象的时候,也会在加载类的时候触发其初始化方法,这个的实现在虚拟机实现的C++代码里,JVM虚拟机规范指出,在执行new指令创建一个对象的时候要对加载的类进行初始化(《深入理解java虚拟机》第七章有说)。实验3在执行ClassLoader.load一个class的时候属于被动加载类,根据虚拟机规范不会对类进行初始化。对于new和ClassLoader.load加载类的方式,在java代码层面已经看不到是否需要对类进行初始化的标志了,内部实现在JVM的C++实现中(C++实现逻辑待哥们的水平提高提高再做分析总结)。

上面都是根据实例的总结,下面来点官方的资料学习总结。

  1. 首先看下关于类加载的时候是否初始化的虚拟机规范:

    虚拟机规范则是严格规定了有且只有四种情况必须立即对类进行初始化(而加载、验证、准备自然需要在此之前开始):

    1) 遇到new 、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行初始化,则需要先触发其初始化。生成这4条指令的最常见的java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。

    2) 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

    3) 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

    4) 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

    可见:当满足上述4中条件之一的任何一种情况都会执行类的静态代码块,而除上述4中情况外,则不会对类的初始化(注意加粗的有且只有4个字)。

2.类从加载到卸载,整个过程可以描述为7个阶段:

加载:虚拟机需要完成以下三件事情:

  • a) 通过一个类的权限定名来获取此类定义的二进制字节流。
  • b) 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
  • c) 在java堆中生成一个代表这个类的java.lang.Class对象,作为访问方法区的入口

验证:

  • 连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。(备注:虽然在java语言是相对安全的,但是在字节码层面,上述java代码无法做到的事情都是可以实现的,至少在语义上是可以表达出来的。所以对字节流进行验证是相当必要的)

准备:

  • 准备阶段是正式为变量分配内存并设置变量初始值的阶段,这些内存都将在方法区中进行分配。

解析:

  • 解析过程就是在类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用替换成直接引用的过程。(至于什么是符号引用,什么是直接引用,参考这篇博客:http://blog.csdn.net/lantian0802/article/details/9152657

(ps:验证、准备、解析统称为连接)

初始化:

  • 为类的静态变量赋予正确的初始值,当然也包括执行静态代码块的内容。

ps: 直接引用和符号引用的一个生动的例子: 那么什么是符号引用,什么又是直接引用呢?我们来举个例子:我们要找一个人,我们现有的信息是这个人的身份证号是1234567890。只有这个信息我们显然找不到这个人,但是通过*局的身份系统,我们输入1234567890这个号之后,就会得到它的全部信息:比如湖北省武汉市武汉大学张三,通过这个信息我们就能找到这个人了。这里,123456790就好比是一个符号引用,而湖北省武汉市武汉大学张三就是直接引用。在内存中也是一样,比如我们要在内存中找一个类里面的一个叫做show的方法,显然是找不到。但是在解析阶段,jvm就会把show这个名字转换为指向方法区的的一块内存地址,比如c17164,通过c17164就可以找到show这个方法具体分配在内存的哪一个区域了。这里show就是符号引用,而c17164就是直接引用。在解析阶段,jvm会将所有的类或接口名、字段名、方法名转换为具体的内存地址。

分类:

技术点:

相关文章: