Java 栈内存(Stack)

        在计算机科学中,Stack(栈)是一种特殊的串行形式的数据结构,由于栈数据结构只允许在一端进行操作,因而按照后进先出(LIFO, Last In First Out)的原理操作数据。允许进行插入和删除操作的一端称为栈顶(Top),另一端为栈底(Bottom);栈底固定,而栈顶浮动;栈中元素个数为零时称为空栈。插入一般称为进栈、入栈(PUSH),删除则称为退栈、出栈(POP)。

        先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。栈指针若向下移动,则分配新的内存;若向上移动,则释放那些内存。这是一种快速有效的分配存储方法,仅次于寄存器。栈具有记忆作用,对栈的插入与删除操作中,不需要改变栈底指针。
        Stack 的模型如下:



 

        这里值得注意的是,堆的物理地址向高地址扩展,而栈的物理地址是向低地址扩展,压栈的操作使得栈顶的地址减小,弹出的操作使得栈顶的地址增大。


        每一条JVM 线程都有自己私有的JVM 栈(Java Virtual Machine Stack),这个栈与线程同时创建,用于存储帧(Frames)。JVM 栈的作用与传统语言(例如C语言)中的栈非常类似,就是用于存储局部变量与一些过程结果的地方。另外,它在方法调用和返回中也扮演了很重要的角色。因为除了帧的出栈和入栈之外,JVM 栈不会再受其他因素的影响,所以帧可以在堆中分配,JVM 栈所使用的内存不需要保证是连续的。
        JVM 规范允许JVM 栈被实现成固定大小的或者是根据计算动态扩展和收缩的。如果采用固定大小的JVM 栈设计,那每一条线程的JVM 栈容量应当在线程创建的时候独立地选定。JVM 实现应当提供给程序员或者最终用户调节虚拟机栈初始容量的手段,对于可以动态扩展和收缩JVM 栈来说,则应当提供调节其最大、最小容量的手段。

        虚拟机只会直接对Java 栈执行两种操作:以帧为单位的压栈与出栈。某个线程正在执行的方法被称为该线程的当前方法,当前方法使用的帧称为当前帧,当前方法所属的类称为当前类,当前类的常量池称为当前常量池。在线程执行一个方法时,它会跟踪当前类和当前常量池。
        每当线程调用一个Java 方法时,虚拟机都会在该线程的Java 栈中压入一个新的帧,而这个帧自然就成为了当前帧。在执行这个方法时,他使用这个帧来存储参数,局部变量,中间运算结果等数据。
        Java方法可以以两种方式结束,一种为通过return 返回,称为正常返回;另一种为通过异常的抛出而中止。无论何种返回方式,虚拟机都会将当前帧弹出Java 栈然后释放掉,这样上一个方法的帧就成为了当前帧。
        Java 栈上的所有数据都是此线程私有的,任何线程都无法访问到另一个线程的栈内数据,所以我们不必去担心多线程下的帧数据同步问题。当一个线程调用一个方法时,方法的局部变量保存在调用线程Java 栈的帧中。只有调用方法的线程才能访问到那些局部变量信息。

        帧(Frames)是用来存储数据和部分结果,由三部分组成:局部变量、操作数栈和帧数据区。 局部变量区与操作数栈的大小要视相应方法而定,他们都是按字长来计算的。编译器在编译时就确定了这些值并存放在class 文件中。而帧数据区的大小则依赖于具体实现。
        当虚拟机调用一个Java 方法时,他从对应类的类型信息中得到此方法的局部变量区和操作数栈大小,并根据此来分配帧内存,最后压入到栈中。

 

        局部变量区:
        Java 帧的局部变量区被组织为一个以字长为单位(字长是指计算机的每个字所包含的位数。根据计算机的不同,字长有固定的和可变的两种。固定字长,即字长度不论什么情况都是固定不变的;可变字长,则在一定范围内,其长度是可变的。)、从0开始计数的数组。字节码指令通过从0开始的索引来使用其中的数据。类型为int、float、reference 和returnAddress 的值在数组中只占据一项,而类型为byte、short 和char 的值在存入数组前会被转换成int 值,因而同样只占据一项。但是类型为long 和double 的值在数组中占据连续的两项,原因可想而知,因为他们比较“长”!
        局部变量区包含对应方法的参数和局部变量,编译器首先按声明顺序把这些参数放入局部变量数组,例如:

