记一次 JVM 源码分析(5.异常处理)

17 篇文章 0 订阅
13 篇文章 1 订阅

异常打印

Java 如果发生异常,通常会调用 Throwable.printStackTrace 去打印堆栈信息。
堆栈信息包括完整类名,方法名,java 文件名,行号
而这样的信息根据发生 Crash 线程所经历的n个方法会打印出n行。
整个过程被称为栈回朔

栈回朔

栈回朔的过程发生于异常被 New 出来的时候

Throwable.backtrace 这个 Throwbale 的成员变量就是用来保存栈回朔链表的

 /**
     * WARNING: this must be the second variable. Native code saves some
     * indication of the stack backtrace in this slot.
     */
    private transient Object backtrace = buildStackElement();//

	private native StackTraceElement buildStackElement();

可见 Throwable 初始化的时候会调用 native 方法 buildStackElement()
StackTraceElement

public class StackTraceElement {

    private String declaringClass;
    private String methodName;
    private String fileName;
    private int lineNumber;
    
    StackTraceElement parent;
	.......
}

buildStackElement()

s32 java_io_Throwable_buildStackElement(Runtime *runtime, JClass *clazz) {
    RuntimeStack *stack = runtime->stack;
    Instance *tmps = (Instance *) localvar_getRefer(runtime->localvar, 0);
#if _JVM_DEBUG_BYTECODE_DETAIL > 5
    invoke_deepth(runtime);
    jvm_printf("java_io_Throwable_buildStackElement %s \n", utf8_cstr(tmps->mb.clazz->name));
#endif
    Instance *ins = buildStackElement(runtime, runtime->parent);
    push_ref(stack, ins);
    return 0;
}

这里从操作数栈中取出当前 Throwbale 对象,runtime 是当前栈帧即调用 buildStackElement() 的栈帧,所以回朔开始应该是上一个栈帧。

//生成堆栈元素对象 StackTraceElement
Instance *buildStackElement(Runtime *runtime, Runtime *target) {
    JClass *clazz = classes_load_get_c(STR_CLASS_JAVA_LANG_STACKTRACE, target);
    if (clazz) {
        Instance *ins = instance_create(runtime, clazz);
        gc_refer_hold(ins);
        instance_init(ins, runtime);
        c8 *ptr;
        //方法所在类
        ptr = getFieldPtr_byName_c(ins, STR_CLASS_JAVA_LANG_STACKTRACE, "declaringClass", STR_INS_JAVA_LANG_STRING, runtime);
        if (ptr) {
            Instance *name = jstring_create(target->clazz->name, runtime);
            setFieldRefer(ptr, name);
        }
        //调用的方法名
        ptr = getFieldPtr_byName_c(ins, STR_CLASS_JAVA_LANG_STACKTRACE, "methodName", STR_INS_JAVA_LANG_STRING, runtime);
        if (ptr) {
            Instance *name = jstring_create(target->method->name, runtime);
            setFieldRefer(ptr, name);
        }
        //java 源文件名
        ptr = getFieldPtr_byName_c(ins, STR_CLASS_JAVA_LANG_STACKTRACE, "fileName", STR_INS_JAVA_LANG_STRING, runtime);
        if (ptr) {
            Instance *name = jstring_create(target->clazz->source, runtime);
            setFieldRefer(ptr, name);
        }
        //代码所在行号
        ptr = getFieldPtr_byName_c(ins, STR_CLASS_JAVA_LANG_STACKTRACE, "lineNumber", "I", runtime);
        if (ptr) {
            if (target->method->access_flags & ACC_NATIVE) {
                setFieldInt(ptr, -1);
            } else {
                setFieldInt(ptr, getLineNumByIndex(target->ca, (s32) (target->pc - target->ca->code)));
            }
        }
        //递归,如果还有父方法栈则递归生成父方法栈信息
        if (target->parent && target->parent->parent) {
            ptr = getFieldPtr_byName_c(ins, STR_CLASS_JAVA_LANG_STACKTRACE, "parent", "Ljava/lang/StackTraceElement;", runtime);
            if (ptr) {
                Instance *parent = buildStackElement(runtime, target->parent);
                setFieldRefer(ptr, parent);
            }
        }
        gc_refer_release(ins);
        return ins;
    }
    return NULL;
}
  • new StackTraceElement 并且向内部填充栈帧信息(类名,源码名,方法名,行号)
  • 递归生成夫栈帧的 StackTraceElement

