【JVM】JVM基础教程(三)

news2024/12/13 22:53:28

上一章:【JVM】JVM基础教程(二)-CSDN博客

目录

运行时数据区

应用场景

程序计数器

程序计数器在运行时会出现内存溢出吗?

IDEA的debug工具查看栈帧的内容

栈帧的组成

局部变量表

关于 this 的内存存储

操作数栈

帧数据

栈内存溢出

栈的默认大小

栈内存溢出模拟

栈修改大小

IDEA中修改栈默认大小

注意事项

本地方法栈

堆内存溢出模拟

Java堆

arthas中堆内存相关的功能

堆修改大小

方法区

类的元信息

运行时常量池 

方法区溢出模拟

字符串常量池 

练习

练习

练习

静态变量的存储

直接内存

最后

Java的内存分成哪几部分?

Java内存中哪些部分会内存溢出?

JDK7和8在内存结构上的区别是什么?



运行时数据区

Java虚拟机在运行Java程序过程中管理的内存区域,称之为运行时数据区

《Java虚拟机规范》中规定了每一部分的作用

应用场景

面试

内存溢出

内存调优

程序计数器

程序计数器(Program Counter Register)也叫PC寄存器,每个线程会通过程序计数器记录当前要执行的字节码指定的地址,换句话说,它的作用是在CPU执行指令时保持跟踪,确保顺序执行指令

在加载阶段,虚拟机将字节码文件中的指令读取到内存中之后,会将原文件中的偏移量转换成内存地址。每一条字节码指令都会拥有一个内存地址

在代码执行过程中,程序计数器会记录下一行字节码指令的地址。执行完当前指令之后,虚拟机的执行引擎根据程序计数器执行下一行指令

程序计数器可以控制程序指令的进行,实现分支、跳转、异常等逻辑

在多线程执行情况下,Java虚拟机需要通过程序计数器记录CPU切换前解释执行到那一句指令并继 续解释运行。

程序计数器在运行时会出现内存溢出吗?

  • 内存溢出指的是:程序在运行过程中超出了分配给它的内存限制,导致系统无法工作,程序在使用某一块内存区域时,存放的数据需要占用的内存大小超过了虚拟机能提供的内存上限
  • 因为每个线程只存储一个固定长度的内存地址,程序计数器是不会发生内存溢出的。
  • 程序员无需堆程序计数器做任何处理
  • 内存溢出与程序计数器并没有直接关系,因为程序计数器的大小是固定的,不会由于运行时数据或指令数量的增加而改变

可能导致的混淆

  • 栈溢出:当递归调用过深或使用大量局部变量时,可能导致栈内存不足,进而引发栈溢出。
  • 堆溢出:当动态分配的内存(如使用 mallocnew 等)超过可用内存时,可能导致堆溢出。

Java虚拟机栈(JVMS:Java Virtual Machine Stack)采用栈的数据结构来管理方法调用的基本数据,先进后出(First In Last Out),每一个方法的调用使用一个栈帧(Stack Frame)来保存

IDEA的debug工具查看栈帧的内容

Java虚拟机栈随着线程的创建而创建,而回收则会在线程的销毁时进行。

由于方法可能会在不同线程中执行,每个线程都会包含一个自己的虚拟机栈


栈帧的组成

局部变量表

局部变量表的作用是在运行过程中存放所有的局部变量

编译成字节码文件时,就可以确定局部变量表的内容

栈帧中的局部变量表是一个数组,数组中每一个位置称之为槽(slot),long和double类型占两个槽(难道说,一个槽4个字节?),其他类型占用一个槽

实例方法中的序号为0的位置存放的是this,指的是当前调用方法的对象,运行时会在内存中存放实例对象 的地址

关于 this 的内存存储
  1. 栈内存:

    • 在 Java 中,方法的局部变量(包括 this 引用)会存储在栈内存中。当一个方法被调用时,会创建一个栈帧,该帧包含所有的局部变量。
    • 因此,当你在某个实例方法中使用 this 时,this 引用的地址会被存储在栈内存中的栈帧里。
  2. 堆内存:

    • 实际的对象实例(即 this 所引用的对象)则存储在堆内存中。Java 的对象是在堆上分配内存的,而栈只存储对这些对象的引用。

方法参数也会保存在局部变量表中,其顺序与方法中参数定义的顺序一致。

