Java类的加载:
Java类的加载是由类加载器来完成的。一般来说,类加载器分成两类:启动类加载器(bootstrap)和用户自定义的类加载器(userdefined)。两者的区别在于启动类加载器是由JVM的原生代码实现的,而用户自定义的类加载器都继承自Java中的java.lang.ClassLoader类。在用户自定义类加载器的部分,一般JVM都会提供一些基本实现。应用程序的开发人员可以根据需要编写自己的类加载器。JVM中最常用的是系统类加载器(system),它用来启动Java应用程序的加载。通过java.lang.ClassLoader的getSystemClassLoader()方法可以获取该类的加载器对象。
类加载器需要完成的最终功能是定义一个Java类,即把Java字节代码转化成JVM中的java.lang.Class类的对象。但是类加载的过程中并不是这么简单。Java类加载器有两个比较重要的特征:层次组织结构和代理模式。层次组织结构指的是每个类加载器都有一个父类加载器,通过getParent()方法可以获取到。类加载器通过父亲-后代的方式组织在 一起,形成树状层次结构。
代理模式则指的是一个类加载器既可以完成Java类的定义工作,也可以代理给其它的类加载器来完成。由于完成Java类的定义工作,也可以代理给其它的类加载器来完成。由于代理模式的存在,启动一个类的加载过程的类加载器和最终定义这个类的类加载器可能并不是一个。前者称为初始类加载器,而后者称为定义类加载器。两者的关联在于:一个Java类的定义加载器是该类所导入的其它java类的初始类加载器。比如类A通过import导入了类B,那么由类A的定义类加载器负责启动类B的加载过程。
Java类得链接指的是将Java类得二进制代码合并到JVM的运行状态之中的过程,在链接之前,这个类必须被成功加载。类得链接包括验证、准备和解析等几个步骤。验证是用来确保Java类得二进制表示在结构上市完全正确的。如果验证过程出现错误的话,会抛出java.lang.VerifyError错误。准备过程则是创建Java类中静态域,并将这些域的值设为默认值。准备过程并不会执行代码。在一个Java类中会包含对其他类或接口的形式引用,包括它的父类、所实现的接口、方法的形式参数和返回值的Java类等。解析过程就是确保这些被引用的类能被正确的找到。解析的过程可能会导致其它的Java类被加载。
不同的JVM实现可能选择不同的解析策略。一种做法是在链接的时候,就递归的把所有依赖的形式引进都进行解析。而另外的做法则是可能是只在一个形式引用真正需要的时候才进行解析。也就是说如果一个Java类只是被引用了,但是并没有真正用到,那么这个类可能就不会被解析。考虑下面的代码:
public class LinkTest{
public static void main(String[] args){
ToBeLined toBeLinked = null;
System.out.println("Test link.");
}
}
类LinkTest引用了类ToBeLinked,但是并没有真正使用它,只是声明了一个变量,并没有创建该类得实例或是访问其中的静态域。在Oracle的JDK6中,如果把编译好的ToBeLinked的Java字节代码删除之后,再运行LinkTest,程序不会抛出错误。这是因为ToBeLinked类没有被真正用到,而Oracle的JDK6所采用的链接策略使得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类或接口中声明的静态域赋值。如
MyClass.value = 10;
。访问java类或接口中声明的静态域,并且该域不是常值变量。如
int value = MyClass.value;
。在顶层Java类中执行assert语句。
通过Java反射AP也可能造成类和接口的初始化。需要注意的是,当访问一个Java类或接口中的静态域的时候,只有真正声明这个域的类或接口才会被初始化。考虑下面的代码:
Class B{
static int value = 100;
static {
System.out.println("Class B is initalized.");//输出
}
}
Class A extends B{
static{
System.out.println("Class A is initalized.");//不会输出
}
}
public lass InitTest{
public static void main(String[] args){
System.out.println(A.value);//输出100
}
}
在上述代码中,类initTest通过A.value引用了类B中声明的静态域value。由于value是在类B中声明的,只有类B会被初始化,而类A则不会被初始化。
创建自己的类加载器
在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);
}
}