技术问答集录(七)(JVM安全点,finally)
问题:
- JVM安全点是什么概念?
- finally是如何实现的?finally中抛出异常会怎么样?
1.JVM安全点是什么概念?
安全点就是某些记录线程此时调用栈、寄存器等一些重要的数据区域里什么地方包含了GC要管理的指针(对象引用),而这些对象引用是通过OopMaps结构进行记录的,可以直接通过对OopMaps结构的访问来获得对象的引用。
安全点意味着在这个点时,所有工作线程的状态是确定的,JVM 就可以安全地执行 GC 。
当JVM发生GC时,正在执行Java code的线程必须全部停下来,才可以进行垃圾回收,也就是所谓的STW(stop the world),但是STW的背后实现原理,比如这些线程如何暂停、又如何恢复,机制是什么等,都需要涉及到一个重要的概念,那就是安全点。
从线程角度看,安全点(safepoint)可以理解成是在代码执行过程中的一些特殊位置,当线程执行到这些位置的时候,说明虚拟机当前的状态是安全的,如果有需要,可以在这个位置暂停。比如发生GC时,需要暂停所有活动线程,但是线程在这个时刻,还没有执行到一个安全点,该线程需要继续执行,直到到达下一个安全点的时候再暂停,等待GC结束。
其实,安全点的本质作用就是保证所有线程都进入安全状态的时候再进行GC操作,以确保不会把还在使用中的对象给回收掉。
实现原理:
当线程访问到被保护的内存地址时,会触发一个SIGSEGV信号,进而触发JVM的signal handler来阻塞这个线程。
安全点位置:
已经挂起的线程处于安全点(安全区域),如WAIT、BLOCK状态
被调用的方法执行之前
循环体进入下一次之前
执行native code
线程正在转换状态
如何恢复:
重新设置pooling page为可读
设置解释器为ignore_safepoints
唤醒所有挂起等待的线程
如何诊断安全点影响:
jdk9以下,jvm参数设置 -XX:+PrintGCApplicationStoppedTime打印系统停止的时间;
jdk9及以上,jvm参数设置 -Xlog:safepoint打印系统停止的时间。
2.finally是如何实现的?finally中抛出异常会怎么样?
java中的finally不可单独使用,需配合try关键字一起使用,一般是以try...catch...finally或者try...finally的形式使用。
finally中抛出异常会怎么样?
会导致一些关键逻辑无法正常执行,比如资源无法正确释放或者流无法正常关闭等,假如try块或者catch块中有返回值需要返回,finally抛异常还会导致try或catch中的返回值无法正常返回,然后程序就异常中断了。
finally 块中抛出的任何异常都会覆盖掉在其前面由 try 或者 catch 块抛出异常。与包含 return 语句的情形相似。
除必要情况下,应尽量避免在 finally 块中抛异常或者包含 return 语句。
finally是如何实现的:
finally实现方式其实是类似于try-catch;java在编译的时候对于try-catch维护一张表,制定从第几行到第几行代码发生什么类型的异常时,跳转到哪一行继续执行;
finally在编辑的时候,就是增加表中两行记录,制定了try代码块和catch代码块中发生任何异常都跳转到finally代码块中;用程序实现类似于:
源代码:
模拟finally的改写代码:
如果我们在finally中抛出异常的话,我们可以看如下示例:
源代码:
javap -v -p Test.class 之后的编译语句:
继续看编译后的字节码,先看异常表,有3条
第一行表示指令0发生Exception类型异常跳转到指令13。
第二行表示指令0发生任何类型异常(也就是Throwable类型)跳转到指令25,前提是前面没有捕获该异常。
第三行表示指令13~17发生任何类型异常则跳转到指令25。
总结:
1.try...catch是通过异常表实现的。
2.java确保finally一定会被执行,是通过复制finally代码块到每个分支实现的。
3.通常return会被编译成2个指令,当return后还有finally,return就会被编译成4个指令。
4.return后还有finally,return会将返回值存储起来,finally中并不能改变返回值(当返回值是引用类型是另外一种情况,自行研究下)。
5.return后的finally里有return,则finally里的return会结束方法。
6.try中抛出异常后,finally里有return,则方法并不会抛出异常,会正常返回
return 返回值当引用类型和值类型
private static int testReturn() { int i = 3; try { return i; } catch (Exception e) { return i; } finally { i++; System.out.println("finally 块中 i=" + i); } } int result = testReturn(); //输出结果 result= 3 System.out.println("result=" + result);
“=”号赋值是常量赋值。但是,方法的存放地址和常量的存放地址是不一样的,方法的存放在方法区的。上面我们把一个方法赋值给一个int型也没有报错。那是因为在声明方法是我们声明了返回值类型。那么编译器就会在代码的最前端预留一段返回值类型的内存。执行return的时候,就会把返回的内容写入到这段内存中 ,在执行了return之后,返回的值已经被写入到那段内存中了,finally再修改i的值,只是修改了后面代码段的i值,对返回段内存没有影响。
private static HashMap<String, String> testObjestReturn() { HashMap<String, String> map = new HashMap<>(); map.put("aa", "bb"); try { map.put("aa", "bb"); System.out.println("first"); return map; } catch (Exception e) { return map; } finally { map.put("aa", "cc"); System.out.println("second"); } } Map<String, String> map = testObjestReturn(); //输出结果 map=cc System.out.println("map=" + map.get("aa"));
当返回值不是基本数据类型的时候,其是指向一段内存的,return将返回段指向一段内存,但是代码段的依然是指向的同一段内存地址,所以当修改它指向内存中的值的时候,其实也就修改了返回段指向内存中的值,所以最终的值改变了。