异常能不能作为控制流,这个争论其实已经存在了很长时间,最近gdpglc同学发的一连四张《验证String是不是整数,用异常作判断怎么了!》的帖子(前三张已经被投为隐藏帖,要看的话可以从第四张进去)令这个争端又一次成为JE主版的话题。
gdpglc同学的语气比较激烈,但发表自己观点是值得肯定的,何况异常可以作为控制流的观点,JavaEye创始人肉饼同学在2003年的时候也提出过,并且也引发了一些讨论,就在这帖子的2楼:http://www.iteye.com/topic/2038。无论是03年还是今天,反方的主流意见都无外乎两点:一是圣经上说不行,列举《Effective Java》等例子。二是从性能上说不行,列举了测试用例,譬如http://www.iteye.com/topic/856221这里,我在二楼发的一个测试用例,有兴趣的话可以看一下,后面被gdpglc吐槽了十几楼那些就不要看了T_T
尽信书不如无书,第一点意见不值得讨论。第二点意见说使用异常很慢,并且测试数据说明了确实很慢,那我们不妨来看看为何使用异常会慢,从深一些的层次来看看异常到底是个神马东西。
异常慢在哪里?
说用异常慢,首先来看看异常慢在哪里?有多慢?下面的测试用例简单的测试了建立对象、建立异常对象、抛出并接住异常对象三者的耗时对比:
- package org.fenixsoft.exception;
- public class ExceptionTest {
- private int testTimes;
- public ExceptionTest(int testTimes) {
- this.testTimes = testTimes;
- }
- public void newObject() {
- long l = System.nanoTime();
- for (int i = 0; i < testTimes; i++) {
- new Object();
- }
- System.out.println("建立对象:" + (System.nanoTime() - l));
- }
- public void newException() {
- long l = System.nanoTime();
- for (int i = 0; i < testTimes; i++) {
- new Exception();
- }
- System.out.println("建立异常对象:" + (System.nanoTime() - l));
- }
- public void catchException() {
- long l = System.nanoTime();
- for (int i = 0; i < testTimes; i++) {
- try {
- throw new Exception();
- } catch (Exception e) {
- }
- }
- System.out.println("建立、抛出并接住异常对象:" + (System.nanoTime() - l));
- }
- public static void main(String[] args) {
- ExceptionTest test = new ExceptionTest(10000);
- test.newObject();
- test.newException();
- test.catchException();
- }
- }
运行结果:
- 建立对象:575817
- 建立异常对象:9589080
- 建立、抛出并接住异常对象:47394475
建立一个异常对象,是建立一个普通Object耗时的约20倍(实际上差距会比这个数字更大一些,因为循环也占用了时间,追求精确的读者可以再测一下空循环的耗时然后在对比前减掉这部分),而抛出、接住一个异常对象,所花费时间大约是建立异常对象的4倍。那我们来看看占用时间的“大头”:抛出、接住异常,系统到底做了什么事情?
当异常发生的那一刹那
注:
RednaxelaFX:用字节码来解释性能问题很抱歉也是比较不靠谱的。字节码用于解释“语义问题”很靠谱,但看字节码是看不出性能问题的——超过它的抽象层次了
IcyFenix:同意RednaxelaFX的观点,但本节中的字节码分解本就不涉及性能,仅想表达“当异常发生那一刹那”时会发生什么事情(准确的说,还要限定为是解释方式执行),这里提及的20%、80%时间是基于前一点测试的结果。
要知道当异常发生的那一刹那系统做了什么事情,先把catchException()方法中循环和时间统计的代码去掉,使得代码变得纯粹一些:
- public void catchException() {
- try {
- throw new Exception();
- } catch (Exception e) {
- }
- }
然后使用javap -verbose命令输出它的字节码,结果如下:
- public void catchException();
- Code:
- Stack=2, Locals=2, Args_size=1
- 0: new #58; //class java/lang/Exception
- 3: dup
- 4: invokespecial #60; //Method java/lang/Exception."<init>":()V
- 7: athrow
- 8: astore_1
- 9: return
- Exception table:
- from to target type
- 0 8 8 Class java/lang/Exception
解释一下这段字节码的运作过程,如果平时看字节码比较多的同学可以直接略过这段。偏移地址为0的new指令首先会在常量池找到第58项常量,此常量现在为CONSTANT_Class_info型的符号引用,类解析阶段被翻译为java.lang.Exception类的直接引用,接着虚拟机会在Java堆中开辟相应大小的实例空间,并将此空间的引用压入操作栈的栈顶。偏移为3的dup指令就简单的把栈顶的值复制了一份,重新压入栈顶,这时候操作栈中有2份刚刚new出来的exception对象的引用。偏移为4的invokespecial指令将第一个exception对象引用出栈,以它为接收者调用了Excepiton类的实例构造器,这句执行完后栈顶还剩下一份exception对象的引用。写了那么多,说白了这3条字节码就是干了“new Exception()”这句Java代码应该做的事情,和创建任何一个Java对象没有任何区别。这一部分耗费的时间在上一节中分析过,创建一个异常对象只占创建、抛出并接住异常的20%时间。
接着是占用80%时间高潮部分,偏移为7的athrow指令,这个指令运作过程大致是首先检查操作栈顶,这时栈顶必须存在一个reference类型的值,并且是java.lang.Throwable的子类(虚拟机规范中要求如果遇到null则当作NPE异常使用),然后暂时先把这个引用出栈,接着搜索本方法的异常表(异常表是什么等写完这段再说),找一下本方法中是否有能处理这个异常的handler,如果能找到合适的handler就会重新初始化PC寄存器指针指向此异常handler的第一个指令的偏移地址。接着把当前栈帧的操作栈清空,再把刚刚出栈的引用重新入栈。如果在当前方法中很悲剧的找不到handler,那只好把当前方法的栈帧出栈(这个栈是VM栈,不要和前面的操作栈搞混了,栈帧出栈就意味着当前方法退出),这个方法的调用者的栈帧就自然在这条线程VM栈的栈顶了,然后再对这个新的当前方法再做一次刚才做过的异常handler搜索,如果还是找不到,继续把这个栈帧踢掉,这样一直到找,要么找到一个能使用的handler,转到这个handler的第一条指令开始继续执行,要么把VM栈的栈帧抛光了都没有找到期望的handler,这样的话这条线程就只好被迫终止、退出了。
刚刚说的异常表,在运行期一般会实现在栈帧当中。在编译器静态角度看,就是上面直接码中看到的这串内容:
- Exception table:
- from to target type
- 0 8 8 Class java/lang/Exception
上面的异常表只有一个handler记录,它指明了从偏移地址0开始(包含0),到偏移地址8结束(不包含8),如果出现了java.lang.Exception类型的异常,那么就把PC寄存器指针转到8开始继续执行。顺便说一下,对于Java语言中的关键字catch和finally,虚拟机中并没有特殊的字节码指令去支持它们,都是通过编译器生成字节码片段以及不同的异常处理器来实现。
字节码指令还剩下2句,把它们说完。偏移地址为8的astore_1指令,作用是把栈顶的值放到第一个Slot的局部变量表中,刚才说过如果出现异常后,虚拟机找到了handler,会把那个出栈的异常引用重新入栈。因此这句astore_1实现的目的就是让catch块中的代码能访问到“catch (Exception e)”所定义的那个“e”,又顺便提一句,局部变量表从0开始,第0个Slot放的是方法接收者的引用,也就是使用this关键能访问的那个对象。最后的return指令就不必多讲了,是void方法的返回指令,因为我们的catch块里面没有内容,所以立刻就return了。
到此为止,这几句字节码讲完了,我们总结一下athrow指令中虚拟机可能做的事情(只会做其中一部份啦):
- 检查栈顶异常对象类型(只检查是不是null,是否referance类型,是否Throwable的子类一般在类验证阶段的数据流分析中做,或者索性不做靠编译器保证了,编译时写到Code属性的StackMapTable中,在加载时仅做类型验证)
- 把异常对象的引用出栈
- 搜索异常表,找到匹配的异常handler
- 重置PC寄存器状态
- 清理操作栈
- 把异常对象的引用入栈
- 把异常方法的栈帧逐个出栈(这里的栈是VM栈)
- 残忍地终止掉当前线程。
- ……
好吧,我勉强认同虚拟机出现异常时要做的事情挺多的,但这要作为直接证据说明它就理所当然的那么慢有点勉强吧?要不,找个具体实现看一下?
(PS:虚拟机:囧……这人好麻烦……鸭梨很大……)
透过虚拟机实现看athrow指令
下面的讲解基于OpenJDK中HotSpot虚拟机的源代码。有兴趣的话可以去OpenJDK网站(http://download.java.net/openjdk/jdk7/)下载一份,没有兴趣可以略过这节。
被JIT编译之后,异常处理变成神马样子我们就不管了,只看一看虚拟机解释执行时处理异常是如何实现的。因为三大商用虚拟机只有Sun一系的(Sun/Oracle、HP、SAP等)以OpenJDK的形式开源了,这里所指的所指的实现也就仅是HotSpot VM,后面就不再严格区分了。
注:此处有个根本性的错误,见2楼RednaxelaFX的指正
RednaxelaFX:HotSpot并没有使用bytecodeInterpreter.cpp里实现的解释器;在OpenJDK里有一套叫Zero/Shark的解释器/JIT编译器,其中Zero的部分用了这里提到的解释器,但它主要是在HotSpot还没良好移植的平台上使用的。
虚拟机字节码解释器的关键代码在hotspot\src\share\vm\interpreter\bytecodeInterpreter.cpp之中,它使用了while(1)的方式循环swith PC寄存器所指向的opcode指令,处理athrow指令的case中是这样写的:
- CASE(_athrow): {
- oop except_oop = STACK_OBJECT(-1);
- CHECK_NULL(except_oop);
- // set pending_exception so we use common code
- THREAD->set_pending_exception(except_oop, NULL, 0);
- goto handle_exception;
- }
第一句提取操作栈中引用的异常对象,第二句检查异常是否为空,虚拟机规范中要求的为null就当NPE异常,就是这句实现的:
- #define CHECK_NULL(obj_)
- if ((obj_) == NULL) {
- VM_JAVA_ERROR(vmSymbols::java_lang_NullPointerException(), "");
- }
- VERIFY_OOP(obj_)
注释中说可以使用“common code”是指handle_return中的代码,每条opcode处理完都会转到这段代码。因为异常不一定来自athrow指令,也就是不一定来自于用户程序直接抛出,虚拟机运作期间也会产生异常,如被0除、空指针,严重一点的OOM神马的。所以出现异常后的方法退出动作在通用的handle_return里面根据pending_exception进行处理,代码太多就不贴了。前面几句没有太特别的动作,看来athrow指令的关键实现还是在handle_exception这节,看看它的代码(为了逻辑清晰,我删除了不必要的代码,譬如支持跟踪调试的语句):
- handle_exception: {
- HandleMarkCleaner __hmc(THREAD);
- Handle except_oop(THREAD, THREAD->pending_exception());
- // Prevent any subsequent HandleMarkCleaner in the VM
- // from freeing the except_oop handle.
- HandleMark __hm(THREAD);
- THREAD->clear_pending_exception();
- assert(except_oop(), "No exception to process");
- intptr_t continuation_bci;
- // expression stack is emptied
- topOfStack = istate->stack_base() - Interpreter::stackElementWords;
- CALL_VM(continuation_bci = (intptr_t)InterpreterRuntime::exception_handler_for_exception(THREAD, except_oop()),
- handle_exception);
- except_oop = (oop) THREAD->vm_result();
- THREAD->set_vm_result(NULL);
- if (continuation_bci >= 0) {
- // Place exception on top of stack
- SET_STACK_OBJECT(except_oop(), 0);
- MORE_STACK(1);
- pc = METHOD->code_base() + continuation_bci;
- // for AbortVMOnException flag
- NOT_PRODUCT(Exceptions::debug_check_abort(except_oop));
- goto run;
- }
- // for AbortVMOnException flag
- NOT_PRODUCT(Exceptions::debug_check_abort(except_oop));
- // No handler in this activation, unwind and try again
- THREAD->set_pending_exception(except_oop(), NULL, 0);
- goto handle_return;
- }
只看这段代码的关键部分,CALL_VM那句是查找异常表,所执行的InterpreterRuntime::exception_handler_for_exception在同目录下的interpreterRuntime.cpp中,查找的具体过程有点复杂,只看程序的主体脉络,这里的代码就不再牵扯进来了。如果找到,也就是if (continuation_bci >= 0)成立的话,(bci的意思是Bytecode Index,字节码索引),把异常对象重新入栈(SET_STACK_OBJECT(except_oop(), 0)这句),并且重置PC指针为异常handler的起始位置(pc = METHOD->code_base() + continuation_bci这句),然后跳转到run处开始下一轮的循环switch过程。查询异常表没有找到合适的handler,那重新设置上pending_exception,因为前面的时候使用clear_pending_exception()清除掉了。在handle_return中会根据这个标志来决定方法是否出现异常,要不要退出。虚拟机规范中要求的athrow指令的动作这里就写完了,HotSpot VM我们写不出来,看一下还是可以的嘛。
观点与小结
这篇文章的主要目的是探讨虚拟机中底层是如何看待“异常”的,并不打算去争论“异常”能不能作为控制流。对事物运作本质了解越深,就越容易根据当前场景衡量代码清晰、实现简单、性能高低、易于扩展等各方面的因素。“能不能”、“会不会”、“是否应该”这类的疑惑就会相对更少一些,也不需要靠“论”去证明了。
最后稍微说一下引子中提到的那件事情,用异常判断整数能不能用,我的观点还是http://www.iteye.com/topic/856221中二楼的第一句话“这个方法如果调用次数不多,怎么写都无所谓,如果次数多还是不要这样用的好”,请gdpglc同学不同意也不要在这个帖子里面吐槽,多多包涵。gdpglc的第一张帖子我投过一次隐藏贴,那是觉得其语言太过偏激了,不想争论,但后面他的另外三张帖子中很多评论都有可取、可想之处,变成隐藏扣分似乎不太应该。
相关推荐
数据异常值分析与处理在大数据领域中扮演着至关重要的角色,因为异常值可能导致数据分析结果的偏差,甚至误导决策。在处理异常值时,通常需要...在大气污染预测等实际问题中,正确处理异常值能显著提高模型的预测性能。
JavaWeb远程性能分析是针对基于Java的Web应用程序在运行时的性能进行监控和优化的过程。这一过程涵盖了服务器响应时间、内存使用、CPU占用率、线程状态等多个关键指标,旨在发现并解决可能导致系统瓶颈或故障的问题...
【系统性能分析与优化】是IT领域中一个关键的话题,主要关注如何提升系统效率,解决性能瓶颈,确保系统稳定运行。对于Unix系统,尤其是大型系统,性能分析和优化显得尤为重要,因为这涉及到内存管理、磁盘I/O等多个...
在IT行业中,性能分析是优化系统的关键步骤,尤其是在Java虚拟机(JVM)环境中。IBM作为全球知名的技术公司,提供了多种强大的性能分析工具,帮助开发者和运维人员深入理解应用程序的运行状况,找出瓶颈并进行调优。...
Linux系统性能分析是一个至关重要的任务,特别是在服务器管理和优化过程中。NMON(Nigel's Monitoring Tool for AIX and Linux)是一款强大的工具,专为Linux系统设计,用于实时监控和记录系统的性能数据,包括CPU、...
### 找出系统性能的瓶颈:企业级系统性能分析实践 #### 一、引言 在当前快速发展的信息技术领域中,企业级应用系统的性能优化变得日益重要。随着业务规模的不断扩大和技术复杂性的增加,如何有效地识别并解决系统...
在这个"MAT Eclipse MemoryAnalyzer java性能分析"主题中,我们将深入探讨MAT的核心功能、使用方法以及如何通过它来提升Java应用的性能。 MAT提供了丰富的视图和功能,帮助开发者识别内存问题。其中,最重要的可能...
理解ARMv8异常处理机制,特别是对于栈操作和异常处理函数调用的分析,对于确保内核的稳定性和性能至关重要。通过分析entry.S和traps.c中的代码,开发者能够洞察ARMv8 Linux内核如何处理异常,以及这些处理机制的内在...
在实际的性能分析过程中,对比分析现象指标与原因指标的曲线趋势是一种常见的有效手段。通过观察两条或更多条曲线的变化趋势,分析它们之间是否存在相互关联,可以帮助我们快速定位潜在的性能瓶颈。这种分析往往需要...
### 网络异常流量监测和用户行为分析 #### 网络异常流量监测的重要性 在当前复杂的网络环境中,网络异常流量监测变得尤为重要。随着网络安全威胁的不断升级,如DDoS攻击、各种类型的网络病毒(如蠕虫、木马等)、...
Java异常的性能分析.在Java中抛异常的性能是非常差的。通常来说,抛一个异常大概会消耗100到1000个时钟节拍。 在Java中抛异常的性能是非常差的。通常来说,抛一个异常大概会消耗100到1000个时钟节拍。 通常是...
一个轻量级的springboot项目性能分析工具,通过方法调用链路追踪以及运行时长监控快速定位性能瓶颈,并进行可视化展示,还支持代码热更新与邮件预警 实时监听方法,统计运行时长 web展示方法调用链路,瓶颈可视化...
本项目是一个集成了性能分析的数据分析工具,使用Python的cProfile模块来识别数据处理过程中的性能瓶颈。项目包括数据加载、清洗、处理和分析的完整流程,并通过性能分析报告提供优化建议。 使用人群: 数据分析师...
Oracle数据库性能分析是数据库管理员(DBA)日常工作中至关重要的一环,特别是在大型金融机构如工商银行这样的环境中,数据库的稳定性和性能直接影响到业务的运行效率和服务质量。本文将基于提供的内容,详细阐述...
《NASTAR日常优化-小区性能分析》 NASTAR是一款强大的网络性能分析工具,尤其在TD-SCDMA网络优化中发挥着重要作用。其核心功能之一是对小区性能进行深度分析,帮助运维工程师快速识别并定位网络中的问题。下面将...
然而,随着网络技术的迅速发展和应用的不断深入,网络性能管理和异常流量分析成为IT领域内至关重要的议题。网络的稳定性、安全性以及高效性直接关联到业务的连续性和用户体验的好坏,特别是在构建大型网络时,这两大...
在探讨燃气-蒸汽联合循环机组性能分析及诊断系统的研究中,大数据技术的应用是一个非常关键的领域。在能源行业中,技术的革新和升级一直是推动行业进步的重要力量,而基于大数据的性能分析及诊断系统正是这一趋势的...
【异常管理与异常分析】 ...总结来说,异常管理与异常分析是IT运维的关键组成部分,它们帮助企业确保服务的稳定性和可靠性,同时通过持续改进和预防措施,减少异常带来的负面影响,提升整体业务性能。
本标准规定了监管场所异常事件视频智能分析系统的架构、功能要求和性能指标,以确保系统能够实时、准确地识别各类异常行为,如逃跑、打架、自伤等,并及时向相关人员发出警报。标准自2019年6月3日起发布并实施,旨在...