局部变量表保存的内容有:实例方法的this对象,方法的参数,方法体中声明的局部变量。

下列代码会在局部变量表中占用几个槽 ?

public class MyTest {
    public void test4(int k, int m){
        {
            int a = 1;
            int b = 2;
        }
        {
            int c = 1;
        }
        int i = 0;
        long j = 1;
    }
}

最初我以为是9个

  • this变量:1个
  • 方法形参:2个
  • 局部代码块:2 + 1个
  • 局部变量:1 + 2个

一共 (1 + 2 + 2 + 1 + 1 + 2) = 9个

后来用jclassdb工具查看字节码命令

画图分析一下

一开始按照顺序存放局部变量this, k, m, a, b

这个时候,JVM做了优化,第一个局部代码块结束了,发现变量a, b处于【未被使用】状态,于是乎,后面新存入局部变量会替换a, b

一开始c替换a,然后c也被优化了

所以,占用了6个槽


操作数栈

操作数栈是栈帧中虚拟机在执行指令过程中用来存放临时数据的一块区域

他是一种栈式的数据结构,如 果一条指令将一个值压入操作数栈,则后面的指令可以弹出并使用该值

帧数据

帧数据主要包含动态连接、方法出口、异常表的引用

 

当前类的字节码指令引用了其他类的属性和方法时,需要将符号引用(编号)转换成对应的运行时常量池中的内存地址

  • 动态链接就保存了编号到运行时常量池的内存地址的映射关系

  • 方法出口指的是方法在正确结束或者异常结束时,当前栈帧会被弹出,同时程序计数器(PC寄存器)应该指向上一个栈帧中的下一条指令的地址。所以在当前栈帧中,需要存储此方法出口的地址
  • 换句话说,方法出口就是一个抽象的概念,而不是具体的内存空间,是方法执行完毕后控制流的转移过程和逻辑,而不是像动态连接一样是某个特定的内存地址或者变量
  • 是当方法执行完毕时,控制权从当前方法返回到调用该方法的地方。
  • 方法出口在JVM栈帧中是通过调用约定来实现的,包括返回地址和返回值的处理。栈帧确保在方法调用结束时能够正确恢复到调用者的上下文。

异常表

异常表存放的是代码中异常的处理信息,包含了异常捕获的生效范围以及异常发生后跳转到的字节码指令位置

栈内存溢出

JVM虚拟机如果栈帧过多,占用内存超过栈内存可以分配的最大值,就会出现内存溢出

JVM栈内存溢出时会出现StackOverFlowError的错误

栈的默认大小

如果我们不指定栈的大小,JVM将创建一个默认大小的栈。大小取决于操作系统和计算机的体系结构

栈内存溢出模拟

需求: 使用递归让方法调用自身,但是不设置退出条件。 定义调用次数的变量,每一次调用让变量加1。 查看错误发生时总调用的次数。

public class MyTest {
    public static int count = 0;
    // 递归方法调用自己
    public static void recursion(){
        System.out.println(++count);
        recursion();
    }
    public static void main(String[] args) {
        recursion();
    }
}

所以一共压栈了10655个栈帧,不过每个栈帧的占存不是固定的,要考虑方法内的局部变量个数、类型,还有操作数栈大小吗,动态链接信息,返回地址,也就是方法出口等

栈修改大小

  • 要修改JVM的每个线程的栈内存大小,可以使用虚拟机参数 -Xss
  • 语法:-Xss栈大小
  • 单位:字节(默认,必须是1024的倍数)、k或者K(KB)、m或者M(MB)、g或者G(GB)

IDEA中修改栈默认大小

此时多了一个虚拟机选项

修改成1MB,每个线程都有独立的栈内存,每个线程1MB,只要不是太多递归调用,还有非常复杂的方法,其实也够用的,不够的话会发现程序在使用过程中出现了栈溢出错误,那就设置大一点呗,一般都是 512KB ~ 2MB

注意事项

1.与-Xss类似,也可以使用-XX:ThreadStackSize 调整标志来配置堆栈大小

格式为:-XX:ThreadStackSize=1024

2.HotSpot版的JVM对栈大小的最大值和最小值有要求:

比如测试如下两个参数:

        -Xss1k

        -Xss1025m

Win10_x64操作系统下的JDK8测试最小值为180k,最大值为1024m

3.局部变量过多、操作数栈深度过大也会影响栈内存的大小

