JVM——栈和堆概述,以及有什么区别?

方法栈

方法栈并不是某一个 JVM 的内存空间,而是我们描述方法被调用过程的一个逻辑概念。

在同一个线程内,T1()调用T2():

  • T1()先开始,T2()后开始;
  • T2()先结束,T1()后结束。

images

堆和栈概述

从英文单词角度来说

  • 栈:stack
  • 堆:heap

从数据结构角度来说

  • 栈和堆一样:都是先进后出,后进先出的数据结构

从 JVM 内存空间结构角度来说

  • 栈:通常指 Java 方法栈,存放方法每一次执行时生成的栈帧。
  • 堆:JVM 中存放对象的内存空间。包括新生代、老年代、永久代等组成部分。 

 栈帧

栈帧存储的数据

方法在本次执行过程中所用到的局部变量、动态链接、方法出口等信息。栈帧中主要保存3 类数据:

  • 本地变量(Local Variables):输入参数和输出参数以及方法内的变量。

  • 栈操作(Operand Stack):记录出栈、入栈的操作。

  • 栈帧数据(Frame Data):包括类文件、方法等等。

栈帧的结构

  • 局部变量表:方法执行时的参数、方法体内声明的局部变量
  • 操作数栈:存储中间运算结果,是一个临时存储空间
  • 帧数据区:保存访问常量池指针,异常处理表

栈帧工作机制

当一个方法 A 被调用时就产生了一个栈帧 F1,并被压入到栈中,

A 方法又调用了 B 方法,于是产生栈帧 F2 也被压入栈,

B 方法又调用了 C 方法,于是产生栈帧 F3 也被压入栈,

……

C 方法执行完毕后,弹出 F3 栈帧;

B 方法执行完毕后,弹出 F2 栈帧;

A 方法执行完毕后,弹出 F1栈帧;

……

遵循“先进后出”或者“后进先出”原则。

images

图示在一个栈中有两个栈帧:

栈帧 2 是最先被调用的方法,先入栈,

然后方法 2 又调用了方法 1,栈帧 1 处于栈顶的位置,

栈帧 2 处于栈底,执行完毕后,依次弹出栈帧 1 和栈帧 2,

线程结束,栈释放。

每执行一个方法都会产生一个栈帧,保存到栈的顶部,顶部栈就是当前方法,该方法执行完毕后会自动将此栈帧出栈。

典型案例

请预测下面代码打印的结果:34

int n = 10;
n += (n++) + (++n);
System.out.println(n);

实际执行结果:32

使用 javap 命令查看字节码文件内容:

D:\record-video-original\day03\code>javap -c Demo03JavaStackExample.class
Compiled from "Demo03JavaStackExample.java"
public class Demo03JavaStackExample{
public Demo03JavaStackExample();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>: ()V
4: return

public static void main(java.lang.String[]);
Code:
0: bipush 10
2: istore_1
3: iload_1
4: iload_1
5: iinc 1, 1
8: iinc 1, 1
11: iload_1
12: iadd
13: iadd
14: istore_1
15: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
18: iload_1
19: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
22: return
}

内存执行过程分析:

images

栈溢出异常

异常名称

java.lang.StackOverflowError

异常产生的原因

下面的例子是一个没有退出机制的递归:

public class StackOverFlowTest {public static void main(String[] args) {methodInvokeToDie();}public static void methodInvokeToDie() {methodInvokeToDie();}}

抛出的异常信息:

Exception in thread "main" java.lang.StackOverflowError at com.atguigu.jvm.test.StackOverFlowTest.methodInvokeToDie(StackOverFlowTest.java:10) at com.atguigu.jvm.test.StackOverFlowTest.methodInvokeToDie(StackOverFlowTest.java:10) at com.atguigu.jvm.test.StackOverFlowTest.methodInvokeToDie(StackOverFlowTest.java:10) at com.atguigu.jvm.test.StackOverFlowTest.methodInvokeToDie(StackOverFlowTest.java:10) at com.atguigu.jvm.test.StackOverFlowTest.methodInvokeToDie(StackOverFlowTest.java:10)

原因总结:方法每一次调用都会在栈空间中申请一个栈帧,来保存本次方法执行时所需要用到的数据。但是一个没有退出机制的递归调用,会不断申请新的空间,而又不释放空间,这样迟早会把当前线程在栈内存中自己的空间耗尽。

栈空间的线程私有验证

提出问题

某一个线程抛出『栈溢出异常』,会导致其他线程也崩溃吗?从以往的经验中我们判断应该是不会,下面通过代码来实际验证一下。

代码

