一.概述
虚拟机栈是一种可以被用来快速访问的存储区域,该区域位于随机存取存储器(random access memory,RAM)中,通过使用它所谓的"栈指针"可以让我们访问处理器.
结构如图所示:
二.结构
1.栈帧(Stack Frame)
①含义:
每一次函数的调用都会在调用栈(call stack)上维护一个独立的栈帧(Stack Frame)
②包含元素:
- 局部变量表(Local Variable Table)
- 操作数栈(Operand Stack)
- 动态链接(Dynamic Linking)
- 方法出口(Return Address)
③机制:
- 函数A()被调用时java栈中生成一个与其对应的栈帧sfA并压入栈内
- 当前栈帧CurrentStackFrame为sfA
- 若此函数内调用了函数B(),则java栈生成函数B的栈帧sfB并压入栈内
- 当前栈帧CurrentStackFrame为sfB
- 函数B执行完毕后,其栈帧sfB弹栈
- 当前栈帧CurrentStackFrame为sfA
- 函数A执行完毕后,其栈帧sfA弹栈
- 程序结束
2.局部变量表(Local Variable Table)
①结构如图:
②相关描述:
- Java程序在编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量
- 局部变量表的容量以变量槽(Slot)为最小单位
- Slot的长度可以随着处理器,操作系统或者虚拟机的不同而发生变化
- 执行实例方法(非Static)时,局部变量表索引为0的Slot存放改实例对象的引用,使用关键字"this"表示
③Slot中可以存储一个占用32位以内的数据类型:
- boolean
- byte
- char
- short
- int
- float
- reference
- returnAddress
④reference类型的解释:
- 其表示对一个对象实例的引用
- 其作用有如下两点:
- 1>从引用中直接或者间接的查找到对象在Java堆中存放的起始地址索引
- 2>从引用中直接或者间接的查找到对象所属数据类型在方法区中存储的信息
- ******C++语言中只能满足第一点,这也是为什么C++中提供Java反射的原因
⑤虚拟机对局部变量表的使用
- 虚拟机通过索引定位的方式使用局部变量表
- 索引值的范围:[0,MaxSlotNum]
- 索引值N的含义:
- 访问的是32位数据类型变量,代表使用第N个Slot
- 访问的是64位数据类型变量.代表使用第N和第N+1个Slot(不支持单独访问其中一个,否则在类加载校验阶段抛异常)
⑥局部变量表引发的JVM调优,下面会以代码展示:
public class PlaceHolderTest {
public static void main(String[] args) {
byte[] placeholder=new byte[64*1024*1024];
System.gc();
}
}
观察到内存的回收情况如下:
[GC (System.gc()) 67548K->66368K(125952K), 0.0016639 secs]
[Full GC (System.gc()) 66368K->66203K(125952K), 0.0098551 secs]
总结:触发gc时,变量placeholder仍处于作用域内,无法被gc回收
public class PlaceHolderTest {
public static void main(String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
System.gc();
}
}
观察到内存的回收情况如下:
[GC (System.gc()) 67548K->66312K(125952K), 0.0016849 secs]
[Full GC (System.gc()) 66312K->66203K(125952K), 0.0084991 secs]
总结:触发gc时看似变量placeholder已经失效,但是其所占用内存并未被回收,原因如下:
a)触发时gc时代码虽然已经离开了placeholder的作用域,但是局部变量表未被复用过
b)局部变量表中仍保持着对placeholder的关联
public class PlaceHolderTest {
public static void main(String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
int a = 0;
System.gc();
}
}
观察到内存回收情况如下:
[GC (System.gc()) 67548K->66384K(125952K), 0.0019308 secs]
[Full GC (System.gc()) 66384K->667K(125952K), 0.0063447 secs]
总结:发现局部变量placeholder的内存被回收,其原因如下:
a)触发gc时代码离开了placeholder作用域,出现了新的局部变量a
b)因此会更新局部变量表的关联关系,取消与placeholder的关联,增加与a的关联
所以,对于那些只存在于局域内的变量,需要通过更新复用局部变量表的方式释放该局部变量的所占内存
public class PlaceHolderTest {
public static void main(String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
placeholder = null;
}
System.gc();
}
}
观察到内存回收情况如下:
[GC (System.gc()) 67548K->66296K(125952K), 0.0016885 secs]
[Full GC (System.gc()) 66296K->667K(125952K), 0.0083023 secs]
调优解决方案:局部变量在出作用域前应该先将其置空,触发更新局部变量表以取消对其的关联,便于释放其所占内存
3.操作数栈(Operand Stack)
①存储数据类型:
- boolean
- byte
- char
- short
- int
- float
- reference
- returnAddress
*****byte,short,char在压栈前会被转为int
②操作数栈中元素都是在弹栈后进行运算.然后再将结果压栈
③简单的算数程序执行流程与其机器指令的对比图:
public class ArithmeticOperation {
public static void main(String[] args) {
int a = 1;
int b = 2;
int c = 3;
int d = (a + b)* c;
System.out.println(d);
}
}
4.动态链接(Dynamic Linking)
5.方法出口(Return Address)
①退出方法的两种方式如图所示:
②退出方法之后的流程
- 根据返回这栈帧中保存的信息恢复调用者的局部变量表以及操作数栈
- 将返回值压入操作数栈顶
- 调整PC计数器的值指向调用者的下一条指令
三.占空间相关的两种异常
1.两种异常
①StackOverFlowError
②OutOfMemoryError
2.栈深度:
①程序运行过程中随着方法不断的被调用,栈内不断压入栈帧,栈帧的高度就是栈深度
②栈的深度受栈帧的大小影响,比如:局部变量表增大,栈帧所占内存增多,栈的高度自然降低
3.作用原理:
①这里以代码的形式展示栈的深度以及其会引起的StackOverFlowError异常
public class JavaStackTest {
//计数器
private int count = 0;
//测试栈溢出的递归函数
public void testStack(){
count++;
testStack();
}
//捕获异常的方法,作为递归函数入口
public void test(){
try {
testStack();
} catch (Throwable e) {
System.out.println(e);
System.out.println("Stack Height:"+count);
}
}
public static void main(String[] args) {
new JavaStackTest().test();
}
}
控制台输出:
java.lang.StackOverflowError
Stack Height:38261
②当方法增加形式参数后,栈的深度有什么变化呢?仍以代码形式展示
public class JavaStackTest {
//计数器
private int count = 0;
/*
测试栈溢出的递归函数
增加了形式参数
*/
public void testStack(int a, int b){
count++;
testStack(a,b);
}
//捕获异常的方法,作为递归函数入口
public void test(){
try {
testStack(1,2);
} catch (Throwable e) {
System.out.println(e);
System.out.println("Stack Height:"+count);
}
}
public static void main(String[] args) {
new JavaStackTest().test();
}
}
控制台输出:
java.lang.StackOverflowError
Stack Height:12757
总结:方法参数增多,栈帧所占内存增多,栈深度降低
③当方法中增加局部变量时,栈的深度又会有什么变化呢?仍以代码形式展示
public class JavaStackTest {
//计数器
private int count = 0;
/*
测试栈溢出的递归函数
①增加了形式参数
②增加了局部变量
*/
public void testStack(int a, int b){
int c = 1;
int d = 2;
count++;
testStack(a,b);
}
//捕获异常的方法,作为递归函数入口
public void test(){
try {
testStack(1,2);
} catch (Throwable e) {
System.out.println(e);
System.out.println("Stack Height:"+count);
}
}
public static void main(String[] args) {
new JavaStackTest().test();
}
}
控制台输出:
java.lang.StackOverflowError
Stack Height:8376
总结:方法局部变量增多,栈帧所占内存增多,栈深度降低
④当方法内局部变量值增大时,栈的深度会发生什么变化呢?仍以代码形式展示(为了使得效果明显,这里增加了变量的数量)
public class JavaStackTest {
//计数器
private int count = 0;
/*
测试栈溢出的递归函数
①增加了形式参数
②增加了局部变量
③增加了变量数值大小
*/
public void testStack(int a, int b){
int c = 1;
long d0 = 999999999999999999L;
long d1 = 999999999999999999L;
long d2 = 999999999999999999L;
long d3 = 999999999999999999L;
long d4 = 999999999999999999L;
long d5 = 999999999999999999L;
count++;
testStack(a,b);
}
//捕获异常的方法,作为递归函数入口
public void test(){
try {
testStack(1,2);
} catch (Throwable e) {
System.out.println(e);
System.out.println("Stack Height:"+count);
}
}
public static void main(String[] args) {
new JavaStackTest().test();
}
}
控制台输出:
java.lang.StackOverflowError
Stack Height:4829
总结:变量数值增大,栈帧所占内存增大,栈深度降低
⑤如何修改jvm栈大小呢?这里采用手动修改的方式
总结:虚拟机在扩展栈时无法申请到足够的空间,会抛出OutOfMemoryError