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