new Thread(()->{while(true) {try {TimeUnit.SECONDS.sleep(2);System.out.println(Thread.currentThread().getName() + " working");} catch (InterruptedException e) {e.printStackTrace();}}
}, "thread-01").start();new Thread(()->{while(true) {try {TimeUnit.SECONDS.sleep(2);// 递归调用一个没有退出机制的递归方法methodInvokeToDie();System.out.println(Thread.currentThread().getName() + " working");} catch (InterruptedException e) {e.printStackTrace();}}
}, "thread-02").start();new Thread(()->{while(true) {try {TimeUnit.SECONDS.sleep(2);System.out.println(Thread.currentThread().getName() + " working");} catch (InterruptedException e) {e.printStackTrace();}}
}, "thread-03").start();

结论:02 线程抛异常终止后,01 和 03 线程仍然能够继续正常运行,说明 02 抛异常并没有影响到 01 和 03,说明线程对栈内存空间的使用方式是彼此隔离的。每个线程都是在自己独享的空间内运行,反过来也可以说,这个空间是当前线程私有的。

images

堆空间

images

堆空间工作机制

  • 新创建的对象会被放在Eden区
  • 当Eden区中已使用的空间达到一定比例,会触发Minor GC
  • 每一次在Minor GC中没有被清理掉的对象就成了幸存者
  • 幸存者对象会被转移到幸存者区
  • 幸存者区分成from区和to区
  • from区快满的时候,会将仍然在使用的对象转移到to区
  • 然后from和to这两个指针彼此交换位置

口诀:复制必交换,谁空谁为to

  • 如果一个对象,经历15次GC仍然幸存,那么它将会被转移到老年代
  • 如果幸存者区已经满了,即使某个对象尚不到15岁,仍然会被移动到老年代
  • 最终效果:
    1. Eden区主要是生命周期很短的对象来来往往
    2. 老年代主要是生命周期很长的对象,例如:IOC容器对象、线程池对象、数据库连接池对象等等
    3. 幸存者区作为二者之间的过渡地带
  • 关于永久代:
    • 从理论上来说属于堆
    • 从具体实现上来说不属于堆

永久代在各个JDK版本之间的演变

永久代常量池
≤JDK1.6在方法区
=JDK1.7有,但开始逐步“去永久代”在堆
≥JDK1.8在元空间

方法区、元空间、永久代之间关系

堆、栈、方法区之间关系

images

堆溢出异常

异常名称

java.lang.OutOfMemoryError,也往往简称为 OOM。

异常信息

  • Java heap space:针对新生代、老年代整体进行Full GC后,内存空间还是放不下新产生的对象
  • PermGen space:方法区中加载的类太多了(典型情况是框架创建的动态类太多,导致方法区溢出)

我们可以参考下面的控制台日志打印:

[GC (Allocation Failure) 4478364K->4479044K(5161984K), 4.3454766 secs] [Full GC (Ergonomics) 4479044K->3862071K(5416448K), 39.3706285 secs] [Full GC (Ergonomics) 4410423K->4410422K(5416448K), 27.7039534 secs] [Full GC (Ergonomics) 4629575K->4621239K(5416448K), 24.9298221 secs] [Full GC (Allocation Failure) 4621239K->4621186K(5416448K), 29.0616791 secs] Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:3210) at java.util.Arrays.copyOf(Arrays.java:3181) at java.util.ArrayList.grow(ArrayList.java:261) at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235) at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227) at java.util.ArrayList.add(ArrayList.java:458) at com.atguigu.jvm.test.JavaHeapTest.main(JavaHeapTest.java:16)

小练习

测试代码

查看下面程序在每个步骤中内存的状态:

public class Review {// 静态变量,类变量public static Review review = new Review();public void showMessage() {// 局部变量Review reviewLocal = new Review();}// 程序入口public static void main(String[] args) {// 局部变量Review reviewMain = new Review();// 通过局部变量调用对象的方法reviewMain.showMessage();// 手动 GCSystem.gc();}
}

各状态分析

images

images

images

images

images

images

栈和堆的区别

堆和栈的区别主要体现在以下几个方面。

  • 内存分配方式

栈(stack)和堆(heap)都是内存中的一段区域,但它们的内存分配方式是不同的。栈是由程序自动创建和释放的,通常用于存储函数调用时的临时变量、函数的返回地址等信息。而堆则是由程序员手动申请和释放的,通常用于存储程序中需要动态分配的内存(如动态数组、对象等)。

  • 内存管理方式

