异常处理 基本概念
异常基本组件
throw:异常抛出
try-catch:异常捕获
异常类型,所有异常都是 Throwable 类或者其子类的实例。Throwable 有两大直接子类。第一个是 Error,涵盖程序不应捕获的异常。
Error:系统级别异常,用户无需处理实现。(无法捕获)
当程序触发 Error 时,它的执行状态已经无法恢复,需要中止线程甚至是中止虚拟机。
Exception:由throw关键字抛出
显示异常:CheckedException,代表:IOException,SQLException;各模块业务异常,一般用户自定义。
隐式异常:RuntimeException,代表:空指针,数组越界,表示程序基本逻辑错误。
一些问题
异常性能问题:new 出一个异常非常耗费性能,因为他需要递归当前线程栈的所有栈帧,逐一的记录当前类,文件,代码行数,方法名等。
不可缓存:异常堆栈信息是在异常new的那一刻记录,和throw语句无关。
如果我们catch住某个异常,并重新抛出新的异常,会导致内存旧异常的堆栈消失。
try-catch块 针对try-catch块,也就是异常处理代码块。其核心逻辑如下:
try块 :用来监控用户的代码是否抛出异常。
catch块 :用来捕获try块的异常。
一次能catch多个异常种类,并且可以多次生明catch块;
上一层的catch块不能覆盖下面层级的catch块申明的异常;
catch块也会抛出异常,但不会被下面层级的异常捕获,会直接抛出。
finally块 :执行必须执行的操作,如资源释放等。注意finally块代码在任何情况 下都会执行。参考一下几种情况:
try{throw ex}->catch->finally
try{throw ex}->no catch match->finally->method throw
try{throw ex_a}->catch(ex_a){throw ex_b}->finally->method throw ex_b:即便catch抛出新的异常,finally也会执行。
虚拟机捕获异常 基本原理 虚拟机通过异常表来监控方法异常抛出和捕获(每个方法都会有一个异常表)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static void main (String[] args) { try { mayThrowException(); } catch (Exception e) { e.printStackTrace(); } }public static void main (java.lang.String[]) ; Code: 0 : invokestatic mayThrowException:()V 3 : goto 11 6 : astore_1 7 : aload_1 8 : invokevirtual java.lang.Exception.printStackTrace 11 : return Exception table: from to target type 0 3 6 Class java/lang/Exception
如上图,异常表包含
from:表达当前异常监控起始位置;
to:表达当前异常监控截至位置;
type:表达所匹配的异常类型;
target:如果触发当前异常条目,则跳转到target指向的代码。
具体执行流程:
方法抛出异常;
调用异常表逐一查看;
判断是否在from-to范围内;
判断是否匹配type异常类型;
获取target指向的代码行,执行跳转;
另外,如果无法找到匹配的异常type类型,会弹出当前方法堆栈,给到上一层调用者的异常表执行这个步骤。
关于finally块 finally块会比较特殊,在当前的java版本。
正常流程:编译器会把finally块代码复制到try区域末尾和所有catch区域末尾,这样确保了正常流程均会执行finally块。
抛出异常:编译器会额外生成一个异常表条目在末尾,监控整个try-catch块区域。
from-to:整个try-catch区域
type:为any匹配所有异常,
target:一个特殊的finally块,他会在最后重新抛出当前异常表匹配的异常。
如下所示:finally块有三分
在catch代码块末尾,用于处理正常catch的流程
在上一个finally块末尾(即22行开始),用于捕获上述代码未被捕获的异常和catch块抛出的新异常(即全局catch)
即第五行程序goto指向的30行,表示程序正常结束。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 public class Foo { private int tryBlock; private int catchBlock; private int finallyBlock; private int methodExit; public void test () { try { tryBlock = 0 ; } catch (Exception e) { catchBlock = 1 ; } finally { finallyBlock = 2 ; } methodExit = 3 ; } } $ javap -c Foo ... public void test () ; Code: 0 : aload_0 1 : iconst_0 2 : putfield #20 5 : goto 30 8 : astore_1 9 : aload_0 10 : iconst_1 11 : putfield #22 14 : aload_0 15 : iconst_2 16 : putfield #24 19 : goto 35 22 : astore_2 23 : aload_0 24 : iconst_2 25 : putfield #24 28 : aload_2 29 : athrow 30 : aload_0 31 : iconst_2 32 : putfield #24 35 : aload_0 36 : iconst_3 37 : putfield #26 40 : return Exception table: from to target type 0 5 8 Class java/lang/Exception 0 14 22 any ...
Suppressed 异常和语法糖 异常Suppressed问题 在某些特殊情况下,异常catch,finally代码块中也会抛出新的异常。如果我们不做特殊处理,新的异常会覆盖我们原有的异常。
如何将这两种异常归并到一起呢?
catch块 很好处理,因为它们获取到旧异常的指针,可以自定义归并,如下:
1 2 3 4 catch (Exception old){ ... throw new Exception (old); }
finally块 并没有这个指针,我们要么选择抛出新的异常,要么原地catch。是否对如下代码很常见?
1 2 3 4 5 6 7 8 9 10 11 12 13 public static void main (String[] args) { InputStream stream = new ByteArrayInputStream (new byte []{}); try { } finally { try { stream.close(); } catch (IOException e) { } } }
我们最多在内层的catch块打印一个新的日志用于排查。针对这个问题,我们需要用到try-with-resources 语法糖 ,它能自动帮助我们在字节码层面自动使用 Suppressed 异常(即将新的异常依附在原有异常之上)。
try-with-resources 语法糖 除了Suppressed异常外,该语法糖更多的用途是自动帮助我们处理资源回收等场景。只需要我们实现AutoClosable接口的内容即可;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class Foo implements AutoCloseable { private final String name; public Foo (String name) { this .name = name; } @Override public void close () { throw new RuntimeException (name); } public static void main (String[] args) { try (Foo foo0 = new Foo ("Foo0" ); Foo foo1 = new Foo ("Foo1" ); Foo foo2 = new Foo ("Foo2" )) { throw new RuntimeException ("Initial" ); } } } Exception in thread "main" java.lang.RuntimeException: Initial at Foo.main(Foo.java:18 ) Suppressed: java.lang.RuntimeException: Foo2 at Foo.close(Foo.java:13 ) at Foo.main(Foo.java:19 ) Suppressed: java.lang.RuntimeException: Foo1 at Foo.close(Foo.java:13 ) at Foo.main(Foo.java:19 ) Suppressed: java.lang.RuntimeException: Foo0 at Foo.close(Foo.java:13 ) at Foo.main(Foo.java:19 )
反射 反射的作用:
提供编辑期查询可用方法,如eclips
提供依据namespace+classname,创建对象,调用对象,如Spring-ioc框架
实现原理 其原理就是JVM依据反射的对象类型,和参数,和方法名,可以找到方法内存地址,最终执行方法调用,将参数传递进去即可。
如下时反射源码:
1 2 3 4 5 6 7 8 9 10 public final class Method extends Executable { public Object invoke (Object obj, Object... args) { MethodAccessor ma = methodAccessor; if (ma == null ) { ma = acquireMethodAccessor(); } return ma.invoke(obj, args); } }
可见最终会通过MethodAccessor的指针去指向一个具体的类型去调用反射,它是一个Interface,实现有如下几个:
DelegatingMethodAccessorImpl:代理实现(基于委派模式)
NativeMethodAccessorImpl:本地实现,由c++代码提供反射调用
上述DelegatingMethodAccessorImpl(委派实现)底层会最终调用 NativeMethodAccessorImpl(本地实现) 。而之所以还需要一个代理机制是因为JVM对反射进行了动态实现 的优化,从而能够在本地实现以及动态实现中切换 。
GeneratedMethodAccessor1(动态实现):由JVM自动生成字节码触发调用
动态实现的调用性能更高,通常比NativeMethodAccessorImpl本地实现快20倍以上;
反之,由于需要字节码生成,初始化过程较慢;仅仅调用一次的话反而本地实现快3-4倍。
java针对以上动态实现 和本地实现 的优劣,进行了优化
默认16次调用之后启用动态实现;
-Dsun.reflect.noInflation=true
可以关闭本地实现直接使用动态实现,会直接使用动态实现不会触发本地实现和委派实现 ;
-Dsun.reflect.inflationThreshold
可以修改默认16次的动态实现触发阈值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 static void test_invoke (int count) { try { TestPrint testObj = new TestPrint (); String name = TestPrint.class.getName(); Class<?> cls = Class.forName(name); Method method = cls.getMethod("test" , int .class); for (int i = 1 ; i < count; i++) { try { method.invoke(testObj, i); } catch (Exception e) { e.printStackTrace(); } } } catch (Exception e) { } } static class TestPrint { public void test (int count) throws Exception { throw new Exception ("my expection = " + count); } } java.lang.reflect.InvocationTargetException at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77 ) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43 ) at java.base/java.lang.reflect.Method.invoke(Method.java:569 ) at org.example.Main.test_invoke(Main.java:21 ) at org.example.Main.main(Main.java:8 ) Caused by: java.lang.Exception: my expection = 16 at org.example.Main$TestPrint.test(Main.java:32 ) ... 6 more java.lang.reflect.InvocationTargetException at jdk.internal.reflect.GeneratedMethodAccessor1.invoke(Unknown Source) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43 ) at java.base/java.lang.reflect.Method.invoke(Method.java:569 ) at org.example.Main.test_invoke(Main.java:21 ) at org.example.Main.main(Main.java:8 ) Caused by: java.lang.Exception: my expection = 17 at org.example.Main$TestPrint.test(Main.java:32 ) ... 5 more
反射调用开销 反射调用成本计算
Class.forName():从JVM中根据命名空间查找类型class;
Class.getMethod():从class类型中查找方法;
此过程会遍历整个方法列表,如果本类没有回从父类等一次找;
此过程会生成一个Method对象的副本返回,如果返回一整个方法列表,性能开销更大。
反射回重复校验访问权限,可以关闭method.setAccessible(true);
针对上述问题我们可以将Class和Method都缓存起来。
剩下的就只需要关注Method.Invoke的性能开销 了。
关于Invoke性能开销 以下代码用来观察反射调用 和直接调用 的性能差距,我们取最后五次的结果。
v1_方法参数int:128 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public class Main { public static void main (String[] args) throws Exception { directInvoke(2_000_000_000 ); reflectInvoke_v1(2_000_000_000 ); } static void directInvoke (int count) { long before = System.currentTimeMillis(); for (int i = 1 ; i < count; i++) { if (i%100_000_000 == 0 ) { long after = System.currentTimeMillis(); System.out.println(after-before); before = after; } Test.test(128 ); } } static void reflectInvoke_v1 (int count) throws Exception { long before = System.currentTimeMillis(); String name = Test.class.getName(); Class<?> cls = Class.forName(name); Method method = cls.getMethod("test" , int .class); method.setAccessible(true ); for (int i = 1 ; i < count; i++) { if (i%100_000_000 == 0 ) { long after = System.currentTimeMillis(); System.out.println(after-before); before = after; } method.invoke(null , 128 ); } } static class Test { public static void test (int count) { } } }
直接调用(毫秒)
反射调用(毫秒)
差距(倍数)
82
202
2.46
81
204
2.52
81
204
2.52
83
194
2.34
82
202
2.46
得出平均基准差距2.46倍
v2_方法缓存参数数组
由于反射接受的参数是一个数组,如果我们只传入一个int值,那么它会自动构建一个数组去封装这个参数。我们可以提前构建数组:
避免数组重复创建;
避免Integer对象自动装配创建;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 static void reflectInvoke_v1 (int count) throws Exception { long before = System.currentTimeMillis(); String name = Test.class.getName(); Class<?> cls = Class.forName(name); method.setAccessible(true ); Method method = cls.getMethod("test" , int .class); Object[] value = {128 }; for (int i = 1 ; i < count; i++) { if (i%100_000_000 == 0 ) { long after = System.currentTimeMillis(); System.out.println(after-before); before = after; } method.invoke(null , value); } }
比基准慢2.18略有提升
V3_使用常量池
如果上述方案替换使用常量池,只避免重复创建Integer对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static void main (String[] args) throws Exception { long before = System.currentTimeMillis(); String name = Test.class.getName(); Class<?> cls = Class.forName(name); Method method = cls.getMethod("test" , int .class); method.setAccessible(true ); for (int i = 1 ; i < 2_000_000_000 ; i++) { if (i % 100_000_000 == 0 ) { long after = System.currentTimeMillis(); System.out.println(after - before); before = after; } method.invoke(null , 127 ); } }static class Test { public static void test (int arg) {} }
是基准的1.5倍,快很多。
当前优化比V2版本少了缓存Object[]对象,为什么频繁创建Object[]数组对象反而会性能更快?
无GC触发:java会对方法内的栈变量进行逃逸分析,如果能确保其只在方法中使用便会将对象创建在栈空间内(实际更复杂)。从而避免了GC触发导致的性能损耗。
即时编译器无法优化:V2版本由于数组在循环外面,编译器无法识别数组是否发生变更,因而无法优化对数组的访问操作。
V4_添加NoInflation参数 添加启动项参数,避免委派实现:java -XX:+PrintGC -Dsun.reflect.noInflation=true org.example.Main
1.2倍,很接近了!!
之所以反射调用能够变得这么快,主要是因为即时编译器中的方法内联。在关闭了 Inflation 的情况下,内联的瓶颈在于 Method.invoke 方法中对 MethodAccessor.invoke 方法的调用。
关于内联的瓶颈:原因在于MethodAccessor.invoke的调用点需要记录的类型可能有很多,即a.invoke,b.invoke,c.invoke…
无法内联的损耗 上述优化都使用到了方法内联,但如果在调用者种类过多的情况下会发生啥。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 public class Main { public static void main (String[] args) throws Exception { long before = System.currentTimeMillis(); String name = Test.class.getName(); Class<?> cls = Class.forName(name); Method method = cls.getMethod("test" , int .class); method.setAccessible(true ); polluteProfile(); for (int i = 1 ; i < 2_000_000_000 ; i++) { if (i % 100_000_000 == 0 ) { long after = System.currentTimeMillis(); System.out.println(after - before); before = after; } method.invoke(null , 127 ); } } public static void polluteProfile () throws Exception { Method method1 = Test.class.getMethod("test1" , int .class); Method method2 = Test.class.getMethod("test2" , int .class); for (int i = 0 ; i < 2000 ; i++) { method1.invoke(null , 0 ); method2.invoke(null , 0 ); } } static class Test { public static void test (int arg) {} public static void test1 (int i) {} public static void test2 (int i) {} } } console结果712 [GC (Allocation Failure) 276789K->821K(610304K), 0.0019333 secs] [GC (Allocation Failure) 267573K->821K(601600K), 0.0010575 secs] [GC (Allocation Failure) 258869K->821K(593408K), 0.0012190 secs] [GC (Allocation Failure) 250677K->821K(585216K), 0.0015212 secs] [GC (Allocation Failure) 242485K->821K(633856K), 0.0020997 secs] [GC (Allocation Failure) 291125K->821K(624128K), 0.0013386 secs] [GC (Allocation Failure) 281397K->821K(614912K), 0.0012273 secs] [GC (Allocation Failure) 272181K->821K(606208K), 0.0014130 secs] [GC (Allocation Failure) 263477K->821K(597504K), 0.0012301 secs]728 [GC (Allocation Failure) 254773K->821K(589312K), 0.0009970 secs] [GC (Allocation Failure) 246581K->821K(581632K), 0.0019493 secs] [GC (Allocation Failure) 238901K->821K(629248K), 0.0021713 secs] [GC (Allocation Failure) 286517K->821K(686592K), 0.0014060 secs] [GC (Allocation Failure) 343861K->821K(755200K), 0.0017024 secs] [GC (Allocation Failure) 412469K->821K(837632K), 0.0017687 secs] [GC (Allocation Failure) 494901K->821K(818176K), 0.0009257 secs]711 [GC (Allocation Failure) 475445K->821K(799744K), 0.0011703 secs] [GC (Allocation Failure) 457013K->821K(782336K), 0.0010471 secs] [GC (Allocation Failure) 439605K->821K(765440K), 0.0011883 secs] [GC (Allocation Failure) 422709K->821K(749568K), 0.0011071 secs] [GC (Allocation Failure) 406837K->821K(734208K), 0.0014714 secs]
8.59倍,差很多了
原因是:
JVM只会缓存部分调用者类型,超过两个则会放弃内联(XX:TypeProfileWidth=x可以修改);
逃逸分析也不再生效,会触发若干GC。
优化