Java堆栈

Java堆栈
jvm为每个新创建的线程都分配一个堆栈。堆栈以帧为单位保存线程的状态。jvm对堆栈只进行两种操作:以帧为单位的压栈和出栈操作。

某个线程正在执行的方法称为此线程的当前方法。当前方法使用的帧称为当前帧。当前方法所属的类称为当前类。当前类的常量池称为当前常量池。当线程执行一个方法时,它会跟踪当前的类和常量池。当jvm会在当前帧内执行帧内数据的操作。

当线程激活一个java方法,jvm就会在线程的java堆栈里新压入一个帧。这个帧自然成为了当前帧。在此方法执行期间,这个帧将用来保存参数,局部变量,中间计算过程和其他数据。

一个方法可以以两种方法结束。一种是正常返回结束。一种是通过异常抛出而异常结束(abruptcompletion)。不管以那种方式返回,jvm都会将当前帧弹出堆栈然后释放掉,这样上一个方法的帧就成为当前帧了。(译者:可能可以这样理解,位于堆栈顶部的帧为当前帧)

java堆栈上的所有数据都为此线程私有。一个线程不能访问另一个线程的堆栈数据,所以在多线程的情况下也不需要对堆栈数据的访问进行同步。

象方法区和堆一样(见以前的译文),java堆栈和帧在内存中也不必是连续的。帧可以分布在连续的内存区,也可以不是。帧的数据结构由jvm的实现者来决定,他们可以允许用户指定java堆栈的初始大小或最大最小尺寸。

堆栈帧( The Stack Frame)
堆栈帧有三部分:局部变量区,操作数堆栈和帧数据区。
局部变量区和操作数堆栈的大小要视对应的方法而定。编译器在编译的时候就对每个方法进行了计算并放在了类文件(classfile)中了。帧数据区的大小对一种jvm实现来说是一定的。当jvm激活一个方法时,它从类信息数据得到此方法的局部变量区和操作数堆栈的大小,并据此分配大小合适堆栈帧压入java堆栈中。

局部变量区
java堆栈帧的局部变量区是一个基为零类型为word的数组。指令通过索引来使用这些数据。类型为int,float,reference和returnAddress的值在数组中占据一项,类型为byte,short,和char的值在存入数组前都转为了int值而占据一项。类型为long和double的值在数组中占据连续的两项,在访问他们的时候,指令提供第一项的索引。例如一个long值占据3,4项,指令会取索引为3的long值。局部变量区的所有值都是字对齐的,long和doubles的起始索引值没有限定。

局部变量区包含此方法的参数和局部变量。编译器首先以声明的顺序把参数放入局部数据区。
5-9显示了下面两个方法的变量区。
// On CD-ROM in file jvm/ex3/Example3a.java
class Example3a {

    public static intrunClassMethod(int i, long l, float f,
        double d, Object o, byte b) {

        return 0;
    }

    public int runInstanceMethod(charc, double d, short s,
        boolean b) {

        return 0;
    }
}

  • 图5-9. 局部变量区中的方法参数

注意在方法runInstanceMethod()的帧中,第一个参数是一个类型为reference的值,尽管方法没有显示的声明这个参数,但这是个对每个实例方法(instancemethod)都隐含加入的一个参数值,用来代表调用的对象。(译者:与c++中的this指针一样)我们看方法runClassMethod()就没有这个变量,这是因为这是一个类方法(classmethod),类方法与类相关,而不与对象相关。

我们注意到在源码中的byte,short,char和boolean在局部变量区都成了ints。在操作数堆栈也是同样的情况。如前所述,jvm不直接
支持boolean类型,java编译器总是用ints来表示boolean。但java对byte,short和char是支持的,这些类型的值可以作为实例变量
存储在局部变量区中,也可以作为类变量存储在方法区中。但在局部变量区和操作数堆栈中都被转成了ints类型的值,期间的运算也是以int来的,只当存回堆或方法区中,才会转回原来的类型。

同样需要注意的是runClassMethod()的对象o。在java中,所以的对象都以引用(reference)传递。所有的对象都存储在堆中,你永远都不会在局部变量区或操作数堆栈中发现对象的拷贝,只会有对象引用。

编译器对局部变量的放置方法可以多种多样,它可以任意决定放置顺序,甚至可以用一个索引指代两个局部变量。例如,当两个局部变量的作用域不重叠时,如Example3b的局部变量i和j。

// On CD-ROM in file jvm/ex3/Example3b.java
class Example3b {

    public static void runtwoLoops() {

        for (int i= 0; i < 10; ++i) {
           System.out.println(i);
        }

        for (int j= 9; j >= 0; --j) {
           System.out.println(j);
        }
    }
}

