1. 程式人生 > >記一次 JVM 原始碼分析(5.異常處理)

記一次 JVM 原始碼分析(5.異常處理)

異常列印

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 塊的修改不會改變返回值