Java字节代码的表现形式是字节数组(byte[]), 而Java类在JVM中的表现形式是java. lang. Class类的对象。 一个Java类从字节代码到能够在JVM中被运用, 需要经过加载、链接和初始化这三个步骤。
这三个步骤中, 对开发人员直接可见的是Java类的加载, 通过运用Java类加载器(class loader)可以在运行时辰静态的加载一个Java类;而链接和初始化则是在运用Java类之前会发生的举措。 本文会详细引见Java类的加载、链接和初始化的进程。
Java类的加载
Java类的加载是由类加载器来完成的。 普通来说, 类加载器分成两类:启动类加载器(bootstrap)和用户自定义的类加载器(user-defined)。 两者的区别在于启动类加载器是由JVM的原生代码实现的, 而用户自定义的类加载器都继承自Java中的java. lang. ClassLoader类。 在用户自定义类加载器的部分, 普通JVM都会提供一些根本实现。 应用顺序的开发人员也可以依据需要编写自己的类加载器。 JVM中最常运用的是零碎类加载器(system), 它用来启动Java应用顺序的加载。 通过java. lang. ClassLoader的getSystemClassLoader()方法可以获取到该类加载器对象。
类加载器需要完成的最终功能是定义一个Java类, 即把Java字节代码转换成JVM中的java. lang. Class类的对象。 但是类加载的进程并不是这么简单。 Java类加载器有两个比较重要的特征:层次组织构造和代理形式。 层次组织构造指的是每个类加载器都有一个父类加载器, 通过getParent()方法可以获取到。 类加载器通过这种父亲-后代的方式组织在一起, 构成树状层次构造。 代理形式则指的是一个类加载器既可以自己完成Java类的定义任务, 也可以代理给其它的类加载器来完成。
由于代理形式的存在, 启动一个类的加载进程的类加载器和最终定义这个类的类加载器能够并不是一个。 前者称为初始类加载器, 然后者称为定义类加载器。 两者的关联在于:一个Java类的定义类加载器是该类所导入的其它Java类的初始类加载器。 比方类A通过import导入了类 B, 那么由类A的定义类加载器负责启动类B的加载进程。
普通的类加载器在尝试自己去加载某个Java类之前, 会首先代理给其父类加载器。 当父类加载器找不到的时候, 才会尝试自己加载。 这个逻辑是封装在java. lang. ClassLoader类的loadClass()方法中的。 普通来说, 父类优先的战略就足够好了。 在某些状况下, 能够需要采取相反的战略, 即先尝试自己加载, 找不到的时候再代理给父类加载器。
这种做法在Java的Web容器中比较常见, 也是Servlet规范推荐的做法。 比方, Apache Tomcat为每个Web应用都提供一个独立的类加载器, 运用的就是自己优先加载的战略。 IBM WebSphere Application Server则允许Web应用选择类加载器运用的战略。
类加载器的一个重要用途是在JVM中为相同名称的Java类创立隔离空间。 在JVM中, 判断两个类是否相同, 不仅是依据该类的二进制名称, 还需要依据两个类的定义类加载器。 只有两者完全一样, 才认为两个类的是相同的。 因此, 即便是异样的Java字节代码, 被两个不同的类加载器定义之后, 所失掉的Java类也是不同的。 假如试图在两个类的对象之间停止赋值操作, 会抛出java. lang. ClassCastException。
这个特性为异样名称的Java类在JVM中共存创造了条件。 在实际的应用中, 能够会要求同一名称的Java类的不同版本在JVM中可以同时存在。 通过类加载器就可以满足这种需求。 这种技术在OSGi中失掉了广泛的应用。
Java类的链接
Java类的链接指的是将Java类的二进制代码合并到JVM的运行状态之中的进程。 在链接之前, 这个类必需被成功加载。 类的链接包括验证、准备和解析等几个步骤。 验证是用来确保Java类的二进制表示在构造上是完全正确的。 假如验证进程出现错误的话, 会抛出java. lang. VerifyError错误。 准备进程则是创立Java类中的静态域, 并将这些域的值设为默许值。 准备进程并不会执行代码。
在一个Java类中会包含对其它类或接口的形式援用, 包括它的父类、所实现的接口、方法的形式参数和前往值的Java类等。 解析的进程就是确保这些被援用的类能被正确的找到。 解析的进程能够会导致其它的Java类被加载。
不同的JVM实现能够选择不同的解析战略。 一种做法是在链接的时候, 就递归的把所有依赖的形式援用都停止解析。 而另外的做规律能够是只在一个形式援用真正需要的时候才停止解析。 也就是说假如一个Java类只是被援用了, 但是并没有被真正用到, 那么这个类有能够就不会被解析。 思索上面的代码:
- public class LinkTest . . . {
- public static void main(String[] args) . . . {
- ToBeLinked toBeLinked = null;
- System. out. println(Test link. );
- }
- }
类 LinkTest援用了类ToBeLinked, 但是并没有真正运用它, 只是声明了一个变量, 并没有创立该类的实例或是访问其中的静态域。 在 Oracle的JDK 6中, 假如把编译好的ToBeLinked的Java字节代码删除之后, 再运行LinkTest, 顺序不会抛出错误。 这是由于ToBeLinked类没有被真正用到, 而Oracle的JDK 6所采用的链接战略使得ToBeLinked类不会被加载, 因此也不会发现ToBeLinked的Java字节代码实际上是不存在的。 假如把代码改成ToBeLinked toBeLinked = new ToBeLinked();之后, 再按照相同的方法运行, 就会抛出异常了。 由于这个时候ToBeLinked这个类被真正运用到了, 会需要加载这个类。
Java类的初始化
当一个Java类第一次被真正运用到的时候, JVM会停止该类的初始化操作。 初始化进程的主要操作是执行静态代码块和初始化静态域。 在一个类被初始化之前, 它的直接父类也需要被初始化。 但是, 一个接口的初始化, 不会引起其父接口的初始化。 在初始化的时候, 会按照源代码中从上到下的顺序依次执行静态代码块和初始化静态域。 思索上面的代码:
- public class StaticTest . . . {
- public static int X = 10;
- public static void main(String[] args) . . . {
- System. out. println(Y); //输入60
- }
- static . . . {
- X = 30;
- }
- public static int Y = X * 2;
- }
在上面的代码中, 在初始化的时候, 静态域的初始化和静态代码块的执行会从上到下依次执行。 因此变量X的值首先初始化成10, 后来又被赋值成30;而变量Y的值则被初始化成60。
Java类和接口的初始化只有在特定的机遇才会发生, 这些机遇包括:
创立一个Java类的实例。 如:MyClass obj = new MyClass()
调用一个Java类中的静态方法。 如:MyClass. sayHello()
在顶层Java类中执行assert语句。
通过Java反射API也能够形成类和接口的初始化。 需要注意的是, 当访问一个Java类或接口中的静态域的时候, 只有真正声明这个域的类或接口才会被初始化。 思索上面的代码:
- class B . . . {
- static int value = 100;
- static . . . {
- System. out. println(Class B is initialized. ); //输入
- }
- }
- class A extends B . . . {
- static . . . {
- System. out. println(Class A is initialized. ); //不会输入
- }
- }
- public class InitTest . . . {
- public static void main(String[] args) . . . {
- }
创立自己的类加载器
在 Java应用开发进程中, 能够会需要创立应用自己的类加载器。 典型的场景包括实现特定的Java字节代码查找方式、对字节代码停止加密/解密以及实现同名 Java类的隔离等。 创立自己的类加载器并不是一件复杂的事情, 只需要继承自java. lang. ClassLoader类并覆写对应的方法即可。 java. lang. ClassLoader中提供的方法有不少, 上面引见几个创立类加载器时需要思索的:
- defineClass():这个方法用来完成从Java字节代码的字节数组到java. lang. Class的转换。 这个方法是不能被覆写的, 普通是用原生代码来实现的。
- findLoadedClass():这个方法用来依据名称查找已经加载过的Java类。 一个类加载器不会重复加载同一名称的类。
- findClass():这个方法用来依据名称查找并加载Java类。
- loadClass():这个方法用来依据名称加载Java类。
- resolveClass():这个方法用来链接一个Java类。
这里比较 容易混淆的是findClass()方法和loadClass()方法的作用。 前面提到过, 在Java类的链接进程中, 会需要对Java类停止解析, 而解析能够会导致以后Java类所援用的其它Java类被加载。 在这个时候, JVM就是通过调用以后类的定义类加载器的loadClass()方法来加载其它类的。 findClass()方规律是应用创立的类加载器的扩展点。 应用自己的类加载器应该覆写findClass()方法来添加自定义的类加载逻辑。 loadClass()方法的默许实现会负责调用findClass()方法。
前面提到, 类加载器的代理形式默许运用的是父类优先的战略。 这个战略的实现是封装在loadClass()方法中的。 假如希望修改此战略, 就需要覆写loadClass()方法。
上面的代码给出了自定义的类加载的常见实现形式:
- public class MyClassLoader extends ClassLoader . . . {
- protected Class findClass(String name) throws ClassNotFoundException . . . {
- byte[] b = null; //查找或生成Java类的字节代码
- return defineClass(name, b, 0, b. length);
- }
- }
希望通过以上关于java中类的加载、链接和初始化三方面的介绍,能够给你带来帮助。