3.2 栈帧组成之操作数栈
操作数栈是栈帧的主要内容之一,它主要用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间。
操作数栈也是一个先进后出的数据结构,只支持入栈和出栈两种操作,许多java字节码指令都需要通过操作数栈进行参数传递。比如add指令,它就会在操作数栈中弹出两个整数并进行加法计算,计算结果会被入栈,如图:显示了iadd前后操作数栈的变化。
3.3 帧数据区
除了局部变量表和操作数栈,java栈帧还需要一些数据来支持常量池的解析、正常方法返回和异常处理等。大部分java字节码指令需要进行常量池访问,在帧数据区中保留着访问常量池的指针,方便程序访问常量池。
此外,当函数返回或者出现异常时,虚拟机必须恢复调用者函数的栈帧,并让调用者函数继续执行下去。对于异常处理,虚拟机必须有一个异常处理表,方便在发生异常时找到处理异常的代码,因此异常处理表也是帧数据区中重要的一部分,一个典型的异常处理表如下所示:
Exception table:
from to target type
4 16 19 any
19 21 19 any
它表示在字节码偏移量4--16字节可能抛出任意异常,如果抛出异常,则跳转到字节码偏移量19处执行。当方法抛出异常时,虚拟机就会查找类似的异常表来处理,如果无法在异常表中找到合适的处理方法,则会结束当前函数调用,返回调用函数,并在调用函数中抛出相同的异常,并查找调用函数的异常表来进行处理。
3.4 栈上分配
栈上分配是java虚拟机提供的一项优化技术,它的基本思想是,对于那些线程私有的对象(这里指不可能被其他线程访问的对象),可以将他们打散分配到栈上,而不是分配到堆上。分配到栈上的好处是可以在函数调用结束后自行销毁,而不需要垃圾回收器的介入,从而提高系统的性能。
栈上分配的一个技术基础是进行逃逸分析,逃逸分析的目的是判断对象的作用域是否有可能逃逸出函数体。如下代码所示显示了一个逃逸对象:
private static User u;
public static void alloc(){
u = new User();
u.id = 5;
u.name = "jim";
}
对象u是类的成员变量,该字段有可能被任何线程访问,因此属于逃逸对象,而以下对象显示了一个非逃逸对象:
public static void alloc(){
User u = new User();
u.id = 5;
u.name = "jim";
}
在上述代码中,对象User u 以局部变量的形式存在,并且该对象并没有被alloc()函数返回或者出现任何形式的公开,因此它未发生逃逸,所以对于这种情况,虚拟机就有可能将User u 分配在栈上,而不是在堆上。
对于大量的零散小对象,栈上分配提供了一种良好的对象分配优化策略,栈上分配速度快,并且可以有效避免垃圾回收带来的负面影响。但由于栈和堆空间相比,栈空间较小,因此对于大对象无法也不适合在栈上分配。
实例1:测试非逃逸对象的分配空间位置
package com.jvm;
public class OnStackTest {
public static class User{
public int id = 0;
public String name = "";
}
public static void alloc(){
User u = new User();
u.id = 5;
u.name = "jim";
}
public static void main(String[] args) {
long b = System.currentTimeMillis();
for(int i=0;i<100000000;i++){
alloc();
}
long e = System.currentTimeMillis();
System.out.println(e-b);
}
}
使用-Xmx10M -XX:+PrintGC 虚拟机参数运行代码:
[GC (Allocation Failure) 2048K->544K(9728K), 0.0015011 secs]
10
上述代码在主函数中进行了1亿次alloc()调用进行对象的创建,由于User对象实例需要占用约16byte的空间,因此累计分配空间将达到1.5G,如果堆空间小于这个值,就必然发生GC。而此时我们只分配了最大的堆内存为10M,如果这些对象在堆上创建,必然会引起大量的垃圾回收现象,查看垃圾回收日志,并没有。所以,说明其对象分配在栈上。
实例2:对比测试逃逸对象的分配空间位置:
package com.jvm;
public class OnStackTest {
public static class User{
public int id = 0;
public String name = "";
}
public static User u;
public static void alloc(){
u = new User();
u.id = 5;
u.name = "jim";
}
public static void main(String[] args) {
long b = System.currentTimeMillis();
for(int i=0;i<100000000;i++){
alloc();
}
long e = System.currentTimeMillis();
System.out.println(e-b);
}
}
同样使用虚拟机参数-Xmx10M -XX:+PrintGC设置最大堆空间和打印垃圾回收日志,运行此代码:
可见,发生大量的垃圾回收现象,说明此时堆内存远远不够,需要不断的进行垃圾回收。
4 方法区
和堆一样,方法区是一块所有线程共享的内存区域,它用于保存系统的类信息,比如类的字段、方法、常量池等。方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区的溢出,虚拟机同样会抛出内存溢出错误。
在JDK1.6、JDK1.7中,方法区可以理解为永久区(Perm)。永久区可以使用参数-XX:PermSize和-XX:MaxPermSize指定,默认情况下,-XX:MaxPermSize为64M。一个大的永久区可以保存更多的类信息。如果系统使用了一些动态代理,那么有可能会在运行时生成大量的类,如果这样,就需要设置一个合理的永久区大小,确保不发生永久区内存溢出。
在JDK1.8中,永久区已经被彻底移除,取而代之的是元数据区,元数据区大小可以使用参数-XX:MaxMetaspaceSize指定(一个大的元数据区可以使系统支持更多的类),这是一块堆外的直接内存。与永久区不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。
如果元数据区发生异常,虚拟机一样会抛出异常。