一般情况下,工作中即便使用了递归进行操作,栈的深度最多也就只能到几百,不会出现栈的溢出。所以此参数可以手动指定为-Xss256k,节省内存

本地方法栈

1.JVM虚拟机栈存储了Java方法调用时的栈帧,而本地方法栈存储的是native本地方法的栈帧

2.在HotSpot虚拟机中,Java虚拟机栈和本地方法栈实际上使用了同一个栈空间。本地方法栈会在栈内生成一个栈帧,临时保存方法的参数同时方便出现异常时也把本地方法的栈信息打印出来


一般Java程序中堆内存是空间最大的一块内存区域。创建出来的对象都在于堆上。

栈上的局部变量表中,可以存放堆上对象的引用。静态变量也可以存放堆对象的引用,通过静态变量就可以实现对象在线程之间共享

堆内存溢出模拟

需求: 通过new关键字不停创建对象,放入集合中,模拟堆内存的溢出,观察堆溢出之后的异 常信息。

现象: 堆内存大小是有上限的,当对象一直向堆中放入对象达到上限之后,就会抛出OutOfMemory 错误。

Java堆

堆空间有三个需要关注的值:used、total、max

used指的是当前已使用的堆内存,total是JVM已经分配的可用堆内存,max是JVM可以分配的最大堆内存

随着堆中的对象增多,当total可以使用的内存即将不足时,JVM会继续分配内存给堆

如果堆内存不足,JVM就会不断的分配内存,total值会变大,total最大只能与max相等

如果不设置任何的虚拟机参数,max默认是系统内存的 1/4,也就是16g的内存条,去掉2G左右的系统占用,差不多能占用3300MB - 3700MB

total默认是系统内存的1/64,在实际引用中一般都需要设置total和max的值

java (oracle.com)

Java服务端程序开发时,建议将-Xmx和-Xms设置为相同的值,这样在程序启动之后可使用的总内存就是最大内存,而无 需向java虚拟机再次申请,减少了申请并分配内存时间上的开销,同时也不会出现内存过剩之后堆收缩的情况。

-Xmx具体设置的值与实际的应用程序运行环境有关,在《实战篇》中会给出设置方案。

arthas中堆内存相关的功能

相关博客:【JVM】JVM基础教程(一)-CSDN博客

堆内存used total max三个值可以通过dashboard命令看到。

是不是当used = max = total的时候,堆内存就溢出了呢?

不是,堆内存溢出的判断条件比较复杂,在下一章《垃圾回收器》中会详细 介绍

堆修改大小

  • 要修改堆的大小,可以使用JVM参数 -Xmx(max最大值)和 -Xms(初始的total)
  • 语法:-Xmx值 -Xms值
  • 单位:字节(默认,必须是1024的倍数)、、k或者K(KB)、m或者M(MB)、g或者G(GB)
  • 限制:Xmx必须大于 2 MB,Xms必须大于1MB

为什么arthas中显示的heap堆大小与设置的值不一样呢?

arthas中的heap堆内存使用了JMX技术中内存获取方式,这种方式与垃圾回 收器有关,计算的是可以分配对象的内存,而不是整个内存。

方法区

方法区是存放基础信息的位置,线程共享,主要包含三部分内容:

方法区(Method Area)除了存储类的元信息之外,还存放了运行时常量池

常量池中存放的是字节码中的常量池内容

类的元信息

字节码文件中通过编号查表的方式找到常量,这种常量池称为静态常量池

运行时常量池 

当常量池加载到内存中之后,可以通过内存地址快速的定位到常量池中的内容,这种常量池称为运行时常量池

方法区是《Java虚拟机规范》中设计的虚拟概念,每款Java虚拟机在实现上都各不相同。Hotspot设计如下:

  • JDK7及之前的版本将方法区存放在堆区域中的永久代空间,堆的大小由虚拟机参数来控制。
  • JDK8及之后的版本将方法区存放在元空间中,元空间位于操作系统维护的直接内存中,默认情况下只要不 超过操作系统承受的上限,可以一直分配

方法区溢出模拟

需求:通过ByteBuddy框架,动态生成字节码数据,加载到内存中。通过死循环不停地加载到方法区,观察方法区是否会出现内存溢出的情况。分别在JDK7和JDK8上运行上述代码

