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

news2024/11/25 14:48:01

方法栈

方法栈并不是某一个 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();

        // 手动 GC
        System.gc();
    }
}

各状态分析

images

images

images

images

images

images

栈和堆的区别

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

  • 内存分配方式

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

  • 内存管理方式

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

  • 内存大小

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

  • 访问速度

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

  • 应用场景

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

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

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

相关文章

【效率系列】简化繁杂表单数据处理,迅速增强办公数字化能力

写在前面 数据处理是数据分析的奠基石&#xff0c;只有使用处理干净的数据&#xff0c;分析才会产生价值。简单而言&#xff0c;数据处理的终极目的是将非结构化数据转换为结构化数据。 虽然数据处理自身未必直接产生数据价值&#xff0c;但其过程往往相当耗时&#xff0c;因此…

【Vue3-Router】历史记录

replace App.vue <template><h1>hello world</h1><div><!-- replace 不保存历史记录 --><router-link replace to"/">login</router-link><router-link replace style"margin-left: 10px;" to"/reg&q…

高效实用小工具之Everything

一&#xff0c;简介 有时候我们电脑文件较多时&#xff0c;想快速找到某个文件不是一件容易的事情&#xff0c;实用windows自带的搜素太耗时&#xff0c;效率不高。今天推荐一个用来搜索电脑文件的小工具——Everything&#xff0c;本文将介绍如何安装以及使用everything&…

Redis——三个特殊的数据类型+事务

概述 全称为远程字典服务。 Redis——基础篇(包含redis在云服务上的docker化安装和连接以及常用命令)_连接docker中的redis_北岭山脚鼠鼠的博客-CSDN博客 Redis能干什么&#xff1f; 1.内存存储、持久化&#xff0c;内存中是断电即失&#xff0c;因此持久化很重要&#xff…

C++ 虚继承

C棱形继承 在 C 中&#xff0c;在使用 多继承 时&#xff0c;如果发生了如果类 A 派生出类 B 和类 C&#xff0c;类 D 继承自类 B 和类 C&#xff0c;这时候就发生了菱形继承。 如果发生了菱形继承&#xff0c;这个时候类 A 中的 成员变量 和 成员函数 继承到类 D 中变成了两…

开源可商业运营的ChatGpt网页源码v1.2.2

&#x1f916; 主要功能 后台管理系统,可对用户,Token,商品,卡密等进行管理 精心设计的 UI&#xff0c;响应式设计 极快的首屏加载速度&#xff08;~100kb&#xff09; 支持Midjourney绘画和DALLE模型绘画,GPT4等应用 海量的内置 prompt 列表&#xff0c;来自中文和英文 一键导…

CV_tutorial1

CV Entry-Level Recurrent Neural Networks序列数据sequence data语言模型languag model循环神经网络recurrent neural networks门控循环单元gated recurrent unit长短期记忆网络long short-term memory OpenCV 图形图像操作文档矫正Gamma变化开运算 传统图像分割分水岭算法 Re…

NX/UG二次开发—建模—文字中心线提取思路简介

一、中心线提取 1、离散文字平面&#xff1a; 离散文字平面&#xff0c;构建一个二维矩阵数组&#xff08;实际操作时用的一维数组&#xff09; 1.1最小边界盒子&#xff1a; 可以计算文字平面的最小边界盒子&#xff0c;然后按步距在平面上采点&#xff0c;优点是点距比较…

【TODO】米哈游20230813笔试第三题

是计算抽中什么当期五星的期望。 现在的程序结果是99.6087。结果不对&#xff0c;有时间再调。 #include <iostream> #include <bits/stdc.h> typedef long long LL; using namespace std;int n 90; double p; // double min_p 1e-7; double min_p 0.0000000000…

根据源码,模拟实现 RabbitMQ - 通过 SQLite + MyBatis 设计数据库(2)

目录 一、数据库设计 1.1、数据库选择 1.2、环境配置 1.3、建库建表接口实现 1.4、封装数据库操作 1.5、针对 DataBaseManager 进行单元测试 一、数据库设计 1.1、数据库选择 MySQL 是我们最熟悉的数据库&#xff0c;但是这里我们选择使用 SQLite&#xff0c;原因如下&am…

