一步一步学习Java虚拟机(二) 加载字节码文件与类装载器

Feb 10, 2017


在C/C++语言中,源码编译后需要连接过程将独立编译的文件以及共享库连接起来成为一个可以执行的程序。Java语言是不同的,其不会生成一个可以执行的文件。

那么谁来负责Java字节码文件的解释和连接过程并执行呢,当然是Java虚拟机了。不同的Java虚拟机可能使用不同的方式来加载类,但基本规则是:只有在需要的时候才加载类。

概括来说,触发加载class的情况有:

  • new调用时,如SomeClass f = new SomeClass();

  • 静态引用时,如System.out;

需要注意的是,class被加载并不意味着类被初始化. 在执行代码之前,逻辑上有三个阶段,加载,链接和初始化。

Java字节码加载
Java字节码加载

加载字节码文件

加载

加载就是通过指定的类全限定名,获取此类的二进制字节流,然后将此二进制字节流转化为方法区的数据结构,如class字节文件中大端存储,但在虚拟机中可以根据底层计算器处理器的要求转换为小端存储。

方法区是什么呢?

方法区是系统分配的一个内存逻辑区域,是用来存储类型信息的(类型信息可理解为类的描述信息)。这些类的信息包括的一些信息如:

  • 常量池

  • 类的全限定名,即全路径名,如java/lang/Object

  • 字段、方法信息、类变量信息

  • 装载该类的装载器的引用(classLoader),虚拟机使用其做动态连接,当一个类型引用另一个类型时,虚拟机将从同一个类装载器去加载参考类型。

  • 类型引用(class),类型被加载后都会创建一个java.lang.Class的实例,方法区提供了一些获取这个实例的方法,如Class的静态方法public static Class forName(String className);或Object类提供的public final Class getClass();

类变量被所有的类实例共享,甚至没有类实例的存在也能被访问。在Java虚拟机使用一个class时,它必须在方法去为class中定义每一个非final的类变量分配内存。 但final的类变量与非final的类变量有些区别,非final类变量存储在声明它的类的方法区中,final类变量的备份都存储在每一个使用它的类的方法区中。

方法区的一些特点是:

  • 线程共享,那么需要考虑线程安全。如果两个线程都试图加载Lava类,则只有一个线程允许加载这个类,另一个类必须等待。

  • 内存大小不必是固定的,也不必连续的,虚拟机可以扩展方法区的大小。

  • 方法区也可被垃圾收集,因为Java程序可以通过用户自定义的加载器动态扩展,当一个类没有任何引用时,Java虚拟机需要能够回收方法去的内存。

链接

验证

验证是为了确保Class文件中的字节流符合虚拟机的要求,并且不会危害虚拟机的安全。

准备

Java虚拟机会在准备阶段给类变量分配内存并设置类变量的初始值。注意,这里只是赋值初始值,比如在 Public static int value = 123 这句话中,在执行准备阶段的时候,会给value分配内存并设置初始值0, 而不是我们想象中的123。

解析(Optional)

解析阶段是虚拟机将常量池中的符号引用转换为直接引用的过程。对于一些还没有被用到的引用类型,其解析可能会推迟到使用的时候。

初始化

类初始化阶段是类加载过程的最后阶段。在这个阶段,java虚拟机才真正开始执行类定义中的java程序代码。

  • 在遇到new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类还没有进行过初始化,则需要先触发其初始化。常见的触发这些指令的Java代码场景有:

    • new关键字实例化对象时

    • 读取或设置一个类变量(static)时。final类变量除外,因为编译期已经把值放入常量池

    • 调用一个类的静态方法时

  • 使用 Java.lang.refect 包的方法对类进行反射调用时,如果类还没有进行过初始化,则需要先触发其初始化。

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

  • 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先执行该主类。

对于类静态字段,只有直接定义这个字段的类才会被初始化,因此,通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。final类变量不会触发任何类的初始化。

通过数组定义来引用类,不会触发类的初始化,但是其执行了对数组引用类型的初初始化,而该数组中的元素仅仅包含一个对类的引用,并没有对其进行初始化。

类和接口初始化的区别: 当一个类在初始化时,要求其父类全部已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量),才会初始化该父接口。接口中定义的成员变量(实际上是 static final 修饰的全局常量)。

类装载器

使用java -verbose:class HelloWorld可以查看Java虚拟机加载系统库以及类的顺序。如果HelloWorld中有一个成员变量是TestClass如private TestClass TestClass;,但由于TestClass没有被使用,其实不会被加载的。但是,如果private TestClass TestClass = new TestClass();则TestClass会被加载。

[Opened /Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Object from /Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.io.Serializable from /Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Comparable from /Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.CharSequence from /Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.String from /Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/rt.jar]
....
[Loaded HelloWorld from file:/Users/qzhou/project/training/jvm/out/production/jvm/]
[Loaded java.lang.Void from /Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/rt.jar]

在加载HelloWorld之前,有很多rt.jar包中的类被加载进来。

rt.jar是怎么加载进来的呢?

Bootstrap ClassLoader

C++语言实现的,其作用是加载 /lib 目录中的文件,并且该类加载器只加载特定名称的文件(如rt.jar、resources.jar、charsets.jar等),而不是该目录下所有的文件。

Extension ClassLoader

负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/或-Djava.ext.dirs指定目录下的jar包。

Application ClassLoader

负责记载classpath中指定的jar包及目录中class。

自定义装载器

属于应用程序根据自身需要自定义的ClassLoader,如tomcat根据j2ee规范实现了自己的classloader。自定义一个装载器的步骤:

  • 继承java.lang.ClassLoader

  • 重写父类的findClass方法,JDK已经在loadClass方法中帮我们实现了ClassLoader搜索类的算法,当在loadClass方法中搜索不到类时,loadClass方法就会调用findClass方法来搜索类,所以我们只需重写该方法即可。

这4种装载器的之间的关系如下图所示:

Java类装载器
Java类装载器

ClassLoader使用的是双亲委托模型来搜索类的,每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它ClassLoader实例的的父类加载器。

为什么要使用双亲委托模型呢?

  • 避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。

  • 避免自定义类来动态替代java核心api中定义的类型。

JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。

参考资料