java栈帧还是需要一些数据支持常量池的解析、正常方法的返回和异常的处理。大部分的java字节码指令需要进行常量池的访问,在栈帧数据区中保存着访问常量池的指针,方便程序访问java常量池。如下图所示:
当函数返回或者程序出现异常的时候,jvm虚拟机必须恢复调用者函数的栈帧,并且让调用者函数继续执行。什么意思呢?举一个通俗的例子:
a()调用b()当b()返回的时候肯定继续让a()继续执行对吧。b()抛出异常的时候a()肯定也需要处理对吧。
对于异常的处理,jvm是如何处理的呢?虚拟机肯定必须有一个异常处理表,方便发生异常的时候找到处理异常的代码,方便程序继续运转,因此异常处理表Exception table也是帧数据区中很重要的一部分。怎么获取Exception table 中的信息呢操作如下:
执行命令
javap -verbose命令输出它的字节码(不懂这个命令的可以javap -help查看)
from to target type
0 8 19 any
表示在字节码 0-8个字节可能抛出异常,如果遇到异常,则跳转到字节码偏移量19出执行,当方法抛出异常的时候,虚拟机就会查找类似的异常表进行处理,如果无法找到异常表中对应的信息,则会结束当前程序的调用,返回调用函数,并在调用函数中抛出这个异常信息,并查找函数的异常表进行处理。
通俗易懂的说:
a()调用b() 当b()方法中有异常的时候,b()中处理了则继续执行,程序没有处理也就没有异常表信息直接抛异常到a()方法,b()栈移除,a()处理了继续流转,没有处理直接异常抛出。
1.1.1. 栈上分配
栈上分配是java虚拟机提供的一种优化技术,基本思想是,对于那些线程私有对象(不可能被其他线程访问的对象),可以将他们打散分配到栈上,而不是分配在堆上,(是不是很毁三观啊,是不是以前认识的都不对啊白着急)。分配在栈上面的好处函数调用完成后直接自杀销毁,而不需要垃圾回收机制介入回收,所以这样对于性能的提升还是蛮有帮助的。
栈上分配有一个前提条件:开启逃逸分析(必须否则不会栈上分配),逃逸分析的目的就是判断对象的作用域是否有可能逃逸出函数体。如下代码:
private static Useruser;
public static void alloc(){
user=new User();
user.setId(5);
user.setName("springok");
}
对象User user 因为是static 方法中因此可以被任何线程访问到。所以属于逃逸对象。
下面的代码显示了一个非逃逸对象:
public static void alloc(){
User user;
user=new User();
user.setId(5);
user.setName("springok");
}
因为不可以被其他的线程访问到。对象user以局部变量形式存在,该对象没有被返回出去,也没有任何地方可以访问到在其他的方法中,因此是非逃逸的,所以虚拟机可能将user分配在栈上。下面的代码对其结论进行证明:
public static void alloc() {
User user = new User();
user.setId(5);
user.setName("springok");
}
public static void main(String[]args) {
long begin = System.currentTimeMillis();
for (int i = 0;i < 100000000;i++) {
alloc();
}
long end = System.currentTimeMillis();
System.out.println(end-begin);
}
上面代码进行一亿次调用进行对象的创建,累计分配的内存可能达到了1.5G,如果堆空间小于这个1.5G肯定会GC垃圾回收,所以我们开启垃圾回收的参数。
运行配置的参数如下:
-server -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-UseTLAB -XX:+EliminateAllocations
jvm参数使用说明:
-server 因为在server模式才能启用逃逸分析。
-XX:+DoEscapeAnalysis 启用逃逸分析
-Xmx10m 堆空间最大10MB.如果对象在堆上创建肯定会GC垃圾回收的。
-XX:+PrintGC 打印GC日志。
-XX:+EliminateAllocations 开启标量替换默认就是开启的,允许对象打散分配到栈上。
user对象拥有的id和name属性将会被视为独立的局部变量分配。
-XX:-UseTLAB 关闭TLAB
程序执行后完整的打印如下:
5
可以看到没有任何形式的GC输出,程序执行完毕了。说明user对象确实在分配过程被优化了。
如果关闭逃逸分析或者标量替换任意一个,再次执行程序,就会看到大量的GC日志如下:
[GC 3226K->474K(9920K), 0.0001595 secs]
[GC 3226K->474K(9920K), 0.0001488 secs]
[GC 3226K->474K(9920K), 0.0001726 secs]
[GC 3226K->474K(9920K), 0.0001614 secs]
1556
代码侧面印证了结论的正确性。
第一说明了上面的结论,栈上分配依赖逃逸分析和标量替换的实现,第二确实开启了对象被分配在栈上,而不是堆上。因为在堆上肯定会GC垃圾回收的。
对于大量零散的小对象,栈上分配提供了很好的优化策略,栈上分配速度快,可以避免垃圾回收。但由于和堆相比栈的空间小,因此大对象不适合在栈上分配。