实验发现,JDK7上运行大概十几万次,就出现了错误。在JDK8上运行上百万次,程序依旧好好的,但是内存会直线升高。这说明JDK7和JDK8在方法区的存放上,采用了不同的设计。

  • JDK7将方法区存放在堆区域中的永久代空间,堆的大小由虚拟机参数-XX:MaxPermSize=值来控制。
  • JDK8将方法区存放在元空间中,元空间位于操作系统维护的直接内存中,默认情况下只要不超过操作系统承受 的上限,可以一直分配。可以使用-XX:MaxMetaspaceSize=值将元空间最大大小进行限制

字符串常量池 

方法区中除了类的元信息、运行常量池之外,还有一块区域叫字符串常量池(StringTable)

字符串常量池存储在代码中定义的常量字符串内容中。比如“123” 这个123就会被放入字符串常量池。

字符串常量池和运行时常量池有什么关系?

早期设计时,字符串常量池是属于运行时常量池的一部分,他们存储的位置也是一致的。后续做出了调整, 将字符串常量池和运行时常量池做了拆分。

练习

public class MyTest {
    public static void main(String[] args) {
        String a = "1";
        String b = "2";
        String c = "12";
        String d = a + b;
        System.out.println(c == d);
    }
}

流程分析大概是:

加载字符串三个常量 "1" "2" "12" 到操作数栈

创建一个StringBuilder 对象

将操作数栈顶的对象引用复制,并将两个相同的引用都放到栈上

调用 StringBuilder 的构造方法,初始化 StringBuilder 实例

加载局部变量表中索引1、2的值,也就是 "1" "2"

调用 StringBuilder 的 append 方法,将 "1" 添加到 StringBuilder 中

再次调用 append 方法,将 "2" 添加到 StringBuilder 中

调用 toString 方法,将 StringBuilder 中的内容转换为字符串

将结果("12")存储到局部变量表中的索引4(即变量 'd')

 

获取 System.out 的静态引用

加载局部变量表中索引3、4的值,也就是俩 "12"

if_acmpne 42 (+7)                                                  // 如果 'c' 和 'd' 不相等,则跳转到指令42

iconst_1                                                                         // 将值1(true)推入栈中

goto 43 (+4)                                                                 // 跳转到指令43

iconst_0                                                                         // 将值0(false)推入栈中

invokevirtual #10 <java/io/PrintStream.println : (Z)V> // 打印 boolean 值到控制台

关键点:常量池中 的两个字符串通过StringBuilder拼接后的字符串,会存入堆内存

练习

public class MyTest {
    public static void main(String[] args) {
        String a = "1";
        String b = "2";
        String c = "12";
        String d = "1" + "2";
        System.out.println(c == d);
    }
}

能很直观的看出来,并没有调用StringBuilder对象的append方法进行拼接,而是隐式了一些细节操作,这应该就是JVM做的优化了

经查询资料

具体原因

  1. String d = "1" + "2";

    • 这个表达式在编译时就能够确定其值,因此 Java 编译器会将它优化为字符串常量 "12",并存储在字符串常量池中。因此,d 实际上引用的是常量池中的 "12" 对象。
  2. String e = a + b;

    • 这里的 a 和 b 是变量,它们的值在运行时才确定。使用 + 运算符进行拼接时,Java 会创建一个新的字符串对象(通常是在堆内存中),即使拼接的结果是 "12"e 引用的也是一个新的对象。因此,e 和 c 引用的是两个不同的对象。

练习

静态变量的存储

直接内存

直接内存(Direct Memory)并不在《Java虚拟机规范》中存在,所以并不属于Java运行时的内存区域。

在 JDK 1.4 中引入了 NIO 机制,使用了直接内存

主要为了解决以下两个问题:

  1、Java堆中的对象如果不再使用要回收,回收时会影响对象的创建和使用。

  2、IO操作比如读文件,需要先把文件读入直接内存(缓冲区)再把数据复制到Java堆中。

现在直接放入直接内存即可,同时Java堆上维护直接内存的引用,减少了数据复制的开销。写文件也是类似的思路。

  • 要创建直接内存上的数据,可以使用ByteBuffer
  • 语法: ByteBuffer directBuffer = ByteBuffer.allocateDirect(size);
  • 注意事项:arthas的memory命令可以查看直接内存的大小,属性名direct

