【JVM 03-JVM内存结构之-虚拟机栈】
虚拟机栈 笔记记录
- 1. 定义
- 1.1 演示栈帧
- 2. 特点
- 3. 线程运行诊断
- 3.1 案例1 cpu占用过多&解决
- 3.2 案例2 程序运行很长时间没有结果
- 4. 拓展知识&问题辨析
- 4.1 栈的内存越大越好嘛?(不是)
- 4.2 方法内的局部变量是否线程安全?(是线程安全的)
- 4.2.1 局部变量全在方法中
- 4.2.2 局部变量可能逃离方法
- 4.3 可能抛出的异常?
- 4.4 什么情况下会发生栈内存溢出?
- 4.5 如何设置栈的大小?
- 4.6 补充栈帧的内部结构
- 重要的两个(局部变量表&操作数栈)
- 4.6.1 局部变量表
- 4.6.2 操作数栈
学习资料来源-b站黑马JVM& 尚硅谷JVM精讲与GC调优
1. 定义
- 虚拟机栈:线程运行需要的内存空间。栈中放的是多个栈帧。
- 栈帧:每个方法运行时需要的内存。(比如参数,局部变量,返回地址等。)
- 每个线程只能有1个活动栈帧,对应着当前正在执行的那个方法。
- Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用。是线程私有的,生命周期和线程一致。
- 虚拟机栈中放的一个个栈帧。
1.1 演示栈帧
public static void main(String[] args) {method1();}private static void method1() {method2(1,2);}private static int method2(int a, int b) {int c=a+b;return c;}
2. 特点
- 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
- 垃圾回收是否涉及栈内存?(不会,一次次方法执行后弹出栈自动被回收掉。)不存在GC ; 但是会存在OOM。
3. 线程运行诊断
3.1 案例1 cpu占用过多&解决
- 首先通过top命令定位哪个进程对cpu占用过高
- 根据pid 32655 我们只直到这个进程占用过高,但是我们需要知道那个线程导致的。使用ps H -eo pid,tid,%cpu | grep 32655 (用ps命令进一步定位哪个线程占用cpu过高)
对Linux忘记的话借用deepseek回顾一下。
- 使用jdk提供的工具,jstack 进程id
我们已经知道是线程32655的导致的,可以直接根据线程编号,找到对应的输出信息。但是注意,32655是10进制的,但是jstack输出的信息是16进制的,所以先换算一下。
这里得到7F99,找到对应的代码。根据线程id7f99找到对应的包下的java代码,第8行。
- 发现原来是代码写了while(true) 死循环,导致CPU高。
- 当然这里只是一个举例,实际情况要看线上的具体问题和代码。
3.2 案例2 程序运行很长时间没有结果
一直滑倒最后看到一个死锁问题。这也是为啥不输出的原因。
再看具体的Java代码,其实也是很好理解。如果不理解也没关系,重温锁的知识或者死锁的产生等即可。
4. 拓展知识&问题辨析
4.1 栈的内存越大越好嘛?(不是)
栈越大 内存分配的栈越少,栈越少 线程越少。所以栈别太大,越小线程越多,也不能太小,太小栈溢出。【一般采用系统默认就好】
4.2 方法内的局部变量是否线程安全?(是线程安全的)
4.2.1 局部变量全在方法中
不会有线程安全问题,每个局部变量都在各自的栈帧中,线程不共享。
static void m1(){int x=0;for (int i=0;i<1000;i++){x++;}System.out.println(x);}
4.2.2 局部变量可能逃离方法
总结就是具体问题具体分析,要注意局部变量逃离本方法的问题。
//线程安全public static void m1() {StringBuilder sb = new StringBuilder();sb.append(1);sb.append(2);sb.append(3);System.out.println(sb);}//线程不安全,StringBuilder作为参数传递进来,就意味着有可能有其他线程能访问到它。就不再是线程私有的了。public static void m2(StringBuilder sb) {sb.append(1);sb.append(2);sb.append(3);System.out.println(sb);}//线程不安全,返回值也是StringBuilder,所以其他线程能访问到它。public static StringBuilder m3() {StringBuilder sb = new StringBuilder();sb.append(1);sb.append(2);sb.append(3);return sb;}
4.3 可能抛出的异常?
StackOverFlowError?OutOfMemoryError?
- Java 虚拟机规范允许Java栈的大小是动态的或者是固定不变的。 如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个
StackOverflowError 异常。- 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出—个
OutOfMemoryError 异常。
4.4 什么情况下会发生栈内存溢出?
一、局部数组过大。当函数内部的数组过大时,有可能导致堆栈溢出。(栈帧过大)
二、递归调用层次太多。递归函数在运行时会执行压栈操作,当压栈次数太多时,也会导致堆栈溢出。(栈帧过多)对于第1种情况可能会发生在对象转换JSON的时候,如Dept中有Emp,Emp中有Dept,无线循环,可以使用
@JsonIgnore
private Dept dept; 忽略即可。
4.5 如何设置栈的大小?
-Xss size (即:-XX:ThreadStackSize)
-Xss256k或者-XX:ThreadStackSize=256k都可以
我们看栈大小window下可能会显示0
- 设置10124K
添加VM options
4.6 补充栈帧的内部结构
重要的两个(局部变量表&操作数栈)
可以IDEA插件中下载jclasslib插件来看代码
4.6.1 局部变量表
局部变量表(local variables)
● 局部变量表也被称之为局部变量数组或本地变量表。
● 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型(8种)、对象引用(reference),以及returnAddress类型。
● 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
● 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。
● 局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
4.6.2 操作数栈
操作数栈(Operand Stack)
● 我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
● 每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出(Last-In-First-Out)的操作数栈,也可以称之为表达式栈(Expression Stack)。
● 操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
● 每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为max_stack的值。
栈中的任何一个元素都是可以任意的Java数据类型。
32bit的类型占用一个栈单位深度
64bit的类型占用两个栈单位深度
● 操作数栈,在方法执行过程中,根据字节码指令,并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈(push)和出栈(pop)操作,往栈中写入数据或提取数据来完成一次数据访问。
某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈。比如:执行复制、交换、求和等操作
如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
- 操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
这里做一下简单的介绍,具体对于栈的深度,以及有过slot的复用,每个类型占多少大小等后续复习继续更新吧。