public static int method1(int a,short b,long c,byte d,Object e){
	return 0;
}

private void method2(long f,double g,Object h){

}

        在局部变量区中各个参数的存储形式:



 

        在图中method2 方法中,局部变量数组第一个参数是reference(引用)类型,尽管我们在方法中没有声明这个参数,但是对于任何一个实例方法都是隐含加入的,他用来表示调用该方法的对象本身,也就是我们常用到的this 关键字。与此相反,method1却没有隐含这个变量,因为method1是一个static 芳芳,也就是类方法,类方法只与类本身有关,而与具体的对象无关,不能直接通过类方法访问类实例的变量,因为在方法调用的时候没有关联到一个具体实例。

        前面已经提到类型为byte、short 和char 的值在存入数组前会被转换成int 值,在操作数栈中也是一样。前面文章提到过虚拟机并不直接支持boolean 类型,因此Java 编译器总是会用int 类型来表示boolean。其他类型是JVM 的基本类型,所以可以比较好的支持。他们在帧中是被当作int 来进行处理的,一旦他们被存回堆或方法区时,会被转换回原来的类型。
        同样的,在Java 中,所有对象都按引用传递,并且都存储在堆中,永远都不会在局部变量区或操作数栈中存在,只会有他们的引用。
        除了Java 方法的参数对于真正的局部变量可以任意觉得放置的顺序,例如在两个for 循环中都会用到int i的情况,当前一个for 循环执行完毕之后,局部变量i 已经超出了有效作用域,所以后面可以继续用i 来表示其他变量。

for(int i=0;i<10;i++){
    //TODO Somthing
}
for(int i=0;i<10;i++){
    //TODO Somthing
}

 

        操作数栈:
        与局部变量区相同,操作数栈也是被组织成一个以字长为单位的数组。但是与前者不同的是,他不是通过索引来访问,而是通过标准的栈操作(入栈,出栈)来访问的。比如,如果某个指令把一个值压入到操作数栈中,稍后另一个指令就可以弹出这个值来使用。虚拟机在操作数栈中存储数据的方式与局部变量区相同。
        虚拟机把操作数栈作为他的工作区,大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈中。比如,iadd(2个int型变量相加) 指令就要从操作数栈中弹出两个整数,执行加法运算,然后将结果压回操作数栈中。

 

0:   iconst_99		//int a=99;
1:   istore_0
3:   iconst_100		//int b=100;
4:   istore_1
6:   iload_0
7:   iload_1
8:   iadd		//a+b=199
9:   istore_2

 

        iconst 操作码表示声明一个int型变量,而istore 操作码则是将其存储在局部变量区中。
        iload_0 与iload_1 则表示从局部变量区中索引为0与1的int 型整数压入到操作数栈中,之后进行加和运算并存储。


 

        帧数据区:
        除了局部变量区和操作数栈外,帧还需要以下数据来支持常量池解析、正常方法返回以及异常派发机制等内容,这些信息就保存在帧数据区。
        Java 虚拟机中的大多数指令都设置及到常量池入口,有些指令仅仅是从常量池中读取数据(int,long,float,double和String)后压入栈中;另有一些指令使用常量池的数据来指示要实例化的类型或数组、要访问的自动或要调用的方法;还有些指令需要常量池中的数据才能确定某个对象是否属于某个类或实现了某个接口。每当虚拟机要执行某个需要用到常量池数据的指令时,他都会通过帧数据区中指向常量池的指针来访问它。
        除了常量池的解析外,帧数据区还要帮助虚拟机处理Java 方法的正常结束和异常中止。如果是通过return 正常结束,虚拟机必须恢复发起调用该方法的帧,包括设置PC 寄存器指向发起调用的方法的指令,即紧跟着调用了完成方法的指令的下一个指令。加入方法有返回值虚拟机必须将他压回入到发起调用方法的操作数栈中。


        对于JVM 栈的结构与操作方式已经有了一个比较清晰的认识,但是在JVM 栈存储数据方面却有着非常大的争议,这些争议来自JVM 栈中基本数据类型的存储与是否共享,以及何时调用静态常量池等方面。


        首先看一段代码:

public class StackTest {