如果需要手动调整直接内存的大小,可以使用-XX:MaxDirectMemorySize=大小

单位k或K表示千字节,m或M表示兆字节,g或G表示千兆字节。

默认不设置该参数情况下,JVM 自动选择 最 大分配的大小

以下示例以不同的单位说明如何将 直接内存大小设置为 1024 KB:-XX:MaxDirectMemorySize=1m -XX:MaxDirectMemorySize=1024k -XX:MaxDirectMemorySize=104857

最后


Java的内存分成哪几部分?

程序计数器、JVM栈,本地方法栈、堆内存、方法区

Java内存中哪些部分会内存溢出?

堆、栈、方法区会内存溢出

JDK7和8在内存结构上的区别是什么?

下一章:【JVM】JVM基础教程(四)-CSDN博客

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

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

相关文章

Postman Sandbox 项目教程

Postman Sandbox 项目教程 postman-sandbox Sandbox for Postman Scripts to run in Node.js or browser 项目地址: https://gitcode.com/gh_mirrors/po/postman-sandbox 1. 项目介绍 Postman Sandbox 是一个用于在 Node.js 或浏览器中执行 Postman 脚本的沙盒环境。它…

Maven、mybatis框架

一、Maven介绍 1.概念&#xff1a; Maven项目对象模型(POM)&#xff0c;可以通过一小段描述信息来管理项目的构建&#xff0c;报告和文档的项目管理工具软件。 2.为啥使用maven: 之前项目中需要引入大量的jar包。这些jar从网上下载&#xff0c;可能下载地址不同意。这些jar之间…

Python连接和操作Elasticsearch详细指南

Python连接和操作Elasticsearch详细指南 一、服务器端配置1. 修改 Elasticsearch 配置文件2. 开放防火墙端口 二、本地 Python 连接 Elasticsearch1. 连接 Elasticsearch2. 索引操作3. 文档操作4. 搜索内容5. 聚合查询6. 批量操作 三、注意事项四、故障排除结论 Elasticsearch …

获得日志记录之外的新视角:应用程序性能监控简介(APM)

作者&#xff1a;来自 Elastic David Hope 日志记录领域即将发生改变。在这篇文章中&#xff0c;我们将概述从单纯的日志记录到包含日志、跟踪和 APM 的完全集成解决方案的推荐流程。 通过 APM 和跟踪优先考虑客户体验 企业软件开发和运营已成为一个有趣的领域。我们拥有一些非…

Python - 面向对象;类和对象;方法属性;init,self;魔法方法;析构方法;函数方法区别(六)

一、面向对象编程&#xff08;OOP&#xff09; 定义 面向过程(Procedure Oriented Programming, POP)是一种程序设计范式&#xff0c;主要关注的是实现功能的步骤&#xff0c;设计时模块化和流程化。面向过程编程是一种以过程为中心的编程方式&#xff0c;它将问题分解成一系…

源码编译安装MySQL

MySQL相应版本的tar包下载 在5.7的版本的MySQL编译安装的时候&#xff0c;需要依赖C语言的库文件【boost】&#xff0c; 如上图所示&#xff0c;如果你使用第一个MySQL的tar包&#xff0c;还需要去网上去下载boost即C语言的库文件&#xff0c;但是第二个tar包就既包含MySQL的源…

关于Kubernetes(K8S)认证含金量?

Kubernetes越来越流行&#xff0c;目前它是市场上最佳的容器编排工具之一&#xff0c;也是运维工程师必备的技能之一。 大厂都在用K8S&#xff08;就业行情&#xff09; 虽说今年的大环境不是很好&#xff0c;但是从招聘数据来看&#xff0c;K8S岗位薪资不降反而上涨不…

Linux / Windows | ping IP + Port 测试

注&#xff1a;本文为 “Linux / Windows | ping IP Port 测试端口通畅” 相关文章合辑。 未整理去重。 windows 如何确认服务器上程序端口是否正常&#xff08;ping、tcping&#xff09; 三希已于 2023-05-22 18:08:06 修改 方式 1&#xff1a;ping 命令 ping 命令说明 p…

C++打造局域网聊天室第七课: Socket编程初步2

文章目录 前言一、Socket的API函数二、服务端建立Socket步骤总结 前言 C打造局域网聊天室第七课&#xff1a; Socket编程初步2 一、Socket的API函数 接着上一课的内容&#xff0c;我们在chartroom.cpp中找到如下位置 插入断点&#xff0c;运行 运行到断点处后&#xff0c;按…

