第十一章 用Java实现JVM之异常处理
用Java实现JVM目录
第零章 用Java实现JVM之随便说点什么
第一章 用Java实现JVM之JVM的准备知识
第二章 用Java实现JVM之命令行工具
第三章 用Java实现JVM之查找Class文件
第四章 用Java实现JVM之解析class文件
第五章 用Java实现JVM之运行时数据区
第六章 用Java实现JVM之指令集和解释器
第七章 用Java实现JVM之类和对象
第八章 用Java实现JVM之方法调用和返回
第九章 用Java实现JVM之数组和字符串
第十章 用Java实现JVM之本地方法调用
第十一章 用Java实现JVM之异常处理
第十二章 用Java实现JVM之结束
文章目录
- 用Java实现JVM目录
- 前言
- 异常
- 一、错误(Error)
- 二、异常(Exception)
- 1. **受检异常(Checked Exception)**
- 2. **非受检异常(Unchecked Exception)**
- 代码实现
- 异常处理表
- `athrow`指令
- 异常打印
- 测试
- 总结
前言
上一篇我们已经实现了本地方法,今天开启新的征程,继续往下。聚焦于异常处理异常
从大的方向来看,Java 在运行过程中可能遇到的问题,主要可以划分为两大类:错误(Error) 和 异常(Exception)。这两者虽然在表现上可能都是程序运行中断,但它们的含义、产生原因、处理方式却截然不同。
一、错误(Error)
错误 通常指的是一些 严重的系统级问题,这些问题非程序本身能处理,一旦出现,基本就意味着 JVM 自身出了问题,程序无法继续运行。例如:
-
OutOfMemoryError
:内存溢出,JVM 无法为对象分配内存 -
StackOverflowError
:栈空间溢出,通常是无限递归调用导致 -
VirtualMachineError
:虚拟机内部错误,如 JIT 编译器 崩溃这类错误并不推荐用
try-catch
来捕获,也不建议也不可能完全恢复,更像是一种“致命信号”:程序可能已经无法继续安全运行。通常我们只能记录日志,或通知运维重启服务,根本原因往往是代码设计或配置不当,或者系统资源不足
二、异常(Exception)
相比之下,异常 则是 应用级别的问题,指程序运行过程中可预期、可以被捕获和处理的问题。它们一般不会导致整个 JVM 崩溃,但需要程序员显式处理。例如:
-
NullPointerException
:空指针异常。 -
IOException
:输入输出异常,文件或网络读取失败。 -
SQLException
:数据库操作时出错。在 Java 中,异常又被进一步分为两类:
1. 受检异常(Checked Exception)
- 特点:必须显示处理(编译期检查),否则编译器报错
- 常见场景:文件操作、数据库连接、网络通信等
- 例如:
IOException
、SQLException
- 解决方式:用
try-catch
捕获,或者用throws
抛出
2. 非受检异常(Unchecked Exception)
- 特点:运行时才出现,不强制处理(编译期不报错)
- 通常是程序逻辑上的错误,如访问空对象、数组越界等
- 例如:
NullPointerException
、IndexOutOfBoundsException
- 解决方式:可以用
try-catch
捕获,也可以通过加强逻辑判断来避免
代码实现
梳理完 Java 的异常体系后,回到现实的问题,JVM 要如何处理异常?在日常开发出,我们可能抛出来一个或多个异常,出现异常后也会打印出来报错的行数。异常发生时程序需要做什么,不妨再深入想一想:
- 当异常抛出时,它具体从哪里“冒出来”?
- 它是怎么被虚拟机捕获的?
- 处理异常的代码又是怎么被找到的?
回想平时用到的 try-catch
,它本质上就是一段告诉虚拟机: “如果在这段代码里出现了某种异常,跳转到这里处理它”。但虚拟机又是如何知道这其中的对应关系?经过前面一系列的洗礼,一旦出现多个XX,基本上都会存在一张表。异常也不例外。字节码文件里藏着一张“异常表”(Exception Table),这张表详细记录了每个 try-catch
的起止位置,以及能处理哪些异常类型。当执行到某条指令时,如果抛出了异常,虚拟机会查这张表,看看当前指令是否在某个 try 区间内;如果在,再根据异常类型找到对应的catch
块。如果找到,就调整程序计数器,跳转到对应的 catch 代码继续执行。如果找不到,说明当前方法不能处理,虚拟机就会弹出当前调用栈,回到调用它的方法,重复这个查找过程。直到找到处理代码,或者调用栈空了,异常传递无果,虚拟机才会把异常打印出来
这样一来,异常的捕获与处理就像在字节码里有一个“地图”,告诉虚拟机:“遇到异常,去这里走。”同时,为了让错误提示更友好,定位到具体的源码行号,字节码中还有专门的“行号表”(LineNumberTable),将字节码指令的偏移量对应到源码的行号。当异常发生,虚拟机结合异常抛出的指令地址和行号表,就能告诉你报错的代码行在哪里。因为源代码中,很多东西是给人看的,不是给虚拟机看的。所以在编译的过程中会过滤掉这些,导致字节码的行号和源代码的行号对不上
这就是 JVM 规范 对异常处理的基本设计:
- 异常表(Exception Table)
- 行号表(LineNumberTable)
- 程序计数器(PC)用于定位当前执行指令
- 调用栈帧的弹出与跳转机制
+-----------+----------+-------------+------------+
| start_pc | end_pc | handler_pc | catch_type |
+-----------+----------+-------------+------------+
| 0 | 20 | 30 | NumberFormatException |
+-----------+----------+-------------+------------+
异常处理表
巴拉了这么多,先把字节码中的 异常处理表 解析出来再说。JMethod
添加如下代码:
private List<JMethodExceptionTable> catchExceptionTable;private List<JClassRef> throwExceptionTable;@Overrideprotected void newDescInfo(AttributeInfo attr, JClass jClass, List<ConstantPool> constantPool) {if (attr instanceof Code) {Code code = (Code) attr;this.setCode(code.getInstructionCode());this.setMaxStack(code.getMaxStack());this.setMaxLocals(code.getMaxLocals());resolveExceptionTable(code, constantPool);resolveAttrs(code, constantPool);} else if (attr instanceof Exceptions) {this.resolveExceptions((Exceptions) attr, constantPool);} else if (attr instanceof RuntimeVisibleParameterAnnotations) {RuntimeVisibleParameterAnnotations paramAnno = (RuntimeVisibleParameterAnnotations) attr;this.paramAnnos = paramAnno.getByteCodes();}}private void resolveExceptions(Exceptions ex, List<ConstantPool> constantPool) {List<Integer> indexList = ex.getExceptionIndexTable();if (CollectionUtils.isNotEmpty(indexList)) {for (Integer index : indexList) {JClassRef jClassRef = (JClassRef) constantPool.get(index).getVal();this.throwExceptionTable.add(jClassRef);}}}
athrow
指令
代码上的功能,对应到虚拟机都是由指令来完成。异常也是一样的。抛出异常的指令为athrow
。整个指令的功能就是根据栈顶的异常对象,去异常表进行匹配。RefInstruction#execute()
改动如下:
case ATHROW: {//抛出异常信息JObject ref = popOperandStackRefVal();if (ref == null) {throw new NullPointerException();}while (!jThread.getJvmStack().isEmpty()) {//回到上一个指令int pc = jThread.getNextPc().get() - 1;int handlerPC = getJMethod().findExceptionHandler(ref.getJClass(), pc);if (handlerPC != -1) {getStackFrame().getOperandStack().getStack().clear();offerIncrement(handlerPC - 1);break;}jThread.getJvmStack().pop();}if (jThread.getJvmStack().isEmpty()) {JField jField = ref.getJClass().getJField("detailMessage", "Ljava/lang/String;");String msg = new String((char[]) ((JObject) (((JObject) ref.getField(jField.getSlotId()).getVal())).getFields()[0].getVal()).getData());System.err.println(ref.getJClass().getClassName() + ": " + msg);end();}break;}
异常打印
异常信息的打印还需要本地方法支持。新增JThrowableNativeRegistry
类,代码如下:
public class JThrowableNativeRegistry extends JNativeRegistry {private static final JThrowableNativeRegistry instance = new JThrowableNativeRegistry();static {registry("java/lang/Throwable", "fillInStackTrace", "(I)Ljava/lang/Throwable;", JThrowableNativeRegistry::fillInStackTrace);}protected static void fillInStackTrace(JThread jThread) {JObject ref = jThread.getJvmStack().getTop().getLocalVars().getRefVal(0);jThread.getJvmStack().pushOperandStackRefVal(ref);ref.setExtra(createStackTraceElements(ref, jThread));}protected static JStackTraceElement[] createStackTraceElements(JObject exJObject, JThread jThread) {int skip = distanceToObject(exJObject.getJClass()) + 2;StackFrame[] sfs = new StackFrame[jThread.getJvmStack().size() - skip];JStackTraceElement[] jstes = new JStackTraceElement[sfs.length];for (int i = 0; i < sfs.length; i++) {StackFrame sf = jThread.getJvmStack().elementAt(sfs.length - i - 1);JClass jc = sf.getJClass();JMethod jm = sf.getJMethod();JStackTraceElement ste = new JStackTraceElement(jc.getSourceFile(), jc.getClassName(), jm.getName(), jm.getLineNumber(sf.getReturnAddress().get() - 1));jstes[i] = ste;}return jstes;}private static int distanceToObject(JClass exJClass) {int distance = 0;for (JClass jc = exJClass.getSuperClass(); jc != null; jc = jc.getSuperClass()) {distance++;}return distance;}public static JThrowableNativeRegistry getInstance() {return instance;}/*** 虚拟机栈信息*/@Getter@Setter@NoArgsConstructor@AllArgsConstructorstaticclass JStackTraceElement {private String fileName;private String className;private String methodName;private Integer lineNumber;}
}
测试
ok,接下来进入测试环节,验证今天的努力成果。新增ParseIntTest
类,代码如下:
public class ParseIntTest {public static void main(String[] args) {foo(args);}private static void foo(String[] args) {try {bar(args);} catch (NumberFormatException e) {System.out.println(e.getMessage());}}private static void bar(String[] args) {if (args.length == 0) {throw new IndexOutOfBoundsException("no args!");}int x = Integer.parseInt(args[0]);System.out.println(x);}
}
在添加一个测试类,ExTest
代码如下:
public class ExTest {public static void main(String[] args) {CmdCommand cmdCommand = new CmdCommand();cmdCommand.parseCmd(args);}
}
idea
增加两个启动类,ExTest-normal
配置如下:
-Xjre "D:\Oracle\Java\jdk1.8.0_281\jre" -cp "Z:\code\jjvm\ch10\target\test-classes" com.hqd.jjvm.ex.ParseIntTest aa bb
idea
增加两个启动类,ExTest-ex
配置如下:
-Xjre "D:\Oracle\Java\jdk1.8.0_281\jre" -cp "Z:\code\jjvm\ch10\target\test-classes" com.hqd.jjvm.ex.ParseIntTest
测试结果如下:
有参数的顺利打印出来参数,无参数的抛出了异常
总结
今天主要讲述异常处理,整个 JVM 的实现进度已经进入尾声了。。。