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