JVM工作原理与实战(十七):运行时数据区-栈内存溢出

简介: JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了栈内存溢出、设置虚拟机栈的大小等内容。

一、Java虚拟机栈

Java虚拟机栈是Java技术中至关重要的部分,它负责管理方法调用中的基本数据。在执行Java程序时,Java虚拟机栈是线程私有的,即每个线程都拥有自己的Java虚拟机栈,并且随着线程的创建而创建,随着线程的销毁而回收。


Java虚拟机栈,运用栈式数据结构对方法调用过程中的基本数据进行管理。遵循先进后出(First In Last Out)的原则,每一个方法调用都会对应一个独立的栈帧(Stack Frame)进行数据存储。

二、栈内存溢出

1.栈内存溢出介绍

Java虚拟机(JVM)的栈内存是用于存储方法执行时的数据和信息的区域。每个方法的执行都会在栈上创建一个新的栈帧,其中包含了方法的局部变量、操作数栈、动态链接和方法出口信息。如果一个方法调用的层级过深,或者每个栈帧占用的内存过大,那么就会导致栈内存溢出。当这种情况发生时,系统会抛出StackOverflowError异常。


在默认情况下,JVM会根据操作系统和计算机体系结构自动设置每个线程的栈大小。这样可以确保JVM能够根据不同的环境进行适当的配置。具体的默认栈大小取决于JVM实现和配置,以下是一些常见的默认值:

  • 在Linux上,对于x86(64位)体系结构,默认栈大小为1MB;对于ppc体系结构,默认栈大小为2MB。
  • 在BSD上,对于x86(64位)体系结构,默认栈大小为1MB。
  • 在Solaris上,对于64位体系结构,默认栈大小为1MB。
  • 在Windows上,默认栈大小基于操作系统的默认值。


模拟栈内存溢出案例

通过递归方式调用同一方法,且不设定退出条件,并定义一个计数器变量来跟踪每次调用的次数。随着每次调用,计数器递增。最后,查看错误发生时总调用的次数。

public class Demo1 {
    public static void main(String[] args) {
        recursion();
    }
    public static int count = 0;
    public static void recursion() {
        System.out.println(++count);
        recursion();
    }
}
image.gif

运行结果:

image.gif

2.设置虚拟机栈的大小

为了解决Java虚拟机栈内存溢出的问题,可以通过调整栈的大小来应对。要修改Java虚拟机栈的大小,可以使用虚拟机参数-Xss。通过这个参数,可以为每个线程设置一个自定义的栈大小。


语法为-Xss栈大小,单位可以是字节(默认,是 1024 的倍数)、k或者K(KB)、m或者M(MB)、g或者G(GB)。除了-Xss参数外,还有一个与它类似的参数-XX:ThreadStackSize可以用来调整堆栈大小。

案例:

public class Demo1 {
    public static void main(String[] args) {
        recursion();
    }
    public static int count = 0;
    public static void recursion() {
        System.out.println(++count);
        recursion();
    }
}
image.gif

设置虚拟机栈的大小(-Xss):

-Xss512k
image.gif

image.gif

运行结果:

image.gif

在调整堆栈大小时,还需要注意一些事项:

  • 最小和最大堆栈大小限制:不同的JVM实现和操作系统可能会有不同的最小和最大堆栈大小限制。例如,在Windows(64位)下的JDK8测试中,最小值为180k,最大值为1024m。因此,在设置堆栈大小时,需要考虑这些限制,以避免出现错误。
  • 局部变量和操作数栈深度:过多的局部变量或者过深的操作数栈深度都可能影响栈内存的大小。因此,在编写代码时,需要合理地控制局部变量的数量和操作数栈的深度,以避免出现内存溢出的情况。
  • 递归深度:虽然递归可以简化代码,但是如果没有适当的退出条件或者递归深度过大,也会导致栈内存溢出。因此,在使用递归时,需要仔细设计递归算法,并设置合适的退出条件,以避免出现内存溢出的情况。
  • 内存限制:在某些情况下,即便设置了合适的堆栈大小和退出条件,由于系统的内存限制,也可能会出现栈内存溢出的情况。在这种情况下,可能需要优化程序的性能,减少内存的使用,或者增加系统的内存。

案例(局部变量过多、操作数栈深度过大影响栈内存的大小):

原代码:

public class Demo1 {
    public static void main(String[] args) {
        recursion();
    }
    public static int count = 0;
    public static void recursion() {
        System.out.println(++count);
        recursion();
    }
}
image.gif

运行结果:

image.gif

添加局部变量:

public class Demo1 {
    public static void main(String[] args) {
        recursion();
    }
    public static int count = 0;
    public static void recursion() {
        long x,y,z,i,j,k,a,b,c;
        System.out.println(++count);
        recursion();
    }
}
image.gif

