JVM–基础–02–内存区域–栈
1、结构图
1.1、java栈
- 线程私有,一个线程有一个栈,一个栈由N个栈帧组成
- 后进先出(LIFO)栈
- 线程请求的栈深度大于虚拟机允许的深度,出现StackOverflowError 异常
- 虚拟机在扩展栈时无法申请到足够的内存空间,出现OutOfMemoryError 异常
- java栈描述的是Java方法执行的内存模型,存储栈帧,支撑 Java 方法的调用、执行和退出
1.2、java栈–栈帧
1.2.1、栈帧结构
1.2.2、栈帧介绍
- java栈 由 栈帧组成。
- 栈帧结构:
- 局部变量表
- 操作数栈
- 动态连接信息
- 方法返回地址
- 方法正常完成和异常完成信息
- 一些额外的附加信息
- 每一次方法调用,都会有一个对应的栈帧被压入java栈,每一个方法调用结束,都会有一个栈帧被弹出java栈。如上图:栈帧和方法调用。
- 方法1对应栈帧1,方法2对应栈帧2,依次类推。
- 方法1中调用方法2,方法2中调用方法3,方法3调用方法4。当方法1被调用时,栈帧1入栈,当方法2调用时,栈帧2入栈,当方法3被调用时,栈帧3入栈,当方法4被调用时,栈帧4入栈。
- 栈顶:表示当前正在执行的方法
1.2.3、在编译期确定栈帧需要多少内存
在编译代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到了方法表的Code属性中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体虚拟机的实现。
1.3、本地方法栈(native method stack)
- 线程私有
- 后进先出(LIFO)栈
- 可能出现 OutOfMemoryError 异常和 StackOverflowError 异常
- 有一些虚拟机(如Sun HotSpot)将 Java 虚拟机栈和本地方法栈合二为一
- 支撑虚拟机使用的 Native 方法的调用、执行和退出
2、局部变量表
- 是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量,局部变量表中的变量只在当前函数调用中有效,当函数调用结束,随着函数栈帧的弹出销毁,局部变量表也会随之销毁。
- 由若干个 Slot(局部变量空间) 组成,长度由编译期决定。
- 在编译Class文件时,就在方法的Code属性的max_locals数据项中已经确定了该方法需要分配的局部变量表的最大容量,因此在程序执行期间局部变量表的大小是不会改变的。
- 对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。
- 是个数组结构
- 通过索引访问
2.1、Slot(Variable Slot)变量槽
- 是局部变量表的最小单位
- 1个Slot可以存储一个类型为 boolean、byte、char、short、float、reference(对象引用) 和 returnAddress(类型) 的数据
- reference(对象引用): 表示对一个对象实例的引用,通过它可以得到对象在Java 堆中存放的起始地址的索引和该数据所属数据类型在方法区的类型信息。
- returnAddress:则指向了一条字节码指令的地址。
- 2个Slot可以存储一个类型为long或double(64位长度)的数据。
2.2、局部变量表的索引分配
- jvm通过索引定位的方式使用局部变量表。
- 局部变量表存放的是方法参数和局部变量,当调用方法是非 static 方法时,局部变量表中第0位索引的 Slot 存储的是 "this "关键字指向的对象。分配完方法参数后,便会依次分配方法内部定义的局部变量。
2.3、Slot 重用
- 为了节省栈帧空间,局部变量表中的 Slot 是可以重用的。如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的
- 这种机制有时候会影响垃圾回收行为。
2.3.1、Slot重用测试 01
public class Test {
public static void main(String[] args) {
byte[] p = new byte[64*1024*1024];
System.gc();
}
}
[GC (System.gc()) 71442K->66576K(376832K), 0.0224084 secs]
[Full GC (System.gc()) 66576K->66462K(376832K), 0.0066806 secs]
没有回收p所占的内存,因为在执行System.gc()时,变量p还处于作用域内,虚拟机自然回收p的内存。
2.3.2、Slot重用测试 02
p的所用域被限制在了块中,执行System.gc()的时候。p已经不能被访问了。可还是没有被回收。为什么?
在解析为什么之前,我们先对这段代码进行修改,在调用System.gc()之前加入一行int a = 1
,如下:
2.3.3、Slot重用测试 03
可以看到p1的垃圾(64MB)被回收了。
原因解析如下
- Slot重用测试 02中
- p1能否被回收取决于:局部变量表中的Slot是否还存在关于p1数组对象的引用。这次修改中,代码虽然已经离开了p1的作用域,但在此之后,没有任何对局部变量表的读写操作,p1原本所占用的Slot还没有被其他变量所复用,所以作为GC Roots一部分的局部变量表仍然保持着对它的关联。所以不会被回收。
- Slot重用测试 03中
- “int a = 1;” a变量 重用了 p1变量的Slot,p1与Slot的关联被打断,所以垃圾回收才会生效。
2.3.4、总结
- jvm不会给局部变量赋初始值,只给全局变量赋初始值。
- slot复用会打断slot中变量与对象的引用,这种关联被打断后,垃圾回收才会生效。
- 虽然块中的变量不可能在块外被访问,妥妥的垃圾了,但是slot和对象的关联仍然保持,不会成为垃圾。
3、操作数栈
- 是一个后进先出栈
- 由若干个 Entry 组成,长度由编译期决定。
- 1个 Entry 即可以存储一个 Java 虚拟机中定义的任意数据类型的值,包括
long和double类型
- 存储
long和double类型
的Entry深度
为2 - 存储其他类型的
Entry深度
为1
- 存储
- Class 文件的
Code属性的max_stacks
指定了执行过程中最大的栈深度。
- 1个 Entry 即可以存储一个 Java 虚拟机中定义的任意数据类型的值,包括
- 是个数组结构。
- 通过标准的栈操作(压栈和出栈)来访问的。
- 程序中的所有计算过程都是在操作数栈中完成。
3.1、案例:演示了虚拟机是如何把两个int类型的局部变量相加,再把结果保存到第三个局部变量
iload_0 // 存储在局部变量中索引为0的整数压入操作数栈中
iload_1 // 存储在局部变量中索引为1的整数压入操作数栈中
iadd // 从操作数栈中弹出那两个整数相加,再将结果压入操作数栈
istore_2 // 操作数栈中弹出结果,并把它存储到局部变量区索引为2的位置
3.2、原理图
4、方法返回地址
- 当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。
- 每个线程都会有一个自己的Java栈,互不干扰。
4.1、当一个方法执行完毕之后,只有两种方法可以退出当前方法
- 当执行遇到返回指令,会将返回值传递给上层的方法调用者,这种退出的方式称为正常完成出口(Normal Method Invocation Completion),一般来说,调用者的PC计数器可以作为返回地址。
- 当执行遇到异常,并且当前方法体内没有得到处理,就会导致方法退出,此时是没有返回值的,称为异常完成出口(Abrupt Method Invocation Completion),返回地址要通过异常处理器表来确定。
4.2、当方法返回时,可能进行3个操作
- 恢复上层方法的局部变量表和操作数栈
- 把返回值压入调用者调用者栈帧的操作数栈
- 调整
PC计数器的值
以指向方法调用指令后面的一条指令
5、动态链接
-
每个栈帧都包含一个执行运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)
-
Class 文件中存放了大量的符号引用,字节码中的方法调用指令就是以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态解析。另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。
6、附加信息
虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试相关的信息,这部分信息完全取决于具体的虚拟机实现。
在实际开发中,一般会把动态连接、方法返回地址与其他附加信息全部归为一类,称为栈帧信息。
7、案例:栈溢出和内存溢出
7.1、StackOverflowError(可通过-Xss来调教栈深度)
由于每次函数调用都会产生对应的栈帧,从而占用一定的栈空间,因此,如果栈空间不足,那么函数调用自然无法继续进行下去。
当请求的栈深度大于最大可用栈深度时,系统会抛出StackOverflowError栈溢出错误。
package com.fei.zhou.day1;
public class StackOverflowErrorTest {
private static int count = 0;
public static void main(String[] args) {
try {
test();
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
private static void test() {
// 描述:使用递归,由于递归没有出口,这段代码可能会抛出栈溢出错误
System.out.println("最大的调用深度:" + (count++));
test();
}
}
结果:
最大的调用深度:12554
Exception in thread "main" java.lang.StackOverflowError
at sun.nio.cs.UTF_8.updatePositions(UTF_8.java:77)
at sun.nio.cs.UTF_8$Encoder.encodeArrayLoop(UTF_8.java:564)
at sun.nio.cs.UTF_8$Encoder.encodeLoop(UTF_8.java:619)
at java.nio.charset.CharsetEncoder.encode(CharsetEncoder.java:561)
at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:271)
at sun.nio.cs.StreamEncoder.write(StreamEncoder.java:125)
at java.io.OutputStreamWriter.write(OutputStreamWriter.java:207)
at java.io.BufferedWriter.flushBuffer(BufferedWriter.java:129)
at java.io.PrintStream.write(PrintStream.java:526)
at java.io.PrintStream.print(PrintStream.java:669)
at java.io.PrintStream.println(PrintStream.java:806)
at com.fei.zhou.day1.StackOverflowErrorTest.test(StackOverflowErrorTest.java:19)
at com.fei.zhou.day1.StackOverflowErrorTest.test(StackOverflowErrorTest.java:20)
at com.fei.zhou.day1.StackOverflowErrorTest.test(StackOverflowErrorTest.java:20)
at com.fei.zhou.day1.StackOverflowErrorTest.test(StackOverflowErrorTest.java:20)
7.2、OutOfMemoryError
我这就不测试了,只要调小内存大小,再来一个无限增加堆内存的方法,就会抛出该异常