《分布式JAVA应用 基础与实践》 第三章 3.1 Java代码的执行机制(一)
Java程序运行在JVM之上,JVM的运行状况对于Java程序而言会产生很大的影响,因此,掌握JVM中的关键机制对于编写稳定、高性能的Java程序至关重要。
JVM规范 定义的标准结构如图3.1所示。
??从上可见,class文件是个完整的自描述文件,字节码在其中只占了很小的部分,源码编译为class文件后,即可放入jvm中执行。执行时jvm首先要做的是装载class文件,这个机制通常称为类加载机制。
3.1.2? 类加载机制
类加载机制是指.class文件加载到JVM,并形成Class对象的机制,之后应用就可对Class对象进行实例化并调用,类加载机制可在运行时动态加载外部的类、远程网络下载过来的class文件等。除了该动态化的优点外,还可通过JVM的类加载机制来达到类隔离的效果,例如Application Server中通常要避免两个应用的类互相干扰。
JVM将类加载过程划分为三个步骤:装载、链接和初始化。装载和链接过程完成后,即将二进制的字节码转换为Class对象;初始化过程不是加载类时必须触发的,但最迟必须在初次主动使用对象前执行,其所作的动作为给静态变量赋值、调用<clinit>()等。
整个过程如图3.3所示。
1. 装载(Load)
装载过程负责找到二进制字节码并加载至JVM中,JVM通过类的全限定名(com.bluedavy. HelloWorld)及类加载器(ClassLoaderA实例)完成类的加载,同样,也采用以上两个元素来标识一个被加载了的类:类的全限定名+ClassLoader实例ID。类名的命名方式如下:
对于接口或非数组型的类,其名称即为类名,此种类型的类由所在的ClassLoader负责加载;
??图3.4? Sun JDK ClassLoader继承关系?
1. Bootstrap ClassLoader
Sun JDK采用C++实现了此类,此类并非ClassLoader的子类,在代码中没有办法拿到这个对象,Sun JDK启动时会初始化此ClassLoader,并由ClassLoader完成$JAVA_HOME中jre/lib/rt.jar里所有class文件的加载,jar中包含了Java规范定义的所有接口及实现。
2. Extension ClassLoader
JVM用此ClassLoader来加载扩展功能的一些jar包,例如Sun JDK中目录下有dns工具jar包等,在Sun JDK中ClassLoader对应的类名为ExtClassLoader。
3. System ClassLoader
JVM用此ClassLoader来加载启动参数中指定的Classpath中的jar包及目录,在Sun JDK中ClassLoader对应的类名为AppClassLoader。
例如一段这样的代码:
??? public class ClassLoaderDemo {?
??????? public static void main(String[] args) throws Exception{?
??????????? System.out.println(ClassLoaderDemo.class.getClassLoader());?
??????????? System.out.println(ClassLoaderDemo.class.getClassLoader().getParent());?
??????? System.out.println(ClassLoaderDemo.class.getClassLoader().getParent().getParent());?
??????? }?
??? }
执行后显示的信息类似如下:
??? (sun.misc.Launcher$AppClassLoader)?
??? (sun.misc.Launcher$ExtClassLoader)?
??? null
按照上面的描述,就可看到典型的System ClassLoader、Extension ClassLoader,而由于Bootstrap ClassLoader并不是Java中的ClassLoader,因此Extension ClassLoader的parent为null。
4. User-Defined ClassLoader
User-Defined ClassLoader是Java开发人员继承ClassLoader抽象类自行实现的ClassLoader,基于自定义的ClassLoader可用于加载非Classpath中(例如从网络上下载的jar或二进制)的jar及目录、还可以在加载之前对class文件做一些动作,例如解密等。
JVM的ClassLoader采用的是树形结构,除BootstrapClassLoader外,其他的ClassLoader都会有parent ClassLoader,User-Defined ClassLoader默认的parent ClassLoader为System ClassLoader。加载类时通常按照树形结构的原则来进行,也就是说,首先应从parent ClassLoader中尝试进行加载,当parent中无法加载时,应再尝试从System ClassLoader中进行加载,System ClassLoader同样遵循此原则,在找不到的情况下会自动从其parent ClassLoader中进行加载。值得注意的是,由于JVM是采用类名加Classloader的实例来作为Class加载的判断的,因此加载时不采用上面的顺序也是可以的,例如加载时不去parent ClassLoader中寻找,而只在当前的ClassLoader中寻找,会造成树上多个不同的ClassLoader中都加载了某Class,并且这些Class的实例对象都不相同,JVM会保证同一个ClassLoader实例对象中只能加载一次同样名称的Class,因此可借助此来实现类隔离的需求,但有时也会带来困惑,例如ClassCastException。因此在加载类的顺序上要根据需求合理把握,尽量保证从根到最下层的ClassLoader上的Class只加载了一次。
ClassLoader抽象类提供了几个关键的方法:
loadClass
此方法负责加载指定名字的类,ClassLoader的实现方法为先从已经加载的类中寻找,如没有,则继续从parent ClassLoader中寻找;如果仍然没找到,则从System ClassLoader中寻找,最后再调用findClass方法来寻找;如果要改变类的加载顺序,则可覆盖此方法;如果加载顺序相同,则可通过覆盖findClass来做特殊的处理,例如解密、固定路径寻找等。当通过整个寻找类的过程仍然未获取Class对象时,则抛出ClassNotFoundException。
如果类需要resolve,则调用resolveClass进行链接。
findLoadedClass
此方法负责从当前ClassLoader实例对象的缓存中寻找已加载的类,调用的为native的方法。
findClass
此方法直接抛出ClassNotFoundException,因此要通过覆盖loadClass或此方法来以自定义的方式加载相应的类。
findSystemClass
此方法负责从System ClassLoader中寻找类,如未找到,则继续从Bootstrap ClassLoader中寻找,如果仍然未找到,则返回null。
defineClass
此方法负责将二进制的字节码转换为Class对象,这个方法对于自定义加载类而言非常重要。如果二进制的字节码的格式不符合JVM Class文件的格式,则抛出ClassFormatError;如果生成的类名和二进制字节码中的不同,则抛出NoClassDefFoundError;如果加载的class是受保护的、采用不同签名的,或者类名是以java.开头的,则抛出SecurityException;如果加载的class在此ClassLoader中已加载,则抛出LinkageError。
resolveClass
此方法负责完成Class对象的链接,如果链接过,则会直接返回。
当Java开发人员调用Class.forName来获取一个对应名称的Class对象时,JVM会从方法栈上寻找第一个ClassLoader,通常也就是执行Class.forName所在类的ClassLoader,并使用此ClassLoader来加载此名称的类。JVM为了保护加载、执行的类的安全,它不允许ClassLoader直接卸载加载了的类,只有JVM才能卸载,在Sun JDK中,只有当ClassLoader对象没有引用时,此ClassLoader对象加载的类才会被卸载。
根据上面的描述,在实际的应用中,JVM类加载过程会抛出这样那样的异常,这些情况下掌握各种异常产生的原因是最为重要的,下面来看类加载方面的常见异常。
1. ClassNotFoundException
这是最常见的异常,产生这个异常的原因为在当前的ClassLoader中加载类时未找到类文件,对位于System ClassLoader的类很容易判断,只要加载的类不在Classpath中,而对位于User-Defined ClassLoader的类则麻烦些,要具体查看这个ClassLoader加载类的过程,才能判断此ClassLoader要从什么位置加载到此类。
例如直接在代码中执行Class.forName("com.bluedavy.A"),而当前类的classloader下根本就没有该类所在的jar或没有该class文件,就会抛出ClassNotFoundException。
2. NoClassDefFoundError
该异常较之ClassNotFoundException更难处理一些,造成此异常的主要原因是加载的类中引用到的另外的类不存在,例如要加载A,而A中调用了B,B不存在或当前ClassLoader没法加载B,就会抛出这个异常。
例如有一段这样的代码:
public class A{?
private B b = new B();?
}
当采用Class.forName加载A时,虽能找到A.class,但此时B.class不存在,则会抛出NoClassDefFoundError。
因此,对于这个异常,须先查看是加载哪个类时报出的,然后再确认该类中引用的类是否存在于当前ClassLoader能加载到的位置。
3. LinkageError
该异常在自定义ClassLoader的情况下更容易出现,主要原因是此类已经在ClassLoader加载过了,重复地加载会造成该异常,因此要注意避免在并发的情况下出现这样的问题。
由于JVM的这个保护机制,使得在JVM中没办法直接更新一个已经load的Class,只能创建一个新的ClassLoader来加载更新的Class,然后将新的请求转入该ClassLoader中来获取类,这也是JVM中不好实现动态更新的原因之一,而其他更多的原因是对象状态的复制、依赖的设置等。
4. ClassCastException
该异常有多种原因,在JDK 5支持泛型后,合理使用泛型可相对减少此异常的触发。这些原因中比较难查的是两个A对象由不同的ClassLoader加载的情况,这时如果将其中某个A对象造型成另外一个A对象,也会报出ClassCastException。