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);
}
}