    public void test() {
        int i1 = 5;
        int i2 = 5;
        int i3 = 6;
        int i4 = 128;
        int i5 = 32768;
    }
}

 

        代码非常简单,但你能否说出这几个int 型变量是如何存储,存储在那里的吗?
        我想很多人都会说错,别担心下面我们就一起来研究研究这其中到底有什么“玄妙”。
        利用JDK 自带的反编译工具javap 来反编译StackTest.class 的代码,控制台输入如下命令:

javap -verbose -c StackTest

 

        反编译后显示的内容:

Compiled from "StackTest.java"
public class StackTest extends java.lang.Object
  SourceFile: "StackTest.java"
  minor version: 0
  major version: 50
  Constant pool:
const #1 = class        #2;     //  StackTest
const #2 = Asciz        StackTest;
const #3 = class        #4;     //  java/lang/Object
const #4 = Asciz        java/lang/Object;
const #5 = Asciz        <init>;
const #6 = Asciz        ()V;
const #7 = Asciz        Code;
const #8 = Method       #3.#9;  //  java/lang/Object."<init>":()V
const #9 = NameAndType  #5:#6;//  "<init>":()V
const #10 = Asciz       LineNumberTable;
const #11 = Asciz       LocalVariableTable;
const #12 = Asciz       this;
const #13 = Asciz       LStackTest;;
const #14 = Asciz       test;
const #15 = int 32768;
const #16 = Asciz       i1;
const #17 = Asciz       I;
const #18 = Asciz       i2;
const #19 = Asciz       i3;
const #20 = Asciz       i4;
const #21 = Asciz       i5;
const #22 = Asciz       SourceFile;
const #23 = Asciz       StackTest.java;