什么是多线程?进程和线程的区别是什么?如何使用Java实现多线程?

文章目录 前言我们为什么要使用线程而不是进程来实现并发编程什么是线程进程和线程的区别如何使用Java实现多线程创建线程1.创建一个继承 Thread 类的线程类2.实现 Runnable 接口匿名内部类方式实现 Runnable 接口lambda 表达式实现 Runnable 接口 Thread 类的常见构造方法Thre…

NO.1 MyBatis配置文件:配置连接数据库的环境,实现数据库连接

目录 1、MyBatis配置数据库环境的连接方式 1.1连接方式一&#xff1a;MyBatis核心配置文件配置数据库连接信息 1.2连接方式二&#xff1a;在MyBatis核心配置文件中引入properties文件&#xff0c;配置数据库的环境 2、MyBatisd核心配置文件连接数据库的环境完整配置信息 3…

基于Matlab实现心电信号小波特征提取和对应疾病识别仿真(附上源码+数据集)

本文基于Matlab平台&#xff0c;研究了心电信号的小波特征提取方法&#xff0c;并应用于心电信号疾病识别仿真实验中。首先&#xff0c;介绍了心电信号的基本特征和常见的心电疾病。然后&#xff0c;详细阐述了小波变换的原理和方法&#xff0c;并提出了一种基于小波分解和小波…

[Leetcode] [Tutorial] 多维动态规划(未完待续)

文章目录 62. 不同路径Solution 62. 不同路径 一个机器人位于一个 m ∗ * ∗ n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角。 问总共有多少条不同的路径&#xff1f; 示例…

线程转换状态,傻傻分不清等待和阻塞吗?你还在暴力的停止线程吗?

线程切换 线程创建之后&#xff0c;调用start()方法开始运行。当线程执行wait()方法之后&#xff0c;线程进入等待状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态&#xff0c;而超时等待状态相当于在等待状态的基础上增加了超时限制&#xff0c;也就是超…

StringJoiner

1、为什么要学习StringJoiner&#xff1f; 2、StringJoiner概述 StringJoiner跟StringBuilder一样&#xff0c;也可以看成一个容器&#xff0c;创建之后里面的内容是可变的。 2.1、作用 提高字符串的操作效率&#xff0c;而且代码编写特别简洁&#xff0c;但是目前市场上很少有…

用友时空KSOA SQL注入漏洞复现(HW0day)

0x01 产品简介 用友时空KSOA是建立在SOA理念指导下研发的新一代产品&#xff0c;是根据流通企业最前沿的I需求推出的统一的IT基础架构&#xff0c;它可以让流通企业各个时期建立的IT系统之间彼此轻松对话&#xff0c;帮助流通企业保护原有的IT投资&#xff0c;简化IT管理&#…

学习笔记整理-JS-03-表达式和运算符

[[toc]] 一、表达式和运算符 1. 表达式 表达式种类 算术、关系、逻辑、赋值、综合 二、JS基本表达式 1. 算术运算符 意义运算符加减-乘*除/取余% 加减乘除 加减的符号和数学一致&#xff0c;乘号是*号&#xff0c;除法是/号默认情况&#xff0c;乘除法的优先级高于加法和…

【软件工程】软件测试

软件测试的对象 软件程序文档 测试对象&#xff1a;各个阶段产生的源程序和文档。 软件测试的目的 基于不同的立场&#xff0c;对软件测试的目的存在着两种完全对立的观点。 &#xff08;1&#xff09;一种观点是通过测试暴露出软件中所包含的故障和缺陷(从用户的角度)&#xf…

ORB-SLAM2第一节---单目地图初始化

单目初始化 1.前提条件&#xff08;640*480&#xff09; 参与初始化的两帧各自的特征点数目都需要大于100.两帧特征点成功匹配的数目需要大于或等于100.两帧特征点三角化成功的三维点数目需要大于50. 2.针对条件三 流程如下 记录当前帧和参考帧&#xff08;第一帧&#xff…