栈的内存分配是按照“后进先出”的原则进行的,即最后一个进入栈的变量最先被释放。因此,栈中的内存管理是由系统自动完成的,程序员不需要过多考虑内存的分配和释放问题。堆的内存管理则需要程序员自行负责,使用完毕后必须手动释放,否则会导致内存泄漏或其他问题。

  • 内存大小

栈的容量较小,一般只有几百KB到几MB的空间,具体容量由操作系统和编译器决定。相对而言,堆用于存储较大的数据结构,大小一般比栈要大得多,可以动态扩展内存空间。但是,因为堆需要手动管理内存,如果不及时释放,会导致内存泄漏,进而影响系统性能。

  • 访问速度

因为栈的内存分配是系统自动完成的,所以访问速度相对堆更快。栈中的数据直接存放在系统内存中,而访问堆中的数据需要通过指针进行间接访问,会造成一定的时间损耗。此外,在多线程环境下,由于栈的线程独享,所以不会发生竞争问题。而堆则需要考虑多线程并发访问时的同步和互斥机制。

  • 应用场景

栈适合用于存储局部变量和函数调用,主要用于内存的临时分配;而堆适合用于存储需要动态分配和管理的数据结构,如动态数组、字符串、对象等。在实际开发中,应该根据具体的应用场景选择合适的内存分配方式。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/35979.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Maven介绍,部署在eclipse中

目录 一.Maven介绍 1&#xff0c;什么是maven&#xff1f; 2. 为什么maven会在企业中大量使用&#xff1f; 3.没有使用maven的前后区别? 4.maven在Java开发中的实际效果图 二.maven部署在eclipse中 1.下载maven在其官方网址下载&#xff08;当然实际下载也要根据个人的…

服务器安装Tomcat

下载Tomcat 下载地址在这&#xff1a; Tomcat官网 下载完成以后把压缩包上传到服务器中&#xff08;我传到了www/java&#xff09;,进行解压(解压到)&#xff0c;如果没有进行指定解压到哪里&#xff0c;默认是到root文件夹中 tar -zxvf /www/java/apache-tomcat-9.0.103.tar.…

统计学补充概念03-核密度估计

概念 核密度估计&#xff08;Kernel Density Estimation&#xff0c;简称 KDE&#xff09;是一种非参数统计方法&#xff0c;用于估计随机变量的概率密度函数&#xff08;Probability Density Function&#xff0c;PDF&#xff09;。它通过在每个数据点周围放置核函数&#xf…

day 10 | 232.用栈实现队列、 225. 用队列实现栈

目录&#xff1a; 解题及思路学习 232.用栈实现队列 https://leetcode.cn/problems/implement-queue-using-stacks/ 模拟题&#xff0c;用两个栈来实现队列的功能。 class MyQueue { public:stack<int> stIn;stack<int> stOut;/** Initialize your data struc…

HCIP学习--BGP3

目录 前置内容 BGP下一跳的修改问题 BGP的属性 配置 PrefVal权重属性 负载分担 LocPrf 负载分担 NextHop AS-PATH Ogn 配置 MED 配置 BGP选路规则 BGP的社团属性 配置及解释 前置内容 HCIP学习--BGP1_板栗妖怪的博客-CSDN博客 HCIP学习--BGP2_板栗妖怪的博客…

031_小驰私房菜_MTK平台Camera基本流程,日志信息打印

这篇文章主要介绍mtk平台,camera基本流程的日志信息打印。针对下面几点展开: 一) camera打开流程; 二) 帧请求 && 帧回调; 三) 拍照; MTK平台camera模块,如果想要打开更多日志,一般需要先设置 adb shell setprop "vendor.debug.camera.log" 1 然后…

STM32控制SG90舵机原理及代码

STM32控制SG90舵机原理及代码 一.SG90舵机原理二.控制SG90舵机三.代码实例3.1 配置定时器3.2 main 函数 四.实验现象 一.SG90舵机原理 舵机的运用还是比较广泛的&#xff0c;那么舵机工作原理是什么呢&#xff0c;一般来说我们给舵机一个信号他就能工作了&#xff0c;那么这个…

00 - 环境配置

查看所有文章链接&#xff1a;&#xff08;更新中&#xff09;GIT常用场景- 目录 文章目录 1. 环境说明2. 安装配置2.1 配置user信息2.2 config的三个作用域 3. 建git仓库3.1 把已有的项目代码纳入git管理3.2 新建的项目直接用git管理3.3 配置local的user和email3.4 优先级&…

Redis_缓存1_缓存类型