{
public StackTest();
  Code:
   Stack=1, Locals=1, Args_size=1
   0:   aload_0
   1:   invokespecial   #8; //Method java/lang/Object."<init>":()V
   4:   return
  LineNumberTable:
   line 1: 0

  LocalVariableTable:
   Start  Length  Slot  Name   Signature
   0      5      0    this       LStackTest;


public void test();
  Code:
   Stack=1, Locals=6, Args_size=1
   0:   iconst_5      //将一个int 类型数据压入到操作数栈中
   1:   istore_1      //将i1 保存到局部变量表中

   2:   iconst_5      //将int 类型数据压入到操作数栈中
   3:   istore_2      //将i2 保存到局部变量表中

   4:   bipush  6     //将一个byte 类型数据入栈
   6:   istore_3      //将i3 保存到局部变量表中

   7:   sipush  128   //将一个short 类型数据入栈
   10:  istore  4     //将i4 保存到局部变量表中

   12:  ldc     #15;  //从运行时常量池中提取数据推入操作数栈
   14:  istore  5     //将i5 保存到局部变量表中

   16:  return        //从当前方法返回void
  LineNumberTable:
   line 4: 0
   line 5: 2
   line 6: 4
   line 7: 7
   line 8: 12
   line 9: 16

  LocalVariableTable:
   Start  Length  Slot  Name   Signature
   0      17      0    this       LStackTest;
   2      15      1    i1       I
   4      13      2    i2       I
   7      10      3    i3       I
   12      5      4    i4       I
   16      1      5    i5       I
}

 

        Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码(Opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(Operands)所构成。虚拟机中许多指令并不包含操作数,只有一个操作码。
        例如:

lcd #15
lcd 为操作码,#15为操作数
sipush  128
sipush 为操作码,128为操作数

 

        class 文件其实就是将Java 代码编译成相关的JVM 指令,每一条指令的含义我已经在后面注释上了,下面是每一步的详细分析:
        0:  此行对应的Java 代码是int i1=5; ,iconst_<i> 指令的作用就是将int 类型的常量<i>(-1,0,1,2,3,4或者5)压入到操作数栈中。
        在JVM 中-1--5 都算作是int 类型常量,也就是说这7个数字是最常用的,你可以把他们当作特殊的int 型。
        1:   然后从操作数栈中取出'i1' 并将其保存到局部变量表中。
        2/3:因为'i2' 的值也未超过5所以操作过程同上。
        4:   在Java 代码中定义了'i3' 变量取值为6,因为已经不再是int 类型常量,所以JVM 自动将其转换称byte 类型并压入到操作数栈中。
        6:   从从操作数栈中取出'i3' 并将其保存到局部变量表中。
        7/10:同理,'i4' 变量取值为128,超过了byte 类型取值范围,JVM 亦自动将其转换为short 类型并压入到操作数栈中,进而将其保存到局部变量表中。
        12/14:从此处就开始有变化了,i5的值为32768 已经超出了short 类型的取值范围。所以编译的时候直接将'32768' 放置到常量池中,然后在从常量池中提取并将其保存到局部变量表中。

 

        ldc 命令格式:ldc index

        ldc:ldc和ldc_w指令用于访问运行时常量池中的对象,包括String实例,但不包括double和long类型的值。当使用的运行时常量池的项的数目过多时(多于256个,1个字节能表示的范围),需要使用ldc_w指令取代ldc指令来访问常量池。ldc2_w 指令用于访问类型为double和long的运行时常量池项,这条指令没有非宽索引的版本(即没有ldc2指令)。
        要执行ldc 指令,JVM 首先查找index 所指定的常量池入口,在index 指向的常量池入口,JVM 将会查找CONSTANT_Integer_info,CONSTANT_Float_info和CONSTANT_String_info 入口。如果还没有这些入口,JVM会解析它们。而对于上面的'32768' JVM 会找到CCONSTANT_Integer_info 入口(其他类型同理)。同时,将把指向被拘留String对象(由解析该入口的进程产生)的引用压入操作数栈。

        index:它作为当前类的运行时常量池的索引使用。index 指向的运行时常量池项必须是一个int 、float 类型的运行时常量,或者是一个类的符号引用或者字符串字面量。在表示运行时常量池索引的操作数前,会井号('#')开头。
        之所以将其写成 ldc #15,最主要的目的是减少了指令代码的长度,之所以限制Java虚拟机操作码的长度为一个字节,并且放弃了编译后代码的参数长度对齐,是为了尽可能地获得短小精干的编译代码,即使这可能会让Java虚拟机的具体实现付出一定的性能成本为代价。由于每个操作码只能有一个字节长度,所以直接限制了整个指令集的数量,又由于没有假设数据是对齐好的,这就意味着虚拟机处理那些超过一个字节的数据的时候,不得不在运行时从字节中重建出具体数据的结构,这在某种程度上会损失一些性能。

        可能上述内容会有些抽象,不过仔细琢磨几次之后还算不难理解,引用一张牛人做的图可以更直观的了解JVM 栈的各种流程:



 

        从以上对JVM 指令的分析可以得出以下几个结论:
        1. 每一个变量都保存着自己的值,他们是分开存储的,不存在某些文章所讲的“栈内数据共享”,这纯属一个自造概念。虽然大于short 取值范围的值会在编译期首先定义到常量池中,但最终存储到栈内存后依旧是值,基本数据类型存储的只是值!所谓共享概念是建立在持有相同引用对象的假象基础上,引用两段Java 官方的解释可以更加有力的印证这点。

        1) A primitive type is a type that is predefined by the Java programming language and named by a reserved keyword. Primitive values do not share state with other primitive values. A variable whose type is a primitive type always holds a primitive value of that type.2
The primitive types are the boolean type and the numeric types. The numeric types are the integral types and the floating-point types.
        文献地址:http://docs.oracle.com/javase/specs/jvms/se5.0/html/Concepts.doc.html

 

        2) Primitive values do not share state with other primitive values. A variable whose type is a primitive type always holds a primitive value of that same type. The value of a variable of primitive type can be changed only by assignment operations on that variable.
        文献地址:http://docs.oracle.com/javase/specs/jls/se5.0/html/typesValues.html

 

        两段话大致意思相同,就是原始值(基本数据类型)不会相互共享状态与值等内容。其值是可以改变的,但只能是对其自身重新赋值。

 

        2. 在定义的int 类型变量值超出short 类型的取值范围时,会在编译的时候将其首先放置到运行时常量池中,然后再从常量池中提取压入栈中,最后保存到局部变量表中。

 

        下面是相关JVM 中的整型类型的取值范围:
        对于byte类型,取值范围是从-128至127,包括-128和127。
        对于short类型,取值范围是从−32768至32767,包括−32768和32767。
        对于int类型,取值范围是从−2147483648至2147483647,包括−2147483648和2147483647。
        对于long类型,取值范围是从−9223372036854775808至9223372036854775807,包括−9223372036854775808和9223372036854775807。
        对于char类型,取值范围是从0至65535,包括0和65535。

 

        如果还是晕乎乎的,那下面来些轻松的内容。

        简单的来讲Java 基本数据类型,Java 指令代码(编译器会将我们的方法及变量转换成相应操作码与操作数),常量都保存在Stack 中。由于Stack 的内存管理是顺序分配的,而且定长,不存在内存回收问题。

        你是否在面试中被问到过:String str="str";这段代码创建了几个对象?这样类似的问题,我想基本所有人都会轻松的答对,但如果你已经阅读过以上文字,那么对于自己的要求标准不应该还那么“低”了。

