Java程序运行在JVM之上,JVM的运行状况对于Java程序而言会产生很大的影响,因此掌握JVM中的关键机制对于编写稳定、高性能的JAVA程序至关重要。
JVM制定了Java类的加载、编译、执行、对象内存的分配和回收、线程以及锁机制,这些机制对Java程序的运行效果起到了重要的影响,当然,JVM涉及的不仅仅是上面这些机制,但在本章节中并不打算介绍所有JVM的机制,而是仅仅深入介绍其中的一些关键机制。
1 JVM类加载机制
JVM类加载机制是指将jar或目录中的.class文件加载到JVM,并形成Class对象的机制,应用可对Class对象进行实例化从而进行调用,JVM的类加载机制是动态的,因此可在运行时动态的加载外部的类、远程网络下载过来的字节码等,除了这个动态化的优点外,也可通过JVM的类加载机制来达到合理的类隔离的效果,例如Application Server中要避免两个应用的类互相干扰。
JVM将整个类加载过程划分为了三个步骤:装载、链接和初始化,其中初始化过程不是必须的,装载和链接过程完成后即以完成将二进制的字节码转为了Class对象,初始化只是将Class对象实例化,从而得到Object的过程,过程图示如下:
装载
装载过程负责找到二进制字节码并加载至JVM中,JVM通过类名、类所在的包名通过ClassLoader来完成类的加载,同样,也采用以上三个元素来标识一个被加载了的类:类名+包名+ClassLoader实例ID,类名为如下格式:
对于接口或非数组型的类,其名称即为类名,此种类型的类由所在的ClassLoader负责加载;
对于数组型的类,其名称为”[”+(基本类型或L+引用类型类名),例如byte[] bytes=new byte[512],这个bytes的类名为:[B;Object[] objects=new Object[10],objects的类名则为:[Ljava.lang.Object;,数组型的类其中的元素由所在的ClassLoader负责加载,但数组类则由JVM直接创建。
链接
链接过程负责对二进制字节码的格式进行校验、初始化装载类中的静态变量以及解析类中调用的接口、类。
二进制字节码的格式校验遵循Java Class File Format(具体请参见JVM规范)规范,如格式不符合则抛出VerifyError,校验过程中如碰到需要引用到其他的接口和类,也会进行加载,如加载过程失败,则会抛出NoClassDefFoundError。
在完成了校验后,JVM初始化类中的静态变量,并将其值赋为默认值。
最后一步为对类中的所有属性、方法进行验证,以确保其需要调用的属性、方法存在,以及具备相应的权限(例如public、private域权限等),这个阶段失败可能会造成NoSuchMethodError、NoSuchFieldError等错误信息。
初始化
初始化过程即为执行类中的静态初始化代码、构造器代码以及静态属性的初始化,在四种情况下初始化过程会被触发执行:
调用了new;
反射调用了类中的方法;
子类调用了初始化;
JVM启动过程中指定的初始化类。
在执行初始化过程之前,必须首先完成链接过程中的校验和准备阶段,解析阶段则不强制。
JVM的类加载通过ClassLoader以及其子类来完成,分为了Bootstrap ClassLoader、Extension ClassLoader、System ClassLoader以及User-Defined ClassLoader,这四种ClassLoader的关系图示如下:
Bootstrap ClassLoader
这是JVM的根ClassLoader,它是用C++实现的,JVM启动时初始化此ClassLoader,并由此ClassLoader完成$JAVA_HOME中jre/lib/rt.jar(Sun JDK的实现)中所有class文件的加载,这个jar中包含了java规范定义的所有接口以及实现。
Extension ClassLoader
JVM用此classloader来加载扩展功能的一些jar包,例如Sun JDK中这个目录下有dns工具jar包等,在Sun JDK中此ClassLoader对应的类名为ExtClassLoader。
System ClassLoader
JVM用此classloader来加载启动参数中指定的Classpath中的jar包以及目录,在Sun JDK中此ClassLoader对应的类名为AppClassLoader。
User-Defined ClassLoader
User-DefinedClassLoader是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会首先获取到调用Class.forName的对象所在的ClassLoader,并使用此ClassLoader来加载此名称的类,JVM为了保护加载、执行的类的安全,并不允许ClassLoader直接卸载加载了的类,只有等待JVM来卸载,在Sun JDK中,只有当ClassLoader对象没有引用时,此ClassLoader对象加载的类才会被卸载。
根据上面的描述,加上实际的应用中,JVM的类加载过程中会抛出这样那样的异常,在这些情况下对于各种异常产生的原因的掌握是最为重要的,下面来看看类加载方面的常见异常:
ClassNotFoundException
这是最常见的异常,产生这个异常的原因为在当前的ClassLoader中加载类时未找到类文件,对于位于System ClassLoader的类很容易判断,就是需要加载的类不在Classpath中,对于,对于位于User-Defined ClassLoader的类则麻烦些,需要具体查看下这个ClassLoader加载类的过程,从而判断此ClassLoader需要从什么位置加载到此类。
NoClassDefFoundError
这个异常较之ClassNotFoundException更难处理一些,造成此异常主要的原因是加载的类中引用到的另外的类不存在,例如需要加载A,然后A中调用了B,B不存在或当前ClassLoader没法加载B,就会抛出这个异常。
因此对于这个异常,需要先查看是加载哪个类时候报出的,然后需要确认这个类中引用的类是否存在于当前ClassLoader能加载到的位置。
LinkageError
这个异常在自定义ClassLoader的情况下更容易出现,主要原因是此类已经在ClassLoader加载过了,重复的加载会造成这个异常,要注意避免在并发的情况下出现这样的问题。
由于JVM的这个保护机制,使得在JVM中是没办法直接更新一个已经load了的Class的,只能是创建一个新的ClassLoader来加载更新了的Class,然后将新的请求转入到这个ClassLoader中来获取类,这也是JVM中不好实现动态更新的原因之一,而其他更多的原因是对象状态的复制、依赖的设置等等。
ClassCastException
这个异常有很多种原因,在JDK 5支持泛型后,相对少了一些,在这些原因中比较难查的一种是两个A对象是由不同的ClassLoader加载的情况,这个时候如果将其中的某个A对象造型成另外一个A对象,也会报出ClassCastException。
感觉本站内容不错,读后有收获?小额赞助,鼓励网站分享出更好的教程