打印栈回朔信息

那么当调用 printStacktrace 的时候就很简单了。

  • Java 层实现
public void printStackTrace(Writer writer) {
        try {
            writer.write(getCodeStack());
        } catch (IOException ex) {
        }

    }
    public String getCodeStack() {
        StringBuilder stack = new StringBuilder();
        String msg = getMessage();
        stack.append(this.getClass().getName()).append(": ").append(msg == null ? "" : msg).append("\n");
        if (backtrace != null) {
            StackTraceElement sf = (StackTraceElement) backtrace;
            while (sf != null) {
                try {
                    Class clazz = Class.forName(sf.getDeclaringClass());
                    if (!clazz.isAssignableFrom(Throwable.class)) {
                        stack.append("    at ").append(sf.getDeclaringClass());
                        stack.append(".").append(sf.getMethodName());
                        stack.append("(").append(sf.getFileName());
                        stack.append(":").append(sf.getLineNumber());
                        stack.append(")\n");
                    }
                    sf = sf.parent;
                } catch (Exception e) {
                }
            }
        }
        return stack.toString();
    }
  • Native
//打印异常
void print_exception(Runtime *runtime) {
    __refer ref = pop_ref(runtime->stack);
    Instance *ins = (Instance *) ref;
    Utf8String *getStackFrame_name = utf8_create_c("getCodeStack");
    Utf8String *getStackFrame_type = utf8_create_c("()Ljava/lang/String;");
    MethodInfo *getStackFrame = find_methodInfo_by_name(ins->mb.clazz->name, getStackFrame_name,
                                                        getStackFrame_type, runtime);
    utf8_destory(getStackFrame_name);
    utf8_destory(getStackFrame_type);
    if (getStackFrame) {
        push_ref(runtime->stack, ins);
        s32 ret = execute_method_impl(getStackFrame, runtime, getStackFrame->_this_class);
        if (ret != RUNTIME_STATUS_NORMAL) {
            ins = pop_ref(runtime->stack);
            return;
        }
        ins = (Instance *) pop_ref(runtime->stack);
        Utf8String *str = utf8_create();
        jstring_2_utf8(ins, str);
        printf("%s\n", utf8_cstr(str));
        utf8_destory(str);
    } else {
        printf("ERROR: %s\n", utf8_cstr(ins->mb.clazz->name));
    }
}

异常抛出

异常产生

  • 虚拟机运行时,内部发出的异常,如没有搜寻到某方法,称为虚拟机内部异常
  • 在 Java 代码中使用 throw 抛出的异常

虚拟机内部异常
如下 Class.forName 如果没有找到对应的 Class 则从虚拟机内部抛出 ClassNotFoundException
抛出流程一般是:

  • New 出内部异常的实例
  • 返回值设为 RUNTIME_STATUS_EXCEPTION,通知解释器下一步跳转到异常处理 Handler
s32 java_lang_Class_forName(Runtime *runtime, JClass *clazz) {
    RuntimeStack *stack = runtime->stack;
    Instance *jstr = (Instance *) localvar_getRefer(runtime->localvar, 0);
    JClass *cl = NULL;
    s32 ret = RUNTIME_STATUS_NORMAL;
    if (jstr) {
       ......
       cl = classes_load_get(ustr, runtime);
        if (!cl) {
            Instance *exception = exception_create(JVM_EXCEPTION_CLASSNOTFOUND, runtime);
            push_ref(stack, (__refer) exception);
            ret = RUNTIME_STATUS_EXCEPTION;
        } else {
            .....
        }
    } else {
        Instance *exception = exception_create(JVM_EXCEPTION_NULLPOINTER, runtime);
        push_ref(stack, (__refer) exception);
        ret = RUNTIME_STATUS_EXCEPTION;
    }
    .....
    return ret;
}

