Java栈

简介: 我们天天在说java堆java栈,究竟什么是java栈? 首先需要明确一个概念,那就是通常我们说的堆、栈与java堆、java栈并不是一个东西。java堆和java栈可能都是在我们通常所说的堆中划分出内存的。在java层面上,将这划分出的内存按其用途称为java堆、java栈。 《The Java Virtual Machine Specification Java SE 7

我们天天在说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

目录
相关文章
|
4月前
|
存储 算法 Java
惊!Java程序员必看:JVM调优揭秘,堆溢出、栈溢出如何巧妙化解?
【8月更文挑战第29天】在Java领域,JVM是代码运行的基础,但需适当调优以发挥最佳性能。本文探讨了JVM中常见的堆溢出和栈溢出问题及其解决方法。堆溢出发生在堆空间不足时,可通过增加堆空间、优化代码及释放对象解决;栈溢出则因递归调用过深或线程过多引起,调整栈大小、优化算法和使用线程池可有效应对。通过合理配置和调优JVM,可确保Java应用稳定高效运行。
150 4
|
25天前
|
存储 算法 Java
Java 内存管理与优化:掌控堆与栈,雕琢高效代码
Java内存管理与优化是提升程序性能的关键。掌握堆与栈的运作机制,学习如何有效管理内存资源,雕琢出更加高效的代码,是每个Java开发者必备的技能。
49 5
|
1月前
|
存储 算法 Java
🧠Java零基础 - Java栈(Stack)详解
【10月更文挑战第17天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
42 2
|
5月前
|
存储 算法 Java
Java面试题:深入探究Java内存模型与垃圾回收机制,解释JVM中堆内存和栈内存的主要区别,谈谈对Java垃圾回收机制的理解,Java中的内存泄漏及其产生原因,如何检测和解决内存泄漏问题
Java面试题:深入探究Java内存模型与垃圾回收机制,解释JVM中堆内存和栈内存的主要区别,谈谈对Java垃圾回收机制的理解,Java中的内存泄漏及其产生原因,如何检测和解决内存泄漏问题
72 0
|
2月前
|
存储 安全 Java
【用Java学习数据结构系列】探索栈和队列的无尽秘密
【用Java学习数据结构系列】探索栈和队列的无尽秘密
37 2
|
4月前
|
Java 索引
java中的栈(利用数组实现栈)
这篇文章通过Java代码示例介绍了如何使用数组实现栈操作,包括栈的初始化、入栈、出栈、判断栈满和空以及遍历栈的方法。
java中的栈(利用数组实现栈)
|
5月前
|
Java 运维
开发与运维命令问题之使用jstack命令查看Java进程的线程栈如何解决
开发与运维命令问题之使用jstack命令查看Java进程的线程栈如何解决
72 2
|
5月前
|
存储 安全 Java
Java面试题:在JVM中,堆和栈有什么区别?请详细解释说明,要深入到底层知识
Java面试题:在JVM中,堆和栈有什么区别?请详细解释说明,要深入到底层知识
96 3
|
5月前
|
存储 缓存 监控
Java面试题:解释堆和栈的OutOfMemoryError通常在什么情况下会发生
Java面试题:解释堆和栈的OutOfMemoryError通常在什么情况下会发生
56 3
|
5月前
|
存储 Java
深入理解Java中的堆内存与栈内存分配
深入理解Java中的堆内存与栈内存分配