我们天天在说java堆java栈,究竟什么是java栈?
首先需要明确一个概念,那就是通常我们说的堆、栈与java堆、java栈并不是一个东西。java堆和java栈可能都是在我们通常所说的堆中划分出内存的。在java层面上,将这划分出的内存按其用途称为java堆、java栈。
《The Java Virtual Machine Specification Java SE 7 Edition》(中文版《Java 虚拟机规范(Java SE 7 版)》)中对java栈是这么解释的:
每一个Java虚拟机线程都有自己私有的Java虚拟机栈(Java Virtual Machine Stack),这个栈与线程同时创建,用于存储栈帧(Frames, §2.6)。Java虚拟机栈的作用与传统语言(例如 C语言)中的栈非常类似,就是用于存储局部变量与一些过程结果的地方。另外,它在方法调用和返回中也扮演了很重要的角色。因为除了栈帧的出栈和入栈之外,Java虚拟机栈不会再受其他因素的影响,所以栈帧可以在堆(此堆不是java堆,就是我们通常意义所说的堆)中分配 ,Java虚拟机栈所使用的内存不需要保证是连续的。
Java虚拟机规范允许 Java虚拟机栈被实现成固定大小的或者是根据计算动态扩展和收缩的。如果采用固定大小的 Java虚拟机栈设计,那每一条线程的Java虚拟机栈容量应当在线程创建的时候独立地选定。Java虚拟机实现应当提供给程序员或者最终用户调节虚拟机栈初始容量的手段,对于可以动态扩展和收缩 Java虚拟机栈来说,则应当提供调节其最大、最小容量的手段。
接下来对此一一解读。
1、当创建了一个新线程时,都会为其分配一个java栈,另外还会分配一个程序计数器。
2、Java栈里能pop或push的内容是栈帧(Stack Frame),用类java伪码表示是这样的Stack<StackFrame>.当线程调用一个方法时,将一个新栈帧push进java栈,当一个方法调用结束(无论是正常结束还是抛出异常),从java栈中pop出一个栈帧。
3、栈帧(Stack Frame)的内容分为三部分:局部变量区、操作数栈、帧数据区。用java伪码可大概表示如下:
class StackFrame {
LocalVariable lv;
OperationStack os;
FrameData fd;
……
}
4、栈帧的作用:执行一个方法时,用新入栈的栈帧存储参数、局部变量、中间运算结果等。
5、因为每个线程都有自己的java栈,所以java栈中的所有数据都是该线程私有的,不存在并发安全问题。
6、java栈和栈帧在内存中不必是连续的,栈帧可以在堆(非java堆)中分配,java栈和栈帧的具体结构由jvm实现者自行决定。java栈的大小可以是固定的也可是动态计算的大小。
前面提到java栈里面存的是栈帧,栈帧包含三部分,接下来解释下这三部分的作用。
1、局部变量区
局部变量区,顾名思义,是用来存在局部变量和方法调用的参数的。局部变量区类似于数组,是用索引来访问的。如果是非静态方法,局部变量区索引为0的位置是当前对象this的引用。假如有以下方法:
public
void
test(String name) {
int
a = ...;
... ...
}
|
局部变量区存储的内存非配可能是这样的:局部变量区索引为0的位置是this引用,索引为1的位置是参数name,索引为2的位置是int变量a…
如果是静态方法,索引为0的位置就不是this引用,需要所有的位置往前推一个。
需要注意的是,局部变量区中可以存储基本类型(int,double,long,float,returnAddress; boolean,byte,short,char,在这里都转换已经隐式的转换成了int)和引用类型(reference)。如果参数是对象类型,如上面的String,局部变量区绝对不会出现java对象(如某个String值),只会是指向该对象的引用(直接或间接的)。若是基本类型,则存储的是其值。同时,long和double占用了,连续的两个索引位置(即使一个索引位置能装得下long或double值,也会用两个索引位置来存储)。
我们可以做些推断,在方法调用的时候其java栈帧就要入栈,也就是说,调用方法前就已经知道调用这个方法需要多大的局部变量区了(同样操作数栈的大小也是确定下来了的),事实确实如此,在编译的时候这些信息就已经确定下来了。换个思维,在调用java方法的时候,如果声明了局部变量,它是在调用前就已经为其分配好内存了,而不是调用过程中。那么,在for循环中声明变量会是怎么处理呢?如下面的代码:
public
class
Test {
public
static
void
main(String[] args) {
for
(
int
i=
0
; i<
1000
; i++) {
String s = String.valueOf(i);
System.out.println(s);
}
}
}
|
基于上面的理论,可以肯定的是,它不会在循环过程中不停的为s分配内存。那它会在调用main方法新建的栈帧的局部变量区中分配1000多个位置吗?如果这件事让我们自己做,我们会怎么处理呢?s在每次迭代开始之前就出了作用域,那么我就可以重用局部变量区的s嘛,真实情况如何?我们运行下javap -c Test,得到main方法如下:
public
static
void
main(java.lang.String[]);
Code:
0
: iconst_0
1
: istore_1
2
: iload_1
3
: sipush
1000
6
: if_icmpge
27
9
: iload_1
10
: invokestatic #
2
;
//Method java/lang/String.valueOf:(I)Ljava/lang/String;
13
: astore_2
14
: getstatic #
3
;
//Field java/lang/System.out:Ljava/io/PrintStream;
17
: aload_2
18
: invokevirtual #
4
;
//Method java/io/PrintStream.println:(Ljava/lang/String;)V
21
: iinc
1
,
1
24
:
goto
2
27
:
return
}
|
在第10条指令调用完String.valueOf(i)后,第13条将其存入局部变量区索引为2的位置,也就是s,OK,我们看到了,它确实一直重用局部变量区索引为2的位置来存取每次迭代中声明的s。
2、操作数栈
操作数栈的作用主要用来存储运算结果以及运算的操作数。如果局部变量区一样,它可以存储基本类型(int,double,long,float,returnAddress; boolean,byte,short,char,在这里都转换已经隐式的转换成了int)和引用类型(reference),对象是以引用的形式出现在操作数栈里,操作数栈里也永远不可能出现对象,只会是对象的引用。操作数栈的操作方式不同于局部变量区,操作数栈是以入栈和出栈的形式操作的,如上面main方法去掉System.out.println(s)后的指令集合的含义如下(假设【】表示栈,左边为栈顶):
public
static
void
main(java.lang.String[]);
Code:
0
: iconst_0
//将0入栈,栈内容【0】
1
: istore_1
//从栈中弹出一个int,存储到局部变量区索引为1的位置,栈内容【】
2
: iload_1
//将局部变量区索引为1的int值入栈,栈内容:【0】
3
: sipush
1000
//将1000入栈,栈内容【1000,0】
6
: if_icmpge
27
//从栈中弹出两个int,比较器大小,若大于或等于,跳到指令27,栈内容【】
9
: iload_1
//将局部变量区索引为1的int值入栈,栈内容:【num】
(循环中第21条指令不停的自增局部变量区索引为1的值,该值是动态变化的)
10
: invokestatic
//从栈顶弹出一个int,作为参数调用String.valueOf方法,并将调用结果入栈。
栈内容【reference】(String.valueOf的返回值是一个String对象,栈里存的是指向该对象的引用)
13
: astore_2
//将栈顶元素出栈,存到局部变量区索引为2的位置,栈内容【】
21
: iinc
1
,
1
//将局部变量区索引为1的int值加1,栈内容【】
24
:
goto
2
//跳回指令2,栈内容【】
27
:
return
//方法调用结束
}
|
3、帧数据区
帧数据区用来支持常量池解析,方法正常返回,方法异常处理等内容。
—————–
更多阅读:《深入java虚拟机 第二版》第五章,《The Java Virtual Machine Specification Java SE 7 Edition》§2.5,§2.6