一般可以采用如下3种方法实现异常处理。
(2)使用静态的异常处理表,运行时查找该表,以搜寻异常处理句柄。
(3)使用有两个返回值的函数。
其中C++和Ada的异常处理使用了第1种方法,第2种方法较第1种可显著提高异常处理的效率,它在C++及Java语言中得到了应用。在该方式下,编译器为每一方法提供异常处理表,由于有了异常处理表,就没有必要再为每一异常处理语句建立数据链表。当异常产生时,异常处理程序可直接查询异常表,快速定位异常处理句柄,若没找到,则将异常传播给上层方法,在它的异常查询表中继续查询。
第3种方法在某些Java虚拟机的及时编译设计时被采用。该处理形式虽然简单,但是编译过程较为复杂。由于方法间没有共享方法异常表,而使编译后的代码冗长,该方法适用于异常发生频繁的程序。
1.解释执行时的异常处理
解释器的异常处理实现较为简单,由于对异常处理的编译是直接针对字节码的,因此异常的查找和传播都较及时编译方便得多。
Java编译程序为有异常处理句柄的方法生成异常处理表,因此我们在实现时可直接利用这一信息生成方法的异常表,方法异常表的数据结构如下。
当产生异常时,异常处理程序根据产生该异常的指令位置及其产生的异常类型,在方法异常表中查找符合这两个条件的异常处理句柄。
Java异常处理机制规定没有处理的异常要沿方法调用栈传播给上层调用方法,因此在程序的执行时我们应建立方法调用关系链表,以实现异常在方法调用栈中的逆向传播。方法调用链表的数据结构应能恢复方法的运行环境、并记录方法的运行状态。方法调用链表的数据结构如下:
当异常产生时,异常处理程序根据pc值及产生的异常类型,在该方法的异常处理表(由meth获得)中查询对应的异常处理句柄,若没找到则异常传播给该方法的调用者(prev指向的方法)。异常处理程序进行同样的查找,当找到对应的异常处理句柄时,异常处理程序恢复该句柄所在方法解释执行时的运行环境,并将该方法的字节码指令指针指向异常处理句柄,运行环境被恢复后解释器执行该异常处理句柄,之后,解释器继续执行其下面的其他语句。
我们一般用操作系统提供的库函数setjmp及longjmp来实现运行环境的保存与恢复。在方法调用链表数据结构中,lock为该方法所属对象的对象锁,若该方法为静态方法,则lock为其所属类的类锁。该域是为同步方法设计的,对于同步方法异常处理完成后,应释放该对象(类)锁。
异常处理程序在进行异常处理时,将异常分为两大类,即内部异常与外部异常。内部异常主要指虚拟机在运行时检测到的异常,它们主要为Error及RuntimeException。例如虚拟机在类加载时需要进行类文件格式的检查,若类文件格式错误,虚拟机将产生ClassFormatError异常;虚拟机执行字节码指令时自动对数组下标进行检查,当数组范围越界时,虚拟机会产生ArrayIndexOutOfBoundsException。当虚拟机检测到异常时,其触发异常程序的执行。外部异常一般指用户自定义的异常,字节码指令athrow产生外部异常并触发异常处理程序的执行,该异常存放在操作数栈的栈顶,如果在当前方法中找到了异常处理句柄,athrow指令抛弃操作数栈上的所有数据,然后将抛出的异常对象压入栈,如果在当前方法中没找到异常处理句柄,则异常沿方法调用栈传播,直到找到处理该异常的方法。此时,处理该异常的方法的操作数栈被清除,并将异常对象压入到这个空的操作数栈,该传播过程中的其他方法栈都将被抛弃。不管内部异常还是外部异常,异常处理句柄都得由用户提供。
对于内部异常,有两个异常是比较特殊的,它们是NullPointerException(空指针引用)、ArithmeticException(浮点溢出),这两个异常可由硬件检测到,操作系统提供这两个异常的中断信号,因此在异常初始化时,可由库函数signal设置这两个异常的服务程序。(www.daowen.com)
在对内部异常进行处理时,异常处理程序除查找异常处理句柄并执行该句柄外,异常处理程序还应提供方法调用过程的全部信息,该信息包括方法调用栈中所有方法的当前字节码指令指针所在的类名、方法名及所处的源文件行号。该过程同样得逆向遍历方法调用链表的全部数据单元,由方法调用表数据结构中的pc值在方法的行号表中查找该pc值所对应的源文件行号。行号表内记录着字节码生成时字节码指令与源文件行号的对应关系,该表保存在类的方法表中,类名、方法名也可在方法表中查到。
以上我们介绍了解释执行时异常处理程序的设计,由于编译器在编译程序时提供了充足的异常处理信息,如方法异常处理表、方法行号表等,使得异常处理过程较为简单。
2.及时编译执行时的异常处理
我们在及时编译异常处理设计上,一般采用了与解释器的异常处理相同的设计方法。在解释执行时,由于解释器控制方法的全部活动,包括栈空间的分配,指令的执行,因此可显式地创建一方法调用表(解释器创建)供异常处理程序使用。但由于及时编译将字节码编译成本地码,方法的栈空间直接分配在线程的本地栈中,我们无法创建一显式的数据结构去反映方法调用关系及记录方法的运行状态。因此,要处理异常在方法调用栈中的传播及异常句柄的查询,我们必须掌握本地方法栈的结构及字节码与本地代码之间的对应关系。
在及时编译时,我们首先应建立字节码与机器码之间的对应关系,在编译每一条字节码时,我们将字节码所对应的机器码在本地方法代码中的偏移记录下来,我们可利用这一信息进行方法异常处理表的翻译,将异常处理表中字节码的指令偏移替换为对应的机器码的内存位置。同理我们可翻译方法的行号表将源文件行号与字节码的对应关系转换为源文件行号同机器码之间的对应关系。下面的程序给出了异常处理表的翻译过程。
在上面的程序中,nativeCodeBase为一指针,其指向编译后本地代码的起始位置,nativeOffset为一整型数组存放每一字节码在本地方法代码中的偏移。经转换后,e->start-pc,e->end-pc,e->handler-pc被转换为对应字节码的本地码在内存中的地址。同理,可进行方法行号表转换。方法异常表及行号表的转换为异常处理提供了方便,异常处理程序可直接由这两个表查询异常处理句柄及异常信息,其查询过程与解释异常处理相同。
及时编译执行时,异常的传播处理较为复杂,这需要对本地方法栈的结构有一个清楚的认识。线程在创建时JVM为其分配一个固定的运行空间(可由用户指定),线程的一切活动所需的空间都被分配在该空间中。对于及时编译,该空间存放本地方法栈,本地方法栈实际上就是传统的C栈。要实现异常在方法调用栈中的传播,我们就得解决如何确定产生异常方法栈的位置,及调用该方法的上层方法的栈位置。例如在图9-2中,给出了线程栈空间中方法栈存放的示意图,图9-2的方法调用过程为方法1调用方法2,方法2调用方法3。方法栈在线程空间中是连续存放的。线程空间也是一个连续的空间,它的起始与结束地址分别存放在线程背景数据结构中。
图9-2 线程栈空间中方法栈存放的示意图
从图9-2可知,方法栈空间的连接及方法的当前执行点分别是由方法栈中的数据retbp,retpc建立的。只要能获取这两个数据就能实现异常在方法中的传播。对于内部异常,虚拟机直接调用异常处理程序执行异常处理;用户产生的异常则在及时编译器编译异常,产生指令athrow时产生调用异常处理程序的机器指令。异常处理程序执行时,异常处理程序的方法栈为当前栈,异常处理程序可根据其第1个方法参数的存储位置,减去8字节偏移得到其调用者方法栈的基址指针retbp,第1个方法参数的存储位置减去4字节偏移得到调用方法的返回地址retpc,retpc-1即为调用点的指令地址;同理在调用方法的retbp处我们又可以得到该方法的调用者的栈基址及返回地址。采用这种方式可实现异常在方法调用栈中的传播。因此,及时编译异常处理时方法调用关系链表的数据结构如下。
及时编译时异常处理的另一个繁琐的过程是,如何确定调用点指令所属的类及方法。在以上的叙述中我们知道,retpc-1为某方法调用其子方法的方法调用指令地址,我们应根据该地址信息去查询方法的异常处理表以获取异常处理句柄,但这一前提是我们如何先找到该方法。为了找到地址retpc-1所处的方法,我们不得不遍历所有的类及其方法,以判断该地址是否在某一方法代码内。显然这一方法较为费时,当然我们也可设计复杂的数据结构记录指令与方法的查询关系以简化这一过程,但这确实没有必要,毕竟异常产生的次数很少,即便产生异常,大多数情况下也要终止程序的执行,因此花费在这方面的时间可忽略不计。
在确定了异常如何传播及异常处理句柄如何查找后,下一步的任务就是如何调用异常处理句柄。在异常处理表中提供的只是异常句柄的地址,我们必须用汇编语言实现异常处理句柄的调用,调用程序如下,下段代码遵循的是汇编调用C子程序的规则。异常处理句柄catch的调用基本上与C函数的调用相同,异常对象可看成是它的方法参数,但不同的是该参数并不被压入参数空间,它是在执行catch的第一条指令时被压入指定的局部变量空间。我们在及时编译时一般都是以基址寄存器的地址为基准进行数据的访问,因此应恢复异常句柄所属方法的基址寄存器,最后是执行异常处理句柄。
本节我们讨论了及时编译执行时异常处理的关键技术,可以看出由于本地代码的存在,使得异常处理过程变得较为复杂。
免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。