`
IcyFenix
  • 浏览: 362513 次
  • 性别: Icon_minigender_1
  • 来自: 珠海
文章分类
社区版块
存档分类
最新评论

透过JVM看Exception本质

阅读更多
引子

        异常能不能作为控制流,这个争论其实已经存在了很长时间,最近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的第一张帖子我投过一次隐藏贴,那是觉得其语言太过偏激了,不想争论,但后面他的另外三张帖子中很多评论都有可取、可想之处,变成隐藏扣分似乎不太应该。
分享到:
评论
20 楼 william_ai 2011-01-03  
try{  
    while(true){//不用hashnext用异常不好么?  
        list.add( it.next());    
    }  
}cache(Exception e ){  
    return list;  
} 

将这个比较了下。下面是测试源码。
import java.util.ArrayList;
import java.util.Iterator;
public class A{
        public static void main(String[] args){
                ArrayList a0=null;
                ArrayList a1=null;
                long t0=0;
                long t1=0;
                long t2=0;//hashNext()用时
                long t3=0;//Exception 用时
                long l=0;
                int[] t={10,100,1000,10000};
                int lt=t.length;
                int sum=0;
                for(int k=0;k<lt;k++){
                        l=t[k];
                        sum=0;
                        for(int i=0;i<l;i++){
                                System.out.print("\b\b\b\b\b\b\b\b\b\bi="+i);
                                a0=new ArrayList();
                                for(int j=0;j<i;j++){
                                        a0.add(""+j);
                                }
                                a1=new ArrayList();
                                Iterator it = a0.iterator();
                                t0=System.nanoTime();
                                while(it.hasNext()){
                                        a1.add(it.next());
                                }
                                t1=System.nanoTime();
                                t2=t1-t0;
                                a1=new ArrayList();
                                it = a0.iterator();
                                t0=System.nanoTime();
                                try{
                                        while(true){
                                                a1.add(it.next());
                                        }
                                }catch(Exception e){
                                        t1=System.nanoTime();
                                }
                                t3=t1-t0;
                                if(t3>t2){
                                        sum++;
                                }
                        }
                        System.out.println("\b\b\b\b\b\b\b\b\b\b\bStatics="+sum+"/"+l);
                }
        }
}

结果如下:
statistics=10/10
statistics=83/100
statistics=697/1000
statistics=6005/10000(在mac的机器上跑的结果是2608/10000)

hasNext()的方式的用时比Exception方式的用时少的test在60%以上。当然,这也意味着有10%-40%的test,Exception的方式比hasNext()的方式用时少。在设计的时候怎么抉择,人各有志,莫强求。
19 楼 agapple 2011-01-03  
其实LZ可以再往下细扣一下,为啥new Exception会慢,profile一看主要就是慢在一个native方法调用上fillInStackTrace。

该方法的主要作用就是获取当前线程执行栈,用于日志记录和以后分析所用

一般针对业务逻辑,其实可以不太关注具体的线程调用栈,我们大家应该关注的是业务异常message和code

所以我还是比较喜欢用异常来进行逻辑控制和返回,首先代码风格上会比较清晰

可以参考下:复写了RumtimeException的一个native方法,在测试过程中性能几乎就和if..else一样了
public static class ServiceException extends RumtimeException {

@Override
public Throwable fillInStackTrace() {
     return this;
}
}


具体的测试分析,可以看下我同事的测试文档 : http://www.blogjava.net/stone2083/archive/2010/07/09/325649.html
18 楼 yangyi 2011-01-03  
lz, 感觉你的测试用例有点问题,我修改了一下:
    public void catchException() {  
        long l = System.nanoTime();
        Exception e = new Exception();
        for (int i = 0; i < testTimes; i++) {  
            try {  
            	throw e;
            } catch (Exception e1) {  
            }  
        }  
        System.out.println("建立、抛出并接住异常对象:" + (System.nanoTime() - l));  
    }


这是我改正后得到的结果:
建立对象:865088
建立异常对象:12278747
建立、抛出并接住异常对象:2129125

看得出逻辑部分速度还不算慢,最慢的是建立异常对象的时间。
通过比较,和普通对象相比,多出了一个fillInStackTrace的过程,看来这个地方是性能瓶颈

通过修改方法:
    public void newException() {  
        Exception e = new Exception(); 
        long l = System.nanoTime();  
        for (int i = 0; i < testTimes; i++) {
        	e.fillInStackTrace();
        }  
        System.out.println("建立异常对象:" + (System.nanoTime() - l));  
    }

可以验证我们的结论。fillInStackTrace是一个native方法,javadoc:
Fills in the execution stack trace. This method records within this Throwable object information about the current state of the stack frames for the current thread.
显然需要和当前虚拟机执行线程的执行环境交互,导致了性能较差
17 楼 safrain 2011-01-03  
强帖。。。学习了!
16 楼 evanzzy 2011-01-03  
实例、分析俱全,LZ真不错。

用异常做流程控制还是比较损失系统性能的,有些企业系统压力不太在乎这点损失,但一些每天pv上亿的互联网站,就要斤斤计较的算计系统性能,作为程序员来讲,用硬件来弥补代码上的缺陷,不是一件值得骄傲的事情
15 楼 IcyFenix 2011-01-03  
gdpglc 写道
一个具体的问题:
之前LZ测试了一下validateInteger的执行时间,执行100000次的时间是:
字符串为非整数,验证方式1:2029725263ns

我想请教一下:
这是一个累加的时间。你之前讲过和另一个方法比,时间差200倍,我想请问,这200倍是以怎样的形式体现到应用程序中的?
我想有如下具体问题需要明确:

第一. 进行10万次调用,影不影响单次调用时间?
       在并发情况下,两个线程分别调用validateInteger,会不会互相增加对方的调用时间?

第二. 在一个多线程系统中,每个线程需要调用validateInteger一次。那么,当并发量具增,会不会因为validateInteger产生性能问题?如果会,达到多少并发会产生?

这里明确一下性能问题:使用户明显感觉到程序响应时间变长了。假设一个用户对应一个线程,响应时间延迟一秒。
这里还得假设这个线程,仅有validatInteger操作。


第三. 我想第二的余量应该是相当大的。那就是结论,只有在内存中反复的调用validateInteger时,才会产生显著的,可以影响程序响应时间的情况,是不是这样呢?

我想了想,这些似乎可以测试,但是当线程多时,线程切换本身也会代来开销,这样测试必然是不准的。不知LZ有没有估算的办法?

我先做一个简单估算:
假设所有validateInteger是互斥的。
那同时有50000个线程并发调用validateInteger时,最后一个会被延迟大约1秒。假设这时用户是可以容忍的。
那可以简单估算一下当前应用有多少用户。
同时请求的有:50000个。
同时在线的有:50000*10=500000
已注册用户应达到:500000*10=5000000



我写的观点是:“这个方法如果调用次数不多,怎么写都无所谓,如果次数多还是不要这样用的好”

放在有用户交互的系统中,譬如你写的使用在验证方面,我觉得属于上面第一个“如果”的情况,你只要觉得代码清晰,后续维护的人好看好懂,那怎么写都无所谓。

但Java不一定只用来做WEB、ERP、MIS这些强交互式应用,譬如在ETL类的应用中,提一个这样的需求(好像在JE上看到迅雷还是腾讯有这样的面试题),给你一个10T的数据文件,每行一个字符串,判断其中有多少整数,这样用异常的写法自然就不太合适(估计用我写的那个也不合适@_@)。

但也不必悲观的说大数据量的事情找C/C++来干。有JIT的介入后,Java和C/C++本质上都是执行native code,这时拼的就是编译器输出代码的智能程度。Java JIT编译优势是在运行期能根据profiling做很多静态编译中不敢做的激进优化,大不了逆向优化退回来,但劣势是编译时间就是运行时间,不敢做耗时太长的编译优化。

另外,如果涉及到同步互斥,那异常导致方法退出时要比我楼顶描写的做更多的事情,具体见虚拟机规范:
引用
    If the current frame represents an invocation of a synchronized method, the monitor acquired or reentered on invocation of the method is released or exited (respectively) as if by execution of a monitorexit instruction.
14 楼 pop1030123 2011-01-03  
强帖!学习一下。
话说gdpglc同学的态度感觉好多了么,呵呵。
13 楼 gdpglc 2011-01-03  
一个具体的问题:
之前LZ测试了一下validateInteger的执行时间,执行100000次的时间是:
字符串为非整数,验证方式1:2029725263ns

我想请教一下:
这是一个累加的时间。你之前讲过和另一个方法比,时间差200倍,我想请问,这200倍是以怎样的形式体现到应用程序中的?
我想有如下具体问题需要明确:

第一. 进行10万次调用,影不影响单次调用时间?
       在并发情况下,两个线程分别调用validateInteger,会不会互相增加对方的调用时间?

第二. 在一个多线程系统中,每个线程需要调用validateInteger一次。那么,当并发量具增,会不会因为validateInteger产生性能问题?如果会,达到多少并发会产生?

这里明确一下性能问题:使用户明显感觉到程序响应时间变长了。假设一个用户对应一个线程,响应时间延迟一秒。
这里还得假设这个线程,仅有validatInteger操作。


第三. 我想第二的余量应该是相当大的。那就是结论,只有在内存中反复的调用validateInteger时,才会产生显著的,可以影响程序响应时间的情况,是不是这样呢?

我想了想,这些似乎可以测试,但是当线程多时,线程切换本身也会代来开销,这样测试必然是不准的。不知LZ有没有估算的办法?

我先做一个简单估算:
假设所有validateInteger是互斥的。
那同时有50000个线程并发调用validateInteger时,最后一个会被延迟大约1秒。假设这时用户是可以容忍的。
那可以简单估算一下当前应用有多少用户。
同时请求的有:50000个。
同时在线的有:50000*10=500000
已注册用户应达到:500000*10=5000000


12 楼 skzr.org 2011-01-03  
<p>楼主分析在理<br>异常怎么用,貌似搜索帖子一大吧,楼主这个帖子论证了:异常是一件非常耗费资源的事情!<br><br>“<span style="">gdpglc</span>”的争执主要在异常怎样使用,这个真的是仁者见仁阿。</p>
<p> </p>
<p>逻辑易懂,找到性能的本质所在就可以了!</p>
<p><span style="font-family: Arial, sans-serif, Helvetica, Tahoma; line-height: 18px;">呵呵,添加点个人使用异常体会 : ) <br></span></p>
<ul style="margin-top: 0px; margin-right: 0px; margin-bottom: 1.5em; margin-left: 0px; padding: 0px;">
<li style="margin-top: 0px; margin-right: 0px; margin-bottom: 0.25em; margin-left: 30px; padding: 0px;">异常:不符合预期的情况时就会发生。</li>
<li style="margin-top: 0px; margin-right: 0px; margin-bottom: 0.25em; margin-left: 30px; padding: 0px;">什么时候需要异常:任何非预期的情况,包括输入或者非预期的逻辑,都需要抛出异常。</li>
<li style="margin-top: 0px; margin-right: 0px; margin-bottom: 0.25em; margin-left: 30px; padding: 0px;">checked异常:checked exception不要用,直接包装到Runtime异常中,交给上层处理。</li>
<li style="margin-top: 0px; margin-right: 0px; margin-bottom: 0.25em; margin-left: 30px; padding: 0px;">异常显示友好:在系统外部,异常必须优雅的展示,而不是导致当前线程或者进程崩溃。</li>
<li style="margin-top: 0px; margin-right: 0px; margin-bottom: 0.25em; margin-left: 30px; padding: 0px;">自定义异常:如果存在特殊的异常处理策略时,定义自定义异常,这样可以针对特定异常进行处理。</li>
<li style="margin-top: 0px; margin-right: 0px; margin-bottom: 0.25em; margin-left: 30px; padding: 0px;">异常记录:一般在组件(如jar包)边界记录异常,或者人机交互层记录异常。</li>
</ul>
<p> </p>
11 楼 RednaxelaFX 2011-01-03  
hmm,楼主坚持字节码角度分析的话,那送上下面的例子。
把楼主的代码稍微简化一下:
public class ExceptionTest {
    public static void newObject() {
        new Object();
    }

    public static void newException() {
        new Exception();
    }

    public static void catchException() {
        try {
            throw new Exception();
        } catch (Exception e) {
        }
    }
    
    public static void newCustomException() {
        new CustomException();
    }

    public static void main(String[] args) {
        CustomException.class.getName();
        newObject();
        newException();
        catchException();
        newCustomException();
    }
}

class CustomException extends Exception {
    @Override
    public Throwable fillInStackTrace() {
        return this;
    }
}

在main()的开头加了句getName()是为了在那个位置上触发CustomException的加载,免得它到newCustomException()的时候才加载,方便过滤日志。

然后找个fastdebug build的HotSpot,用下面的参数来跑:
java -XX:+TraceBytecodes ExceptionTest


把从main()开始到结束的部分提取出来,并忽略掉其中与类加载相关的部分之后,剩下的字节码序列(bytecode trace)是
[6872] static void ExceptionTest.main(jobject)
[6872]  5377751     0  ldc_w <unresolved klass at 5>
[6872]  5389978     3  invokevirtual 7 <getName> <()Ljava/lang/String;> 

[6872] virtual jobject java.lang.Class.getName()
[6872]  5390841     0  aload_0
[6872]  5390842     1  getfield 50 <name> <Ljava/lang/String;> 
[6872]  5390843     4  astore_1
[6872]  5390844     5  aload_1
[6872]  5390845     6  ifnonnull 19
[6872]  5390846     9  aload_0
[6872]  5390847    10  aload_0
[6872]  5390848    11  invokespecial 51 <getName0> <()Ljava/lang/String;> 
[6872]  5390849    14  dup
[6872]  5390850    15  astore_1
[6872]  5390851    16  putfield 50 <name> <Ljava/lang/String;> 
[6872]  5390852    19  aload_1
[6872]  5390853    20  areturn

[6872] static void ExceptionTest.main(jobject)
[6872]  5390854     6  pop
[6872]  5390855     7  invokestatic 8 <newObject> <()V> 

[6872] static void ExceptionTest.newObject()
[6872]  5390856     0  new 2 <java/lang/Object>
[6872]  5390857     3  dup
[6872]  5390858     4  invokespecial 1 <<init>> <()V> 

[6872] virtual void java.lang.Object.<init>()
[6872]  5390859     0  return_register_finalizer

[6872] static void ExceptionTest.newObject()
[6872]  5390860     7  pop
[6872]  5390861     8  return

[6872] static void ExceptionTest.main(jobject)
[6872]  5390862    10  invokestatic 9 <newException> <()V> 

[6872] static void ExceptionTest.newException()
[6872]  5390863     0  new 3 <java/lang/Exception>
[6872]  5390864     3  dup
[6872]  5390865     4  invokespecial 4 <<init>> <()V> 

[6872] virtual void java.lang.Exception.<init>()
[6872]  5390866     0  aload_0
[6872]  5390867     1  invokespecial 1 <<init>> <()V> 

[6872] virtual void java.lang.Throwable.<init>()
[6872]  5390868     0  aload_0
[6872]  5390869     1  invokespecial 1 <<init>> <()V> 

[6872] virtual void java.lang.Object.<init>()
[6872]  5390870     0  return_register_finalizer

[6872] virtual void java.lang.Throwable.<init>()
[6872]  5390871     4  aload_0
[6872]  5390872     5  aload_0
[6872]  5390873     6  putfield 2 <cause> <Ljava/lang/Throwable;> 
[6872]  5390874     9  aload_0
[6872]  5390875    10  invokevirtual 3 <fillInStackTrace> <()Ljava/lang/Throwable;> 
[6872]  5390876    13  pop
[6872]  5390877    14  return

[6872] virtual void java.lang.Exception.<init>()
[6872]  5390878     4  return

[6872] static void ExceptionTest.newException()
[6872]  5390879     7  pop
[6872]  5390880     8  return

[6872] static void ExceptionTest.main(jobject)
[6872]  5390881    13  invokestatic 10 <catchException> <()V> 

[6872] static void ExceptionTest.catchException()
[6872]  5390882     0  new 3 <java/lang/Exception>
[6872]  5390883     3  dup
[6872]  5390884     4  invokespecial 4 <<init>> <()V> 

[6872] virtual void java.lang.Exception.<init>()
[6872]  5390885     0  aload_0
[6872]  5390886     1  invokespecial 1 <<init>> <()V> 

[6872] virtual void java.lang.Throwable.<init>()
[6872]  5390887     0  aload_0
[6872]  5390888     1  invokespecial 1 <<init>> <()V> 

[6872] virtual void java.lang.Object.<init>()
[6872]  5390889     0  return_register_finalizer

[6872] virtual void java.lang.Throwable.<init>()
[6872]  5390890     4  aload_0
[6872]  5390891     5  aload_0
[6872]  5390892     6  putfield 2 <cause> <Ljava/lang/Throwable;> 
[6872]  5390893     9  aload_0
[6872]  5390894    10  invokevirtual 3 <fillInStackTrace> <()Ljava/lang/Throwable;> 
[6872]  5390895    13  pop
[6872]  5390896    14  return

[6872] virtual void java.lang.Exception.<init>()
[6872]  5390897     4  return

[6872] static void ExceptionTest.catchException()
[6872]  5390898     7  athrow
[6872]  5390899     8  astore_0
[6872]  5390900     9  return

[6872] static void ExceptionTest.main(jobject)
[6872]  5390901    16  invokestatic 11 <newCustomException> <()V> 

[6872] static void ExceptionTest.newCustomException()
[6872]  5390902     0  new 5 <CustomException>
[6872]  5390903     3  dup
[6872]  5390904     4  invokespecial 6 <<init>> <()V> 

[6872] virtual void CustomException.<init>()
[6872]  5390905     0  aload_0
[6872]  5390906     1  invokespecial 1 <<init>> <()V> 

[6872] virtual void java.lang.Exception.<init>()
[6872]  5390907     0  aload_0
[6872]  5390908     1  invokespecial 1 <<init>> <()V> 

[6872] virtual void java.lang.Throwable.<init>()
[6872]  5390909     0  aload_0
[6872]  5390910     1  invokespecial 1 <<init>> <()V> 

[6872] virtual void java.lang.Object.<init>()
[6872]  5390911     0  return_register_finalizer

[6872] virtual void java.lang.Throwable.<init>()
[6872]  5390912     4  aload_0
[6872]  5390913     5  aload_0
[6872]  5390914     6  putfield 2 <cause> <Ljava/lang/Throwable;> 
[6872]  5390915     9  aload_0
[6872]  5390916    10  invokevirtual 3 <fillInStackTrace> <()Ljava/lang/Throwable;> 

[6872] virtual jobject CustomException.fillInStackTrace()
[6872]  5390917     0  aload_0
[6872]  5390918     1  areturn

[6872] virtual void java.lang.Throwable.<init>()
[6872]  5390919    13  pop
[6872]  5390920    14  return

[6872] virtual void java.lang.Exception.<init>()
[6872]  5390921     4  return

[6872] virtual void CustomException.<init>()
[6872]  5390922     4  return

[6872] static void ExceptionTest.newCustomException()
[6872]  5390923     7  pop
[6872]  5390924     8  return

[6872] static void ExceptionTest.main(jobject)
[6872]  5390925    19  return

这个日志中,每段字节码的开头都有方法名,说明该字节码指令属于哪个方法;
每行开头的方括号内是线程ID;
在字节码指令的行中,线程ID之后的是字节码计数器,也就是解释器执行到当前位置已经执行了多少条字节码。中间有不连续的地方因为把类加载的代码过滤掉了;
再后面是当前字节码在方法内的偏移量(也就是bci);
然后是字节码指令的名字,和参数(如果有的话)。

看这个bytecode trace,到底整个执行过程中都经过了那些字节码指令一目了然。
留意到Throwable.fillInStackTrace()在HotSpot里是native的,所以在5390894跟5390895之间看不到该方法的实现;而它做的事情嘛……呵呵

完整的trace日志请看附件。注意:解压后日志有211MB左右,请有所准备…
10 楼 gdpglc 2011-01-03  
我的观点并没有变化...
需要一些时间,把LZ说的搞明白...
9 楼 lucane 2011-01-03  
看到高手了,这下应该平静了。。。

报这个“Exception in thread "main" java.lang.NullPointerException”

难道是声明异常的时候没有有参的构造方法,一般有参和无参的都重写了吧
所以即使捕捉到了异常也打印不出来啥东西

8 楼 IcyFenix 2011-01-02  
gdpglc 写道
LZ这样熟悉jvm的底层机制很是佩服。只是,这贴实在太专业,我的确云里雾里。估计没有相关知识的人也是够呛。不知LZ从事哪个行业,为何需要对JVM有如此的研究?的确很好奇,开发什么软件,需要这么深入jvm? 楼主不想回答,我也理解。


做大型ERP平台的,3楼住着RednaxelaFX,“专业”这2字我表示鸭梨很大。
7 楼 IcyFenix 2011-01-02  
RednaxelaFX 写道

楼主应该加一组例子,用一个自定义的异常类型:
public class JustForDemoException extends Exception {
    @Override
    public Throwable fillInStackTrace() {
        return this;
    }
}


这个Test Case应该能说明问题了:
package org.fenixsoft.exception;

public class ExceptionTest2 {

	public static class JustForDemoException extends Exception {
		@Override
		public Throwable fillInStackTrace() {
			return this;
		}
	}

	private Exception exception;

	public void setException(Exception exception) {
		this.exception = exception;
	}

	public void method1() throws Exception {
		method2();
	}

	public void method2() throws Exception {
		method3();
	}

	public void method3() throws Exception {
		method4();
	}

	public void method4() throws Exception {
		throw exception;
	}

	public static void main(String[] args) {
		ExceptionTest2 test = new ExceptionTest2();
		try {
			test.method1();
		} catch (Exception e) {
			e.printStackTrace();
		}

		test.setException(new JustForDemoException());
		try {
			test.method1();
		} catch (Exception e) {
			e.printStackTrace();
		}

		test.setException(new Exception());
		try {
			test.method1();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

        上面的testcase是描述使用上的影响,结果不贴了。
        关于性能方面,比以前new Exception()节省了75%的时间,只测试了调用栈很短的情况,调用栈更长的话节省时间会更多。
        在调用栈不是太长的时候,主要耗时集中在athrow指令上,也就是创建异常不算太慢,抛出异常比较慢(顶楼那个20%、80%的时间),基于这点就不补充到顶楼去了。当然这些都是基于JIT未介入的前提下。

        另外,我也发一个“有趣”的自定义异常案例:
public static class I_AM_NULL_POINT_Exception extends RuntimeException {
	@Override
	public String toString() {
		return super.getMessage();
	}
}


在JDK1.6之前,接住这个异常会,很容易会搞出一个真的“java.lang.NullPointerException”,测试用例我也写好了:
public class ExceptionTest3 {

	public static class I_AM_NULL_POINT_Exception extends RuntimeException {
		@Override
		public String toString() {
			return super.getMessage();
		}
	}

	public static void main(String[] args) {
		try {
			throw new I_AM_NULL_POINT_Exception();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}
        输出如下(用1.4、1.5跑,1.6修正了):
Exception in thread "main" java.lang.NullPointerException

        输出结果我只写了头一行,想不明白的同学,运行一下看看堆栈就清楚这个是神马把戏了。如果有不运行光看代码就发现问题的同学可以回个贴,没奖品,不过说明你对JDK的API还是挺了解的。
6 楼 gdpglc 2011-01-02  
LZ这样熟悉jvm的底层机制很是佩服。只是,这贴实在太专业,我的确云里雾里。估计没有相关知识的人也是够呛。不知LZ从事哪个行业,为何需要对JVM有如此的研究?的确很好奇,开发什么软件,需要这么深入jvm? 楼主不想回答,我也理解。
5 楼 beneo 2011-01-02  
2012年不远了,大家安了安了
4 楼 IcyFenix 2011-01-02  
RednaxelaFX 写道
那个…


谢谢sajia同学不吝斧正,解释器代码和bci的问题,属于根本性质的错误,为了避免误导别人,已经通过红色字体修正到主贴了。再次感谢!

解释器的问题,光看代码不debug的问题,这没啥可说的,我得多学习= =#
bci那个……我居然一直都以为是“Bytecode Instrumentation”,包括大脑中自动把“Instrumentation”当成“Instruction”来译,囧……

至于“或者索性不做靠编译器保证了”那个应该属于理解性的问题,靠编译器保证就是java se 6新的类型验证,既然可能产生误解,我在后面补充了一句红字。

你那个例子我待会回帖再补充一下,我也有个自定义异常好玩的问题,待会一起写一些。
3 楼 william_ai 2011-01-02  
2011年一开始就不消停。
2 楼 RednaxelaFX 2011-01-02  
那个…
楼主对OpenJDK的代码熟悉的话,会知道其实我们平时用的(x86或SPARC上的)HotSpot并没有使用bytecodeInterpreter.cpp里实现的解释器;在OpenJDK里有一套叫Zero/Shark的解释器/JIT编译器,其中Zero的部分用了这里提到的解释器,但它主要是在HotSpot还没良好移植的平台上使用的。

简单介绍一下背景:
HotSpot的源码里,平台中立的部分,有两套解释器实现:一个叫模板解释器(template interpreter),平时用的就是这个;另一个叫“C++解释器”,也就是楼主举例用的。平时用的是前者而不是后者主要是历史原因。

Anyway,用“C++解释器”来讲解概念也不是不行,不过它跟常见的一些性能测试的例子没啥关系。另外用字节码来解释性能问题很抱歉也是比较不靠谱的。字节码用于解释“语义问题”很靠谱,但看字节码是看不出性能问题的——超过它的抽象层次了。

楼主应该加一组例子,用一个自定义的异常类型:
public class JustForDemoException extends Exception {
    @Override
    public Throwable fillInStackTrace() {
        return this;
    }
}

用这个异常类来跑类似的测试看看,相信能发现些别的有趣的结论 ^_^

顺带简单说点别的:
HotSpot的解释器,在server模式上默认会开启profiling而在client模式默认不开。该解释器的profiler会记录某方法里有没有发生过某些类型的异常,或者发生异常的比例如何。这些profile信息接下来会交由JIT编译器作为优化的依据。如果一段代码虽然带有try/catch,但运行过程中实际上一直没发生过异常,那么JIT编译器会有偏向的做优化,使得不发生异常的情况是零开销的,而发生一次异常则可能触发一个很慢的过程——逆向优化(deoptimization),可能因此需要抛弃掉之前编译过的一个方法的代码让它退回到解释器里执行。这些动态特性使得在现代高性能JVM上用很简单的测试代码根本无法反映出我们平时真正写的Java程序在运行时的情况——除非谁的工作内容本身就只是写microbenchmark。另外还有一些类型的异常默认是使用隐式处理的,例如NullPointerException、除以零的ArithmeticException之类。
有兴趣知道更多细节的话欢迎讨论。

就像John Rose,Sun开发JVM的元老级工程师所说,“抛个异常什么的可以很廉价”。当然这是有具体场景在里面的咯。楼主加前面提到的那组测试之前先别读这个链接的文章,会剧透 >_<

IcyFenix 写道
bci的意思是Bytecode Instrumentation,字节码指令

首先bytecode instrumentation的意思是字节码测量,意思是在已有代码的基础上插入一些用于测量的字节码来达到获取某些数据的目的。
其次,在解释器里,这里的bci不是bytecode instrumentation,而是bytecode index,字节码索引或者说偏移量的意思。在HotSpot的解释器里除了bci这个缩写很常见之外,还有个类似的,叫bcp,是bytecode pointer,这个基本上可以看作是bci + 方法的字节码在内存中的起始地址(基地址)。

IcyFenix 写道
我们总结一下athrow指令中虚拟机可能做的事情(只会做其中一部份啦):
  • 检查栈顶异常对象类型(只检查是不是null,是否referance类型,是否Throwable的子类一般在类验证阶段的数据流分析中做,或者索性不做靠编译器保证了)
  • 把异常对象的引用出栈
  • 搜索异常表,找到匹配的异常handler
  • 重置PC寄存器状态
  • 清理操作栈
  • 把异常对象的引用入栈
  • 把异常方法的栈帧逐个出栈(这里的栈是VM栈)
  • 残忍地终止掉当前线程。
  • ……

是不是Throwable子类这个是在类加载的字节码校验阶段会做一次。对版本号为50.0或更高的Class文件(也就是Java SE 6或更高版本的)做的是类型检查(type checking verification),若失败或者是对50.0以下的版本号的Class文件则做类型推导(type inference verification)。后者是用数据流分析做的,而前者就是为了降低校验成本而做的改进,不需要繁琐的数据流分析算法。
1 楼 IcyFenix 2011-01-02  
看看反应如何,好的话明天再写一篇来小淌一下“C/C++会比Java语言快多少多少倍”这浑水,也是这2天由那几张判断字符串能不能用异常的帖子中看到的观点。

相关推荐

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

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

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

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

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

    SAP JVM 8.1 64 bits

    SAP JVM 8.1 64位是一个专为SAP系统设计的Java虚拟机,它基于Oracle的Java Development Kit (JDK) 进行优化,以满足SAP应用程序的特定需求。SAP JVM旨在提高性能、可靠性和安全性,同时确保与SAP产品的无缝集成。...

    慢慢琢磨jvm 经典

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

    jvm视频及笔记

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

    JVM中文指令手册.pdf

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

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

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

    (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探究.rar

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

    JVM 输出 GC 日志导致 JVM 卡住

    JVM 输出 GC 日志导致 JVM 卡住 JVM 输出 GC 日志导致 JVM 卡住是一个常见的问题,尤其是在高并发和高性能应用中。这个问题的根源在于 JVM 的垃圾回收机制(Garbage Collection,GC),它会在 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底层

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

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

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

    深入JVM内核—原理、诊断与优化视频教程-3.常用JVM配置参数

    在Java开发领域,JVM(Java Virtual Machine)是运行所有Java程序的核心,它负责解析字节码并执行程序。深入理解JVM的内核原理、诊断技巧以及优化方法对于提升应用性能至关重要。本教程——“深入JVM内核—原理、...

Global site tag (gtag.js) - Google Analytics