jvm的实现者对局部变量区的设计仍然有象其他数据区一样的灵活性。关于long和double数据如何分布在数组中,jvm规范没有指定。假如一个jvm实现的字长为64位,可以把long或double数据放在数组中的低项内,而使高项为空。(在字长为32位的时候,需要两项才能放下一个long或double)。

操作数堆栈
操作数堆栈象局部变量区一样是用一个类型为word的数组存储数据,但它不是通过索引来访问的,而是以堆栈的方式压入和弹出。假如一个指令压入了一个值,另一个指令就可以弹出这个值并使用之。

jvm在操作数堆栈中的处理数据类型的方式和局部变量区是一样的,同样有数据类型的转换。jvm没有寄存器,jvm是基于堆栈的而不是基于寄存器的,因为jvm的指令从堆栈中获得操作数,而不是寄存器。虽然操作数还可以从另外一些地方获得,如字节码中,或常量池内,但主要是从堆栈获得的。


jvm把操作数堆栈当作一个工作区使用。许多指令从此堆栈中弹出数据,进行运算,然后压入结果。例如,iadd指令从堆栈中弹出两个数,相加,然后压入结果。下面显示了jvm是如何进行这项操作的:
iload_0    // push the int in local variable 0
iload_1    // push the int in local variable 1
iadd       // pop two ints, add them, push result
istore_2   // pop int, store into local variable 2

在这个字节码的序列里,前两个指令iload_0和iload_1将存储在局部变量区中索引为0和1的整数压入操作数据区中,然后相加,将
结果压入操作数据区中。第四条指令istore_2从操作数据区中弹出结果并存储到局部数据区索引为2的地方。在图5-10中,详细的表述了这个过程,图中,没有使用的区域以空白表示。

图5-10. 两个局部变量的相加.

帧数据区
除了局部变量区和操作数据堆栈外,java栈帧还需要数据来支持常量池解析(constantpool resolution),方法的正常返回(normal method return)和异常分派(exception dispatch)。这些信息保存在帧数据区中。

jvm中的许多指令都涉及到常量池的数据。一些指令仅仅是取出常量池中的数据并压入操作数堆栈中。一些指令使用常量池中的数据来指示需要实例化的类或数组,需要访问的域,或需要激活的方法。还有一些指令来判断某个对象是否是常量池指定的某个类或接口的子孙实例。

每当jvm要执行需要常量区数据的指令,它都会通过帧数据区中指向常量区的指针来访问常量区。以前讲过,常量区中对类型,域和方法的引用在开始时都是符号。如果当指令执行的时候仍然是符号,jvm就会进行解析。

除了常量区解析外,帧数据区还要帮助jvm处理方法的正常和异常结束。正常结束,jvm必须恢复方法调用者的环境,包括恢复pc指针。假如方法有返回值,jvm必须将值压入调用者的操作数堆栈。

为了处理方法的异常退出,帧数据区必须保存对此方法异常表的引用。一个异常表定义了这个方法受catch子句保护的区域,每项都有一个catch子句的起始和开始位置(position),和用来表示异常类在常量池中的索引,以及catch子句代码的起始位置。

当一个方法抛出异常时,jvm使用帧数组区指定的异常表来决定如何处理。如果找到了匹配的catch子句,就会转交控制权。如果没有发现,方法会立即结束。jvm使用帧数据区的信息恢复调用者的帧,然后重新抛出同样的异常。

除了上述信息外,jvm的实现者也可以将其他信息放入帧数据区,如调试数据。

java堆栈的一种实现
实现者可以按自己的想法设计java堆栈。如以前所讲,一个方法是从堆中单独的分配帧。我以此为例,看下面的类:
// On CD-ROM in file jvm/ex3/Example3c.java
class Example3c {

    public static void addAndPrint() {
        double result = addTwoTypes(1,88.88);
        System.out.println(result);
    }

    public static doubleaddTwoTypes(int i, double d) {
        return i + d;
    }
}

图5-11显示了一个线程执行这个方法的三个快照。在这个jvm的实现中,每个帧都单独的从堆中分配。为了激活方法addTwoTypes(),方法
addAndPrint()首先压入int 1和double88.88到操作数堆栈中,然后激活addTwoTypes()方法。

图5-11. 帧的分配

激活addTwoTypes()的指令使用了常量池的数据,jvm在常量池中查找这些数据如果有必要则解析之。

注意addAndPrint()方法使用常量池引用方法addTwoTypes(),尽管这两个方法是属于一个类的。象引用其他类一样,对同一个类的方法和域的引用在初始的时候也是符号,在使用之前需要解析。