来看一段简单的代码:

public class StackTest {

    public void test() {
        String str="str";
        String str2="str";
    }
}

 

        利用javap StackTest -verbose -c命令将class 文件反编译,得到以下内容:

Compiled from "StackTest.java"
public class StackTest extends java.lang.Object
  SourceFile: "StackTest.java"
  minor version: 0
  major version: 50
  Constant pool:
const #1 = class        #2;     //  StackTest
const #2 = Asciz        StackTest;
const #3 = class        #4;     //  java/lang/Object
const #4 = Asciz        java/lang/Object;
const #5 = Asciz        <init>;
const #6 = Asciz        ()V;
const #7 = Asciz        Code;
const #8 = Method       #3.#9;  //  java/lang/Object."<init>":()V
const #9 = NameAndType  #5:#6;//  "<init>":()V
const #10 = Asciz       LineNumberTable;
const #11 = Asciz       LocalVariableTable;
const #12 = Asciz       this;
const #13 = Asciz       LStackTest;;
const #14 = Asciz       test;
const #15 = String      #16;    //  hello world!
const #16 = Asciz       hello world!;
const #17 = Asciz       str;
const #18 = Asciz       Ljava/lang/String;;
const #19 = Asciz       str2;
const #20 = Asciz       SourceFile;
const #21 = Asciz       StackTest.java;

{
public StackTest();
  Code:
   Stack=1, Locals=1, Args_size=1
   0:   aload_0
   1:   invokespecial   #8; //Method java/lang/Object."<init>":()V
   4:   return
  LineNumberTable:
   line 1: 0

  LocalVariableTable:
   Start  Length  Slot  Name   Signature
   0      5      0    this       LStackTest;


public void test();
  Code:
   Stack=1, Locals=3, Args_size=1
   0:   ldc     #15; //String hello world!
   2:   astore_1
   3:   ldc     #15; //String hello world!
   5:   astore_2
   6:   return
  LineNumberTable:
   line 4: 0
   line 5: 3
   line 6: 6

  LocalVariableTable:
   Start  Length  Slot  Name   Signature
   0      7      0    this       LStackTest;
   3      4      1    str       Ljava/lang/String;
   6      1      2    str2       Ljava/lang/String;

}

 

        名词解释:
        class:代表类
        Method:代表方法
        Asciz:声明由JVM 调用,其他无法调用。
        Ljava/lang/String; JNI字段描述符,代表了String 类型对象。
        更多解释可以参照JVM 规范。

       

        21-25行代码:声明了str 与str2 两个String 类型对象,并且只声明了一个字符串值"hello world!"。
        47行代码:str 变量从常量池中提取#15 索引的值,这里就需要注意了,#15并不像前面那段代码那样直接存储的是值内容,而是指向#16的一个引用,所以返回给ldc 的就是一个引用类型数据。
        48行代码:将str 的引用值保存到局部变量表中,astore_<n> 的作用就是将一个reference 类型数据保存到局部变量表中。
        49行代码:此时的str2 变量直接从#15 索引这里获取了同str 变量一样的对象引用值,所以可以断定在同一个类或方法中,具有相同内容的String 类型对象持有的是相同的引用对象地址。
        50行代码:将str2 的引用值保存到局部变量表中。


        好了通过以上一些知识相信大家已经对JVM Stack 有了比较深的认识,从短短的一篇文章很难讲的全面,只能抓住一些重点来突出,更进一步的思考还需要在不同的实践中得到印证与结果。

 

        Java 虚拟机规范(英文版):http://docs.oracle.com/javase/specs/jvms/se7/html/index.html
        Java 虚拟机规范(中文版):http://yunpan.cn/QXewm9IUyWjRt

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值