vue-router路由传参的两种方式(params 和 query )

一、vue-router路由传参问题 1、概念&#xff1a; A、vue 路由传参的使用场景一般应用在父路由跳转到子路由时&#xff0c;携带参数跳转。 B、传参方式可划分为 params 传参和 query 传参&#xff1b; C、而 params 传参又可分为在 url 中显示参数和不显示参数两种方式&#x…

Docker Compose应用实战

文章目录 1、使用Docker Compose必要性及定义2、Docker Compose应用参考资料3、Docker Compose应用最佳实践步骤1_概念2_步骤 4、Docker Compose安装5、Docker Compose应用案例1_网站文件准备2_Dockerfile文件准备3_Compose文件准备4_使用docker-compose up启动容器5_访问6_常见…

el-table组件树形数据修改展开箭头

<style lang"scss" scoped> ::v-deep .el-table__expand-icon .el-icon-arrow-right:before {content: ">"; // 箭头样式font-size: 16px; }::v-deep .el-table__expand-icon{ // 没有展开的状态background-color: rgba(241, 242, 245, 1);color:…

5.2 JavaScript 案例 - 轮播图

JavaScript - 轮播图 文章目录 JavaScript - 轮播图基础模版一、刷新页面随机轮播图案例二、轮播图 定时器版三、轮播图完整版 基础模版 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta http-equiv"…

MongoDB与阿里云庆祝合作五周年,展望AI赋能新未来

12月3日&#xff0c;在印尼举行的阿里云合作伙伴大会2024上&#xff0c;MongoDB荣膺阿里云“2024技术创新成就奖”&#xff0c;该奖项旨在表彰与阿里云保持长期稳定合作&#xff0c;通过深度技术融合&#xff0c;在产品技术创新、行业区域深耕等领域取得卓越成就的伙伴。自2019…

数据结构(Queue队列)

前言&#xff1a; 在计算机科学中&#xff0c;数据结构是构建高效算法和程序的基础&#xff0c;而队列&#xff08;Queue&#xff09;作为一种经典的线性数据结构&#xff0c;具有重要的地位。与栈&#xff08;Stack&#xff09;不同&#xff0c;队列遵循“先进先出”&#xf…

EDA - Spring Boot构建基于事件驱动的消息系统

文章目录 概述事件驱动架构的基本概念工程结构Code创建事件和事件处理器创建事件总线创建消息通道和发送逻辑创建事件处理器消息持久化创建消息发送事件配置 Spring Boot 启动类测试消息消费运行项目 概述 在微服务架构和大规模分布式系统中&#xff0c;事件驱动架构&#xff…

仿iOS日历、飞书日历、Google日历的日模式

仿iOS日历、飞书日历、Google日历的日模式&#xff0c;24H内事件可自由上下拖动、自由拉伸。 以下是效果图&#xff1a; 具体实现比较简单&#xff0c;代码如下&#xff1a; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color;…

软考高级架构 - 10.5 软件架构演化评估方法

10.4 软件架构演化原则总结 本节提出了18条架构演化的核心原则&#xff0c;并为每条原则设计了简单而有效的度量方法&#xff0c;用于从系统整体层面提供实用信息&#xff0c;帮助评估和指导架构演化。 演化成本控制&#xff1a;成本小于重新开发成本&#xff0c;经济高效。进…

DocFlow票据AI自动化处理工具:出色的文档解析+抽取能力,提升企业文档数字化管理效能

目录 财务应付 金融信贷业务 近期&#xff0c;DocFlow票据自动化产品正式上线。DocFlow是一款票据AI自动化处理工具&#xff0c;支持不同版式单据智能分类扩展&#xff0c;可选功能插件配置流程&#xff0c;满足多样业务场景。 随着全球化与信息化进程&#xff0c;企业的文件…

C# 探险之旅:第二节 - 定义变量与变量赋值

欢迎再次踏上我们的C#学习之旅。今天&#xff0c;我们要聊一个超级重要又好玩的话题——定义变量与变量赋值。想象一下&#xff0c;你正站在一个魔法森林里&#xff0c;手里拿着一本空白的魔法书&#xff08;其实就是你的代码编辑器&#xff09;&#xff0c;准备记录下各种神奇…