解析后的常量池数据项将指向存储在方法区中有关方法addTwoTypes()的信息。
jvm将使用这些信息决定方法addTwoTypes()局部变量区和操作数堆栈的大小。
如果使用Sun的javac编译器(JDK1.1)的话,方法addTwoTypes()的局部变量区
需要三个words,操作数堆栈需要四个words。(帧数据区的大小对某个jvm实现
来说是定的)jvm为这个方法分配了足够大小的一个堆栈帧。然后从方法
addAndPrint()的操作数堆栈中弹出double参数和int参数(88.88和 1)并把他们
分别放在了方法addTwoType()的局部变量区索引为1和0的地方。

当addTwoTypes()返回时,它首先把类型为double的返回值(这里是89.88)
压入自己的操作数堆栈里。jvm使用帧数据区中的信息找到调用者(为
addAndPrint())的堆栈帧,然后将返回值压入addAndPrint()的操作数堆栈
中并释放方法addTwoType()的堆栈帧。然后jvm使addTwoType()的堆栈帧
为当前帧并继续执行方法addAndPrint()。

图5-12显示了相同的方法在不同的jvm实现里的执行情况。这里的堆栈帧是在
一个连续的空间里的。这种方法允许相邻方法的堆栈帧可以重叠。这里调用者的
操作数堆栈就成了被调者的局部变量区。

图5-12. 从一个连续的堆栈中分配帧

这种方法不仅节省了空间,而且节省了时间,因为jvm不必把参数从一个
堆栈帧拷贝到另一个堆栈帧中了。

注意当前帧的操作数堆栈总是在java堆栈的顶部。尽管这样可能
可以更好的说明图5-12的实现。但不管java堆栈是如何实现的,
对操作数堆栈的操作总是在当前帧执行的。这样,在当前帧的
操作数堆栈压入一个数也就是在java堆栈压入一个值。

java堆栈还有一些其他的实现,基本上是上述两种的结合。一个jvm可以
在线程初期时从堆栈分出一段空间。在这段连续的空间里,jvm可以采用
5-12的重叠方法。但在与其他段空间的结合上,就要使用如图5-11的方法

JAVA经验谈:尽可能使用堆栈变量