14.redis缓存 14.1简介 穿透型缓存&#xff1a; 缓存与后端数据交互在一起&#xff0c;对服务端的调用隐藏细节。如果从缓存中可以读到数据&#xff0c;就直接返回&#xff0c;如果读不到&#xff0c;就到数据库中去读取&#xff0c;从数据库中读到数据&#xff0c;也是先更…

股票指数——RSI指数

RSI指数的计算非常简单&#xff0c;就是使用一段时间内的平均上涨除以平均上涨加平均下跌&#xff08;取正值&#xff09;。也就意味着RSI指数的取值是[0,100]之间&#xff0c;其中0表示周期内没有上涨的&#xff0c;100表示周期内没有下跌的。RSI的直观意义是它表示了一段周期…

学习笔记整理-JS-06-函数

一、函数基本使用 1. 什么是函数 函数就是语句的封装&#xff0c;可以让这些代码方便地被复用。函数具有"一次定义&#xff0c;多次调用"的优点。使用函数&#xff0c;可以简化代码&#xff0c;让代码更具有可读性。 2. 函数的定义和调用 和变量类似&#xff0c;函…

Jupyter并发测试以后出现EOFError marshal data too short

Jupyter 并发测试以后出现EOFError: marshal data too short 背景 由于项目需求需要用户能进行网页在线运行python代码程序&#xff0c;调研后决定使用Jupyter的服务接口实现此功能&#xff0c;目前使用docker进行容器化部署&#xff0c;测试针对次服务进行并发测试。测试并发…

JimuReport积木报表 v1.6.0版本发布—免费的可视化报表

项目介绍 一款免费的数据可视化报表&#xff0c;含报表和大屏设计&#xff0c;像搭建积木一样在线设计报表&#xff01;功能涵盖&#xff0c;数据报表、打印设计、图表报表、大屏设计等&#xff01; Web 版报表设计器&#xff0c;类似于excel操作风格&#xff0c;通过拖拽完成报…

开源代码分享(13)—整合本地电力市场与级联批发市场的投标策略(附matlab代码)

1.引言 1.1摘要 本地电力市场是在分配层面促进可再生能源的效率和使用的一种有前景的理念。然而&#xff0c;作为一个新概念&#xff0c;如何设计和将这些本地市场整合到现有市场结构中&#xff0c;并从中获得最大利润仍然不清楚。在本文中&#xff0c;我们提出了一个本地市场…

中睿天下Coremail | 2023年第二季度企业邮箱安全态势观察

今日&#xff0c;中睿天下联合Coremail邮件安全发布《2023第二季度企业邮箱安全性研究报告》&#xff0c;对2023第二季度和2023上半年的企业邮箱的安全风险进行了分析。 一 垃圾邮件同比下降16.38% 根据监测&#xff0c;2023年Q2垃圾邮件数量达到6.47亿封&#xff0c;环比下降…

003-Spring boot 启动流程分析

目录 启动流程分析创建 SpringApplication启动 run(String... args) 启动流程分析 SpringApplication.run(App.class, args);return new SpringApplication(primarySources).run(args);创建 SpringApplication SpringApplication(primarySources):this.primarySources new L…

opencv图片灰度二值化

INCLUDEPATH D:\work\opencv_3.4.2_Qt\include LIBS D:\work\opencv_3.4.2_Qt\x86\bin\libopencv_*.dll #include <iostream> #include<opencv2/opencv.hpp> //引入头文件using namespace cv; //命名空间 using namespace std;//opencv这个机器视…

Springloc和aop的基础概念

什么是控制反转和依赖注入&#xff1f; 控制反转(IoC)和依赖注入(DI)是软件开发中常用的编程范式&#xff0c; 它们极大地提高了代码可维护性和可复用性&#xff0c;简化了代码结构。 什么是控制反转(IoC) 控制反转是- - 种编程模式&#xff0c;它将应用程序中的控制权转移到…

使用 SSL/TLS 加强 MQTT 通信安全

在之前的文章中&#xff0c;我们探讨了认证和访问控制机制。接下来&#xff0c;我们将介绍传输层安全协议&#xff08;TLS&#xff09;在提升 MQTT 通信安全方面的重要作用。本文将着重介绍 TLS 以及它如何保证 MQTT 通信的完整性、机密性和真实性。 概念解释 在开始之前&…

TypeScript项目中Axios的封装

目录 前言 一、axios中的常见类型 1. AxiosInstance 2. AxiosRequestConfig 3. AxiosResponse 4. AxiosError 二、axios封装步骤 三、封装后的完整代码 1. 基础封装 2. 高级封装 前言 为了实现统一的网络请求处理和管理&#xff0c;在日常开发中我们常常封装 axios&…