Java 中抛出异常

  • 操作数栈中取出目标异常的对象并推入本地变量
  • 返回值设为 RUNTIME_STATUS_EXCEPTION,通知解释器下一步跳转到异常处理 Handler
case op_athrow: {
                        Instance *ins = (Instance *) pop_ref(stack);
                        push_ref(stack, (__refer) ins);

#if _JVM_DEBUG_BYTECODE_DETAIL > 5
                        invoke_deepth(runtime);
                    jvm_printf("athrow  [%llx].exception throws  \n", (s64)(intptr_t)ins);
#endif
                        //opCode +=  1;
                        ret = RUNTIME_STATUS_EXCEPTION;
                        break;
                    }

异常处理

在 Switch 解释器取指令循环体的末尾,如果判断到上一次指令执行的返回值 ret == RUNTIME_STATUS_EXCEPTION,则代表现在需要进入异常分发的流程。

  • 循环对比 Code 属性中的异常向量表,如果 catch 的类型符合抛出异常的类型的话,则进入该分支
else if (ret == RUNTIME_STATUS_EXCEPTION) {
                    //取出目标异常对象
                    Instance *ins = pop_ref(stack);
                    //jvm_printf("stack size:%d , enter size:%d\n", stack->size, stackSize);
                    //restore stack enter method size, must pop for garbage
                    while (stack->size > stackSize)pop_empty(stack);
                    push_ref(stack, ins);

                    //                    if (utf8_equals_c(ins->mb.clazz->name, "espresso/util/NotConstant")) {
                    //                        int debug = 1;
                    //                    }

#if _JVM_DEBUG_BYTECODE_DETAIL > 3
                    s32 lineNum = getLineNumByIndex(ca, runtime->pc - ca->code);
                    printf("   at %s.%s(%s.java:%d)\n",
                        utf8_cstr(clazz->name), utf8_cstr(method->name),
                        utf8_cstr(clazz->name),
                        lineNum
                    );
#endif
                    //从异常向量表中找到合适的异常 Handler,即对应的 catch 分支
                    ExceptionTable *et = _find_exception_handler(runtime, ins, ca, (s32) (opCode - ca->code), ins);
                    if (et == NULL) {
                        break;
                    } else {
#if _JVM_DEBUG_BYTECODE_DETAIL > 3
                        jvm_printf("Exception : %s\n", utf8_cstr(ins->mb.clazz->name));
#endif
                        //跳转到合适的分支
                        opCode = (ca->code + et->handler_pc);
                        ret = RUNTIME_STATUS_NORMAL;
                    }
                }

static ExceptionTable *
_find_exception_handler(Runtime *runtime, Instance *exception, CodeAttribute *ca, s32 offset, __refer exception_ref) {
    Instance *ins = (Instance *) exception_ref;

    s32 i;
    ExceptionTable *e = ca->exception_table;
    for (i = 0; i < ca->exception_table_length; i++) {

        if (offset >= (e + i)->start_pc
            && offset <= (e + i)->end_pc) {
            if (!(e + i)->catch_type) {
                return e + i;
            }
            ConstantClassRef *ccr = class_get_constant_classref(runtime->clazz, (e + i)->catch_type);
            JClass *catchClass = classes_load_get(ccr->name, runtime);
            //catch 类型和抛出类型对比
            if (instance_of(catchClass, exception, runtime))
                return e + i;
        }
    }
    return NULL;
}

关于 finally 块
finally 块在字节码中并没有什么特殊的标志,正常来说它会紧跟在 try 块之后, try 完,catch 块处理完就会走到 finally 块。如果 try 块中含有 return,则 return 指令会被编译器放到 finally 后面,需要注意的是 retrun 的返回值,在 try 块末尾就回被保存起来准备返回,finally 块的修改不会改变返回值

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值