运行结果:

image.gif


总结

JVM是Java程序的运行环境,负责字节码解释、内存管理、安全保障、多线程支持、性能监控和跨平台运行。本文主要介绍了栈内存溢出、设置虚拟机栈的大小等内容,希望对大家有所帮助。

相关文章
|
6月前
|
Arthas 存储 算法
深入理解JVM,包含字节码文件,内存结构,垃圾回收,类的声明周期,类加载器
JVM全称是Java Virtual Machine-Java虚拟机JVM作用:本质上是一个运行在计算机上的程序,职责是运行Java字节码文件,编译为机器码交由计算机运行类的生命周期概述:类的生命周期描述了一个类加载,使用,卸载的整个过类的生命周期阶段:类的声明周期主要分为五个阶段:加载->连接->初始化->使用->卸载,其中连接中分为三个小阶段验证->准备->解析类加载器的定义:JVM提供类加载器给Java程序去获取类和接口字节码数据类加载器的作用:类加载器接受字节码文件。
551 55
|
17天前
|
存储 缓存 Java
我们来说一说 JVM 的内存模型
我是小假 期待与你的下一次相遇 ~
133 4
|
24天前
|
存储 缓存 算法
深入理解JVM《JVM内存区域详解 - 世界的基石》
Java代码从编译到执行需经javac编译为.class字节码,再由JVM加载运行。JVM内存分为线程私有(程序计数器、虚拟机栈、本地方法栈)和线程共享(堆、方法区)区域,其中堆是GC主战场,方法区在JDK 8+演变为使用本地内存的元空间,直接内存则用于提升NIO性能,但可能引发OOM。
|
7月前
|
Arthas 监控 Java
Arthas memory(查看 JVM 内存信息)
Arthas memory(查看 JVM 内存信息)
532 6
|
6月前
|
存储 安全 Java
JVM深入原理(七)(一):运行时数据区
栈的介绍:Java虚拟机栈采用栈的数据结构来管理方法调用中的基本数据,先进后出,每一个方法的调用使用一个栈帧来保存栈的组成:栈:一个线程运行所需要的内存空间,一个栈由多个栈帧组成栈帧:一个方法运行所需要的内存空间活动栈帧:一个线程中只能有一个活动栈帧栈的生命周期:栈随着线程的创建而创建,而回收会在线程销毁时进行栈的执行流程:栈帧压入栈内执行方法执行完毕释放内存若方法间存在调用,那么会压入被调用方法入栈,执行完后释放内存,再执行当前方法,直到执行完毕,释放所有内存。
105 0
|
6月前
|
存储 缓存 安全
JVM深入原理(七)(二):运行时数据区
堆的作用:存放对象的内存空间,它是空间最大的一块内存区域.栈上的局部变量表中,可以存放堆上对象的引用。静态变量也可以存放堆对象的引用,通过静态变量就可以实现对象在线程之间共享。堆的特点:线程共享:堆中的对象都需要考虑线程安全的问题垃圾回收:堆有垃圾回收机制,不再引用的对象就会被回收方法区的概述:方法区是存放基础信息的位置,线程共享,主要包括:类的元信息:保存了所有类的基本信息运行时常量池:保存了字节码文件中的常量池内容静态常量池:字节码文件通过编号查表的方式找到常量。
80 0
|
8月前
|
存储 Java C++
JVM 运行时数据区
Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存区域划分为若干个不同的数据区域。这 些区域都有各自的用途,以及创建和销毁的时间,有些区域随着虚拟机进程的启动而存在,有些区 域则是依赖线程的启动和结束而建立和销毁。Java 虚拟机所管理的内存被划分为如下几个区域 程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解 析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳 转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成; 为什么要线程计数器?因为线程是不具备记忆功能 Java 虚拟机
|
10月前
|
存储 设计模式 监控
快速定位并优化CPU 与 JVM 内存性能瓶颈
本文介绍了 Java 应用常见的 CPU & JVM 内存热点原因及优化思路。
992 166
|
12月前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
1940 1
|
8月前
|
存储 缓存 算法
JVM简介—1.Java内存区域
本文详细介绍了Java虚拟机运行时数据区的各个方面,包括其定义、类型(如程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和直接内存)及其作用。文中还探讨了各版本内存区域的变化、直接内存的使用、从线程角度分析Java内存区域、堆与栈的区别、对象创建步骤、对象内存布局及访问定位,并通过实例说明了常见内存溢出问题的原因和表现形式。这些内容帮助开发者深入理解Java内存管理机制,优化应用程序性能并解决潜在的内存问题。
348 29
JVM简介—1.Java内存区域