来源: 优快云   作者: chensheng913

  如果您频繁存取变量,就需要考虑从何处存取这些变量。变量是 static 变量,还是堆栈变量,或者是类的实例变量?变量的存储位置对存取它的代码的性能有明显的影响?例如,请考虑下面这段代码:

   class StackVars
  {
  private int instVar;
  private static int staticVar;
  
  //存取堆栈变量
  void stackAccess(int val)
  {
  int j=0;
  for (int i=0; i<val; i++)
  j += 1;
  }
  
  //存取类的实例变量
  void instanceAccess(int val)
  {
  for (int i=0; i<val; i++)
  instVar += 1;
  }  
  
  //存取类的 static 变量
  void staticAccess(int val)
  {
  for (int i=0; i<val; i++)
  staticVar += 1;
  }
  }
 

  这段代码中的每个方法都执行相同的循环,并反复相同的次数。唯一的不同是每个循环使一个不同类型的变量递增。方法 stackAccess 使一个局部堆栈变量递增,instanceAccess 使类的一个实例变量递增,而 staticAccess 使类的一个 static 变量递增。

  instanceAccess 和 staticAccess 的执行时间基本相同。但是,stackAccess 要快两到三倍。存取堆栈变量如此快是因为,JVM 存取堆栈变量比它存取 static 变量或类的实例变量执行的操作少。请看一下为这三个方法生成的字节码:


   Method void stackAccess(int)
  0 iconst_0         //将 0 压入堆栈。
  1 istore_2         //弹出 0 并将它存储在局部分变量表中索引为 2 的位置 (j)。
  2 iconst_0         //压入 0。
  3 istore_3         //弹出 0 并将它存储在局部变量表中索引为 3 的位置 (i)。
  4 goto 13          //跳至位置 13。
  7 iinc 2 1         //将存储在索引 2 处的 j 加 1。
  10 iinc 3 1         //将存储在索引 3 处的 i 加 1。
  13 iload_3          //压入索引 3 处的值 (i)。
 
  14 iload_1          //压入索引 1 处的值 (val)。
  15 if_icmplt 7      //弹出 i 和 val。如果 i 小于 val,则跳至位置 7。
  18 return           //返回调用方法。
  
  Method void instanceAccess(int)
  0 iconst_0         //将 0 压入堆栈。
  1 istore_2         //弹出 0 并将它存储在局部变量表中索引为 2 的位置 (i)。
  2 goto 18          //跳至位置 18。
  5 aload_0          //压入索引 0 (this)。
  6 dup              //复制堆栈顶的值并将它压入。
  7 getfield #19 <Field int instVar>
  //弹出 this 对象引用并压入 instVar 的值。
  10 iconst_1         //压入 1。
  11 iadd             //弹出栈顶的两个值,并压入它们的和。
  12 putfield #19 <Field int instVar>
  //弹出栈顶的两个值并将和存储在 instVar 中。
  15 iinc 2 1         //将存储在索引 2 处的 i 加 1。
  18 iload_2          //压入索引 2 处的值 (i)。
  19 iload_1          //压入索引 1 处的值 (val)。
  20 if_icmplt 5      //弹出 i 和 val。如果 i 小于 val,则跳至位置 5。
  23 return           //返回调用方法。
 
  
   Method void staticAccess(int)
  0 iconst_0         //将 0 压入堆栈。
  1 istore_2         //弹出 0 并将它存储在局部变量表中索引为 2 的位置 (i)。
  2 goto 16          //跳至位置 16。
  5 getstatic #25 <Field int staticVar>
  //将常数存储池中 staticVar 的值压入堆栈。
  8 iconst_1         //压入 1。
  9 iadd             //弹出栈顶的两个值,并压入它们的和。
  10 putstatic #25 <Field int staticVar>
  //弹出和的值并将它存储在 staticVar 中。
  13 iinc 2 1         //将存储在索引 2 处的 i 加 1。
  16 iload_2          //压入索引 2 处的值 (i)。
  17 iload_1          //压入索引 1 处的值 (val)。
  18 if_icmplt 5      //弹出 i 和 val。如果 i 小于 val,则跳至位置 5。
  21 return           //返回调用方法。
 

  查看字节码揭示了堆栈变量效率更高的原因。JVM 是一种基于堆栈的虚拟机,因此优化了对堆栈数据的存取和处理。所有局部变量都存储在一个局部变量表中,在 Java 操作数堆栈中进行处理,并可被高效地存取。存取 static 变量和实例变量成本更高,因为 JVM 必须使用代价更高的操作码,并从常数存储池中存取它们。(常数存储池保存一个类型所使用的所有类型、字段和方法的符号引用。)

  通常,在第一次从常数存储池中访问 static 变量或实例变量以后,JVM 将动态更改字节码以使用效率更高的操作码。尽管有这种优化,堆栈变量的存取仍然更快。

  考虑到这些事实,就可以重新构建前面的代码,以便通过存取堆栈变量而不是实例变量或 static 变量使操作更高效。请考虑修改后的代码:


   class StackVars
  {
  //与前面相同...
  void instanceAccess(int val)
  {
  int j = instVar;
  for (int i=0; i<val; i++)
  j += 1;
  instVar = j;
  } 
  
  void staticAccess(int val)
  {
  int j = staticVar;
  for (int i=0; i<val; i++)
  j += 1;
  staticVar = j;
  }
  }   
 

  方法 instanceAccess 和 staticAccess 被修改为将它们的实例变量或 static 变量复制到局部堆栈变量中。当变量的处理完成以后,其值又被复制回实例变量或 static 变量中。这种简单的更改明显提高了 instanceAccess 和 staticAccess 的性能。这三个方法的执行时间现在基本相同,instanceAccess 和 staticAccess 的执行速度只比 stackAccess 的执行速度慢大约 4%。

  这并不表示您应该避免使用 static 变量或实例变量。您应该使用对您的设计有意义的存储机制。例如,如果您在一个循环中存取 static 变量或实例变量,则您可以临时将它们存储在一个局部堆栈变量中,这样就可以明显地提高代码的性能。这将提供最高效的字节码指令序列供 JVM 执行。

 

这两种方式是不相同的。声明变量/数组的方式,是在栈中分配内存。程序中的每一个函数都有自己的栈,用于为函数作用域内的变量/对象分配存储空间。当调用完此函数返回的时候,栈空间自动被收回,其中的内容也就全部无效了。而new是在堆中分配内存的,而且一经分配则永久保留,直到显式的释放掉。否则会造成内存泄露。实际上Java是通过实现一个“垃圾收集器”在运行期间实现这一点的。<br/>说白了,栈和堆都是进程空间内的一段虚存(将被映射到物理内存,所以也可说是内存中的一段空间),所不同的是,计算机对栈给予了更多支持。还记得栈指针寄存器吗,当程序调用内存时,栈指针将指向栈顶,存贮空间的分配就是通过移动栈指针来完成的。那么,哪些变量将在栈中分配空间呢?既然栈用起来这么方便,那么要&nbsp; 堆 &nbsp; 来干什么呢?显然,如果我们在程序运行之前(编译时)就能知道要申请多少空间,栈就足够了,但在复杂的应用中,人们很难提前知道自己要申请多少空间.我们可以把&nbsp; 堆 &nbsp; 看成存储空间的仓库,当我们需要存储空间时就从仓库中领取,不使用这段空间时就把它还回去(这是一个相当复杂的算法)。领取和归还的过程是程序执行时决定的(所谓的动态决定),编译时无法确定存储空间的位置。<br/>线程是程序执行的最小单位,每个线程都有自己独立的堆栈.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值