`

透过JVM看Exception本质(转载)

    博客分类:
  • JVM
阅读更多

转载自 ---- http://icyfenix.iteye.com/blog/857722

 

引子

        异常能不能作为控制流,这个争论其实已经存在了很长时间,最近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
        尽信书不如无书,第一点意见不值得讨论。第二点意见说使用异常很慢,并且测试数据说明了确实很慢,那我们不妨来看看为何使用异常会慢,从深一些的层次来看看异常到底是个神马东西。

异常慢在哪里?

        说用异常慢,首先来看看异常慢在哪里?有多慢?下面的测试用例简单的测试了建立对象、建立异常对象、抛出并接住异常对象三者的耗时对比:

Java代码  收藏代码
  1. package  org.fenixsoft.exception;  
  2.   
  3. public   class  ExceptionTest {  
  4.   
  5.     private   int  testTimes;  
  6.   
  7.     public  ExceptionTest( int  testTimes) {  
  8.         this .testTimes = testTimes;  
  9.     }  
  10.   
  11.     public   void  newObject() {  
  12.         long  l = System.nanoTime();  
  13.         for  ( int  i =  0 ; i < testTimes; i++) {  
  14.             new  Object();  
  15.         }  
  16.         System.out.println("建立对象:"  + (System.nanoTime() - l));  
  17.     }  
  18.   
  19.     public   void  newException() {  
  20.         long  l = System.nanoTime();  
  21.         for  ( int  i =  0 ; i < testTimes; i++) {  
  22.             new  Exception();  
  23.         }  
  24.         System.out.println("建立异常对象:"  + (System.nanoTime() - l));  
  25.     }  
  26.   
  27.     public   void  catchException() {  
  28.         long  l = System.nanoTime();  
  29.         for  ( int  i =  0 ; i < testTimes; i++) {  
  30.             try  {  
  31.                 throw   new  Exception();  
  32.             } catch  (Exception e) {  
  33.             }  
  34.         }  
  35.         System.out.println("建立、抛出并接住异常对象:"  + (System.nanoTime() - l));  
  36.     }  
  37.   
  38.     public   static   void  main(String[] args) {  
  39.         ExceptionTest test = new  ExceptionTest( 10000 );  
  40.         test.newObject();  
  41.         test.newException();  
  42.         test.catchException();  
  43.     }  
  44. }  

运行结果:

Java代码  收藏代码
  1. 建立对象: 575817   
  2. 建立异常对象:9589080   
  3. 建立、抛出并接住异常对象:47394475   

        建立一个异常对象,是建立一个普通Object耗时的约20倍(实际上差距会比这个数字更大一些,因为循环也占用了时间,追求精确的读者可以再测一下空循 环的耗时然后在对比前减掉这部分),而抛出、接住一个异常对象,所花费时间大约是建立异常对象的4倍。那我们来看看占用时间的“大头”:抛出、接住异常, 系统到底做了什么事情?

当异常发生的那一刹那

        注:
        RednaxelaFX:用字节码来解释性能问题很抱歉也是比较不靠谱的。字节码用于解释“语义问题”很靠谱,但看字节码是看不出性能问题的——超过它的抽象层次了
        IcyFenix:同意RednaxelaFX的观点,但本节中的字节码分解本就不涉及性能,仅想表达“当异常发生那一刹那”时会发生什么事情(准确的说,还要限定为是解释方式执行),这里提及的20%、80%时间是基于前一点测试的结果。

        要知道当异常发生的那一刹那系统做了什么事情,先把catchException()方法中循环和时间统计的代码去掉,使得代码变得纯粹一些:

Java代码  收藏代码
  1. public   void  catchException() {  
  2.     try  {  
  3.         throw   new  Exception();  
  4.     } catch  (Exception e) {  
  5.     }  
  6. }  

然后使用javap -verbose命令输出它的字节码,结果如下:

Java代码  收藏代码
  1. public   void  catchException();  
  2.   Code:  
  3.    Stack=2 , Locals= 2 , Args_size= 1   
  4.    0 :    new      # 58 //class java/lang/Exception   
  5.    3 :   dup  
  6.    4 :   invokespecial   # 60 //Method java/lang/Exception."<init>":()V   
  7.    7 :   athrow  
  8.    8 :   astore_1  
  9.    9 :    return   
  10.   Exception table:  
  11.    from   to  target type  
  12.      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, 这样的话这条线程就只好被迫终止、退出了。
刚刚说的异常表,在运行期一般会实现在栈帧当中。在编译器静态角度看,就是上面直接码中看到的这串内容:

Java代码  收藏代码
  1. Exception table:  
  2.  from   to  target type  
  3.    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中是这样写的:

C++代码  收藏代码
  1. CASE(_athrow): {  
  2.     oop except_oop = STACK_OBJECT(-1);  
  3.     CHECK_NULL(except_oop);  
  4.     // set pending_exception so we use common code   
  5.     THREAD->set_pending_exception(except_oop, NULL, 0);  
  6.     goto  handle_exception;  
  7. }  

        第一句提取操作栈中引用的异常对象,第二句检查异常是否为空,虚拟机规范中要求的为null就当NPE异常,就是这句实现的:

C++代码  收藏代码
  1. #define CHECK_NULL(obj_)   
  2.     if  ((obj_) == NULL) {   
  3. VM_JAVA_ERROR(vmSymbols::java_lang_NullPointerException(), "" );   
  4.     }   
  5. VERIFY_OOP(obj_)  

        注释中说可以使用“common code”是指handle_return中的代码,每条opcode处理完都会转到这段代码。因为异常不一定来自athrow指令,也就是不一定来自于 用户程序直接抛出,虚拟机运作期间也会产生异常,如被0除、空指针,严重一点的OOM神马的。所以出现异常后的方法退出动作在通用的 handle_return里面根据pending_exception进行处理,代码太多就不贴了。前面几句没有太特别的动作,看来athrow指令的 关键实现还是在handle_exception这节,看看它的代码(为了逻辑清晰,我删除了不必要的代码,譬如支持跟踪调试的语句):

C++代码  收藏代码
  1. handle_exception: {  
  2.   
  3.   HandleMarkCleaner __hmc(THREAD);  
  4.   Handle except_oop(THREAD, THREAD->pending_exception());  
  5.   // Prevent any subsequent HandleMarkCleaner in the VM   
  6.   // from freeing the except_oop handle.   
  7.   HandleMark __hm(THREAD);  
  8.   
  9.   THREAD->clear_pending_exception();  
  10.   assert(except_oop(), "No exception to process" );  
  11.   intptr_t  continuation_bci;  
  12.   // expression stack is emptied   
  13.   topOfStack = istate->stack_base() - Interpreter::stackElementWords;  
  14.   CALL_VM(continuation_bci = (intptr_t )InterpreterRuntime::exception_handler_for_exception(THREAD, except_oop()),  
  15.           handle_exception);  
  16.   
  17.   except_oop = (oop) THREAD->vm_result();  
  18.   THREAD->set_vm_result(NULL);  
  19.   if  (continuation_bci >= 0) {  
  20.     // Place exception on top of stack   
  21.     SET_STACK_OBJECT(except_oop(), 0);  
  22.     MORE_STACK(1);  
  23.     pc = METHOD->code_base() + continuation_bci;  
  24.     // for AbortVMOnException flag   
  25.     NOT_PRODUCT(Exceptions::debug_check_abort(except_oop));  
  26.     goto  run;  
  27.   }  
  28.   // for AbortVMOnException flag   
  29.   NOT_PRODUCT(Exceptions::debug_check_abort(except_oop));  
  30.   // No handler in this activation, unwind and try again   
  31.   THREAD->set_pending_exception(except_oop(), NULL, 0);  
  32.   goto  handle_return;  
  33. }  

        只看这段代码的关键部分,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的第一张帖子我投过一次隐藏贴,那是觉得其语言太过偏激了,不想争论,但后面他的另外三张帖子中很多评论都有可取、可想之处,变成隐藏扣 分似乎不太应该。

分享到:
评论

相关推荐

    推荐一些JVM原理,JVM调优,JVM内存模型,JAVA并发 电子书1

    从提供的部分内容来看,提到了Java发展历程、JVM列表、OpenJDK、编译执行过程和JIT编译等。Java的发展历程始于1995年,伴随着众多技术的引入和版本的更新,包括AWT、JDBC、JavaBeans、JDK1.1、J2SE、泛型、NIO、JMX...

    jdk,jvm源码

    Java虚拟机(JVM)是Java程序运行的核心,它负责解释和执行字节码,为Java应用程序提供了一个跨平台的运行环境。JDK(Java Development Kit)包含了开发和运行Java程序所需的所有工具,包括JVM。当我们谈论"jdk,jvm...

    [转载]深入理解JVM

    ### 深入理解JVM #### 一、Java技术与Java虚拟机 Java不仅仅是一种编程语言,更是一项综合性的技术。它主要包括四个关键组成部分: 1. **Java编程语言**:这是一种面向对象的编程语言,提供了丰富的类库支持,...

    jvm 配置jvm参数

    ### JVM参数配置详解 #### 一、理解JVM参数配置的重要性 Java Virtual Machine (JVM) 是运行Java程序的核心环境,其性能优化很大程度上依赖于正确的JVM参数配置。合理配置JVM参数不仅可以显著提升应用程序的运行...

    jvm 启动过程 JVM 原理

    Java虚拟机(JVM)是Java程序运行的基础,它是一个抽象的计算机系统,负责执行Java字节码。本文将深入探讨JVM的启动过程及其基本原理。 首先,我们需要理解JVM的基本概念。JVM是Java Virtual Machine的缩写,它是...

    JVM图解-JVM指令-JVM原型图.rar

    在这个压缩包中,"JVM图解.png"可能是对JVM内部结构的可视化表示,"JVM图解"可能是一个详细的文档,解释了JVM的工作原理,而"JVM指令手册 中文版"则提供了JVM可执行的所有指令的详细信息。下面,我们将深入探讨JVM的...

    慢慢琢磨jvm 经典

    #### JVM的本质与架构 JVM是一种在物理计算机上运行的抽象计算机,不同于可见的虚拟化软件如VMWare,它主要存在于内存中。其设计目标是实现“一次编译,处处运行”,通过将Java字节码转换为特定平台的机器语言,使...

    jvm 详细介绍,了解jvm各个组成部分和功能

    ### JVM 详细介绍:掌握 JVM 的各个组成部分与功能 #### 一、Java 源文件编译及执行 Java 应用程序的核心在于源文件的编译与执行。不同于 C/C++ 这类需要针对不同平台进行编译的语言,Java 采用了一种更为灵活的...

    jvm视频及笔记

    Java虚拟机(JVM)是Java程序运行的核心组件,它负责解释和执行字节码,为开发者提供了跨平台的运行环境。"jvm视频及笔记"这个资源显然是一份全面学习JVM的材料,结合了视频教程和书面笔记,帮助学习者深入理解JVM的...

    jvm-mon基于控制台的JVM监视

    【jvm-mon基于控制台的JVM监视】 `jvm-mon`是一款实用的工具,它允许开发者通过控制台界面实时监控Java虚拟机(JVM)的状态。在Java开发过程中,性能分析是至关重要的,因为良好的性能能提升用户体验,降低服务器...

    JVM中文指令手册.pdf

    JVM(Java Virtual Machine,Java虚拟机)是运行所有Java程序的假想计算机,是Java程序的运行环境,负责执行指令、管理数据、内存、寄存器等,是实现Java跨平台特性的关键部分。JVM指令手册详细记录了JVM的所有操作...

    (46页完整版)JVM体系结构与GC调优.zip

    46页PPT详解JVM,46页PPT详解JVM,46页PPT详解JVM,46页PPT详解JVM,46页PPT详解JVM,46页PPT详解JVM,46页PPT详解JVM,46页PPT详解JVM,46页PPT详解JVM,46页PPT详解JVM,46页PPT详解JVM,46页PPT详解JVM,46页PPT...

    JVM必知必会

    ### JVM必知必会知识点梳理 #### 1. JVM的定义与层次 Java虚拟机(JVM)具有多重含义: - **一套规范**:即Java虚拟机规范,定义了Java虚拟机应该具有的行为。 - **一种实现**:例如HotSpot、J9、JRockit,它们都是...

    JVM 输出 GC 日志导致 JVM 卡住

    JVM 输出 GC 日志导致 JVM 卡住 JVM 输出 GC 日志导致 JVM 卡住是一个常见的问题,尤其是在高并发和高性能应用中。这个问题的根源在于 JVM 的垃圾回收机制(Garbage Collection,GC),它会在 JVM 运行时周期性地...

    狂神说JVM探究.rar

    【狂神说JVM探究】是一份集合了多种格式的学习资料,主要涵盖了Java虚拟机(JVM)的基础知识。这份资料出自B站上的【狂神说Java】系列教程,为快速入门JVM提供了详实的笔记。以下是根据这些资源可能包含的一些关键...

    JVM

    Java虚拟机(JVM)是Java编程语言的核心组成部分,它为Java程序提供了运行环境,使得Java代码能够在不同的操作系统上“一次编写,到处运行”。JVM是Java平台的一部分,负责执行字节码,管理内存,垃圾收集,以及提供...

    jvm-exporter.json

    说明:kubernetes集群监控jvm内存监控模板

    浅谈jvm原理

    "浅谈 JVM 原理" JVM(Java Virtual Machine)是一种虚拟机,它可以模拟完整的硬件系统功能,运行在一个完全隔离的环境中,提供了一个完整的计算机系统。JVM 可以分为三类:VMWare、Visual Box 和 JVM。其中,...

    JVM课件(云析学院JVM课程课件)

    在上述提供的文件信息中,我们看到一系列关于Java虚拟机(JVM)的知识点。文件主要包括一个链接指向视频资源、云析学院的讲师信息、以及课件的主要内容概要。内容概要被分为三个部分:基础篇、高级篇和优化篇,并...

    JVM原理讲解和调优,详细讲解JVM底层

    JVM(Java虚拟机)是Java语言运行的基础,它负责执行Java字节码,并且是Java跨平台特性的关键实现。JVM的主要职责包括加载Java程序、验证字节码、将字节码转换成机器码执行、内存管理、垃圾回收和提供安全机制等。...

Global site tag (gtag.js) - Google Analytics