虚拟机内存分配之栈(学习有感)

  • 栈的分类

虚拟机栈和本地方法栈

  • 什么是虚拟机栈

虚拟机栈是用于描述java方法执行的内存模型,跟程序计数器一样,都是线程私有的,生命周期和线程相同。

每个java方法在执行时,会创建一个“栈帧(stack frame)”,栈帧的结构分为“局部变量表、操作数栈、动态链接、方法出口”几个部分(具体的作用会在字节码执行引擎章节中讲到,这里只需要了解栈帧是一个方法执行时所需要数据的结构)。我们常说的“堆内存、栈内存”中的“栈内存”指的便是虚拟机栈,确切地说,指的是虚拟机栈的栈帧中的局部变量表,因为这里存放了一个方法的所有局部变量。

  方法调用时,创建栈帧,并压入虚拟机栈;方法执行完毕,栈帧出栈并被销毁,如下图所示:

 

  • 局部变量表

存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、
float、long、double)、对象引用等。------可以理解为方法的参数、局部变量以及方法内对对象的引用

  • 操作数栈

​​​​​​​操作数栈也常被称为操作栈,它是一个后入先出栈。JVM底层字节码指令集是基于栈类型的,所有的操作码都是对操作数栈上的数据进行操作,对于每一个方法的调用,JVM会建立一个操作数栈,以供计算使用。和局部变量一样。操作数栈的最大深度也是编译的时候写入到方法表的code属性的max_stacks数据项中。操作数栈的每一个元素可以是任意的Java数据类型,包括long、double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。栈容量的单位为“字宽”,对于32位虚拟机来说,一个“字宽”占4个字节,64位虚拟机来说,一个“字宽”占8个字节。当一个方法刚刚执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指向操作数栈中写入和提取值,也就是入栈与出栈操作。例如,在做算术运算的时候就是通过操作数栈来进行的,又或者调用其它方法的时候是通过操作数栈来行参数传递的。 另外,在概念模型中,两个栈帧作为虚拟机栈的元素,相互之间是完全独立的,但是大多数虚拟机的实现里都会作一些优化处理,令两个栈帧出现一部分重叠。让下栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用返回时就可以共用一部分数据,而无须进行额外的参数复制传递了

  • 动态链接

​​​​​​​ 每个栈帧都包含一个指向运行时常量池中该栈帧所属性方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。在Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态连接

  • 方法返回地址

​​​​​​​ 当一个方法被执行后,有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法方式称为正常完成出口(Normal Method Invocation Completion)。另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的调用都产生任何返回值的。     无论采用何种方式退出,在方法退出之前,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。 方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用都栈帧的操作数栈中,调用PC计数器的值以指向方法调用指令后面的一条指令等。

  •  StackOverflowError和OutOfMemoryError异常​​​​​​​

 若单个线程请求的栈深度大于虚拟机允许的深度,则会抛出StackOverflowError(栈溢出错误)。

  JVM会为每个线程的虚拟机栈分配一定的内存大小(-Xss参数),因此虚拟机栈能够容纳的栈帧数量是有限的,若栈帧不断进栈而不出栈,最终会导致当前线程虚拟机栈的内存空间耗尽,典型如一个无结束条件的递归函数调用,代码见下:

 1 /**
 2  * java栈溢出StackOverFlowError
 3  * JVM参数:-Xss128k
 4  * Created by chenjunyi on 2018/4/25.
 5  */
 6 public class JavaVMStackSOF {
 7 
 8     private int stackLength = -1;
 9 
10     //通过递归调用造成StackOverFlowError
11     public void stackLeak() {
12         stackLength++;
13         stackLeak();
14     }
15 
16     public static void main(String[] args) {
17         JavaVMStackSOF oom = new JavaVMStackSOF();
18         try {
19             oom.stackLeak();
20         } catch (Throwable e) {
21             System.out.println("Stack length:" + oom.stackLength);
22             e.printStackTrace();
23         }
24     }
25 
26 }

 

  设置单个线程的虚拟机栈内存大小为128K,执行main方法后,抛出了StackOverflow异常

1 Stack length:983
2 java.lang.StackOverflowError
3     at com.manayi.study.jvm.chapter2._02_JavaVMStackSOF.stackLeak(_02_JavaVMStackSOF.java:14)
4     at com.manayi.study.jvm.chapter2._02_JavaVMStackSOF.stackLeak(_02_JavaVMStackSOF.java:15)
5     at com.manayi.study.jvm.chapter2._02_JavaVMStackSOF.stackLeak(_02_JavaVMStackSOF.java:15)
6     ······

 

1.4)虚拟机栈的OutOfMemoryError

  不同于StackOverflowError,OutOfMemoryError指的是当整个虚拟机栈内存耗尽,并且无法再申请到新的内存时抛出的异常。

  JVM未提供设置整个虚拟机栈占用内存的配置参数。虚拟机栈的最大内存大致上等于“JVM进程能占用的最大内存(依赖于具体操作系统) - 最大堆内存 - 最大方法区内存 - 程序计数器内存(可以忽略不计) - JVM进程本身消耗内存”。当虚拟机栈能够使用的最大内存被耗尽后,便会抛出OutOfMemoryError,可以通过不断开启新的线程来模拟这种异常,代码如下:

 1 **
 2  * java栈溢出OutOfMemoryError
 3  * JVM参数:-Xss2m
 4  * Created by chenjunyi on 2018/4/25.
 5  */
 6 public class JavaVMStackOOM {
 7 
 8     private void dontStop() {
 9         while (true) {
10         }
11     }
12 
13     //通过不断的创建新的线程使Stack内存耗尽
14     public void stackLeakByThread() {
15         while (true) {
16             Thread thread = new Thread(() -> dontStop());
17             thread.start();
18         }
19     }
20 
21     public static void main(String[] args) {
22         JavaVMStackOOM oom = new _03_JavaVMStackOOM();
23         oom.stackLeakByThread();
24     }
25 
26 }

  设置单个线程虚拟机栈的占用内存为2m并不断生成新的线程,最终虚拟机栈无法申请到新的内存,抛出异常:

1 Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
  • 异常解析

实验结果表明:在单个线程下,无论是由于栈帧太大,还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是 StackOverflowError 异常。 
如果测试时不限于单线程,通过不断地建立线程的方式倒是可以产生内存溢出异常,如代码清单 2-3所示。但是,这样产生的内存溢出异常与栈空间是否足够大并不存在任何联系,或者准确地说,在这种情况下,给每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。  原因其实不难理解,操作系统分配给每个进程的内存是有限制的,譬如32位的Windows限制为2GB。虚拟机提供了参数来控制 Java 堆和方法区的这两部分内存的最大值。

  • 本地方法栈

​​​​​​​本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间
的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚
拟机使用到的Native方法服务。在虚拟机规范中对本地方法栈中方法使用的语言、使用方式
与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如
Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法
栈区域也会抛出StackOverflowError和OutOfMemoryError异常。

ps:native方法 我理解为虚拟机底层调用c++等语言进行与电脑硬件打交道。

​​​​​​​

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值