JVM笔记1--Java内存区域

1、运行时数据区域

image.png
从上图可以看出来,Java虚拟机运行时数据区域整体上可以分成5大块:

1.1、程序计数器

程序计数器是一块较小的内存空间。它可以看做当前线程所执行的字节码的行号指示器。在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条所需要执行的字节码指令。它是程序控制流的指示器、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器。
由于Java虚拟机的多线程是通过线程轮流切换分配处理器执行时间的方式来实现的。在任何一个时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条字节码执行。因此,为了线程切换后能恢复到正确的执行位置每个线程都需要一个独立的私有的程序计数器。各个线程间互不影响独立存储。我们称这种类型的内存区域为“线程私有”的内存。
如果线程正在执行一个Java方法,那么这个计数器记录的就是正在执行的字节码指令的地址。如果正在执行的是本地(Native)方法,那么这个计数器的值为空。同时,此内存区域是唯一一个在**《Java虚拟机规范》没有规定任何OutOfMemoryError**情况的区域。

1.2、Java虚拟机栈

与程序计数器类似,Java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行时,Java虚拟机都会同步创建一个栈帧用于存储局部变量表操作数栈动态链接方法出口等信息每个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中入栈出栈的过程
经常有人把Java内存区域笼统的换分为堆内存和栈内存。这种划分方式直接继承自C,C++程序内存布局结构。但是对于Java这种划分方式就显得比较粗糙。实际的内存区域划分比这更复杂。不过这种划分方式流行,也间接说明了程序员最关注的内存区域就是“堆”和“栈”。这里面的“栈”通常指的就是虚拟机栈。或者更多的情况下指的是虚拟机栈中的局部变量表部分。
局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(booleanbytecharshortintfloatlongdouble)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一个字节码指令的地址)。
这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位的long和double类型占用两个局部变量槽其余的数据类型占用一个变量槽局部变量表所需的内存空间在编译期完成分配当进入一个方法时,这个方法需要在栈中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。请注意,这里的**“大小”指的是变量槽的数量**。虚拟机真正使用多大的内存空间(例如一个变量槽占用32位还是64位)来实现一个变量槽,完全是由具体的虚拟机实现自行决定。
在《Java虚拟机规范》中,对这个内存区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机栈允许的最大深度,将抛出StackOverflowError异常;如果,Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存就会抛出OutOfMemoryError异常

1.3、本地方法栈

本地方法栈与虚拟机栈所发挥的作用非常相似。不过虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈为虚拟机执行本地方法服务
《Java虚拟机规范》对本地方法栈中方法使用的语言。使用方式与数据结构并没有任何强制规定。也就是说具体的虚拟机可以根据需要自由的实现它。甚至有的虚拟机(Hot-Spot虚拟机)直接将本地方法栈和虚拟机栈合二为一。与虚拟机栈一样本地方法栈也会在栈深度超出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。

PS:在HotSpot虚拟机中,并不区分虚拟机栈和本地方法栈,因此只能通过-Xss来设置栈的大小。
对于栈中的OutOfMemoryError,Java虚拟机规范中规定实现者自主选择是否支持栈动态扩展。如果不支持栈的动态扩展,那么在运行时是不会出现OutOfMemoryError异常错误的。只有在创建线程时,申请内存时就无法获取到足够内存才会出现OutOfMemoryError异常。在运行时只会出现由于栈容量无法容下新的栈帧而出现StackOverflowError。

验证出现StackOverflowError异常的方法:

1、使用-Xss来设置栈的容量
2、定义大量的本地变量,增大此方法栈中本地变量表的长度。

1.4、Java堆

Java堆是虚拟机管理的最大的一块内存区域。Java对是被所有线程共享的一块内存区域,在虚拟机启动时创建。在《Java虚拟机规范》中对Java堆的描述是“所有的对象实例以及数组都应当在堆上分配”。在《深入理解Java虚拟机》中作者是这样说的:“在Java世界里,几乎所有的对象实例都在Java堆上分配内存”。这里作者用的是“几乎”是指从实现角度上来说的。随着Java语言的发展,现在已经能看到些许迹象表明日后可能出现值类型的支持。即使在现在,由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配标量替换优化手段已经导致一些微妙的变化。所以说Java对象实例都分配在堆上也渐渐变得不是那么绝对了。
如果从分配内存的角度来看Java堆,所有线程共享的Java堆可以划分外出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),以提升对象分配的效率。但是无论怎么划分,都不会改变Java堆存储内容的共性,无论哪个区域存储的都是对象实例。
在《Java虚拟机规范》中规定,Java堆可以处于物理上不连续的内存空间中,但在逻辑上应被视为连续的。
Java堆既可以被实现成固定大小,也可以被实现为可扩展的。可以通过**-Xms,-Xmx**来设置堆的最小,最大内存(如果Xms和Xmx一样大,则不能动态扩展)。如果Java堆没有足够内存用来新实例创建,且无法扩展时,将会抛出OutOfMemoryError异常。

验证OutOfMemoryError异常方法

1、通过-Xms,-Xmx来设置堆的最小和最大容量,然后创建大量的类。

1.5、方法区

方法区与Java堆一样,也是线程间共享的内存区域。用于存储被虚拟机加载的类型信息常量静态变量、以及即时编译器编译后的代码缓存等数据。虽然**《Java虚拟机规范》中把方法区描述为堆的一部分**,但是它却有一个别名叫“非堆(Non-Heap)”,目的是与Java堆区分开。
在JDK8以前,由于很多人习惯在HotSpot虚拟机上开发,很多人更愿意把方法区称之为“永久代(Permanent Generation)”,或将两者混为一谈。但是本质上两者还是有区别的。因为仅仅是当时的HotSpot虚拟机的开发团队选择把收集器的分代设计扩展至方法区,或者使用永久代实现方法区而已。这样就可以让垃圾收集器管理方法区的内存回收。
JDK6之后,HotSpot虚拟机,就有了放弃永久代,逐步采用本地内存(Native Memory)来实现方法区的计划。在JDK7的HotSpot虚拟机中,已经将方法区中的字符串常量池、静态变量等移出(放到堆中)。到了JDK8,已完全放弃永久代的概念用在本地内存中实现的元空间(Meta-space)来替代,把JDK7中剩余的内容(主要是类型信息)全移动到元空间
《Java虚拟机规范》中规定,当方法区无法满足新的内存分配时,将抛出OutOfMemoryError异常。

MetaSpace VM 参数

1、-XX:MaxMetaspaceSize:设置元空间最大值,默认时-1,表示没有限制,或者说只受限于本地内存大小。
2、-XX:MetaspaceSize:设置元空间的初始大小。以字节为单位,达到该值就会触发垃圾收集进行类型卸载,,同时收集器会调整该值:如果释放了大量的空间,就适当降低该值;如果释放了少量空间,在不超过MaxMetaspaceSize情况下,适当提高该值。
3、-XX:MinMetaspaceFreeRatio:在垃圾收集之后控制最小的元空间剩余容量百分比。可以减少由于元空间不足导致的垃圾收集频率。相应的还有-XX:MaxMetaspaceFreeRatio控制元空间最大的剩余容量百分比。

1.6、运行时常量池

运行时常量池是是方法区的一部分Class文件中除了有类的版本字段方法接口等描述信息。还有一项信息就是常量池表,用于存放在编译期生成的各种字面量与符号引用这部分内容在类加载好之后存放到运行时常量池中
Java虚拟机对Class文件的每一部分(自然包括常量池)都有严格的规定。如每一个字节用来存储哪种数据都必须符合规范上的要求才能被虚拟机认可、加载和执行。但是对于运行时常量池,《Java虚拟机规范》并没有作任何细节上的要求。
运行时常量池相对于Class文件中的常量池区别在于,其具备动态性。Java语言并没有要求常量只能在编译期才能产生。也就是说,并非内置于Class文件常量池中的内容,在类加载后才能进入运行时常量池中。在运行期间也可以将新的常量放入到池中。就是String类的intern方法
运行时常量池也是方法区的一部分,所以其在没有足够内存进行新的内存分配时,也会抛出OutOfMemoryError异常。

验证OutOfMemoryError异常方法

1、在JDK7之前,常量池是放在永久代中的。因此可以使用-XX:PermSize=6m和-XX:MaxPermSize=6m来变相限制常量池的大小。
2、在JDK7之后,永久代逐渐被metaSpace取代,在JDK8中,永久代就已经不存在了。而常量池而迁移到到了堆中。因此只能通过限制堆的大小来限制常量池的大小。

1.7、直接内存

直接内存既不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也频繁的使用,且也可能产生OutOfMemoryError异常
在JDK1.4中新加入的NIO类,引入了一种基于通道与缓冲区的I/O方式。它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
显然,本机直接内存的分配不受Java堆大小的限制。但是,既然是内存,那肯定还是会受到本机总内存大小的限制,一般服务器管理人员配置虚拟机参数时,会根据实际内存去设置-Xmx等参数信息,但经常忽略掉直接内存,是得各个内存区域的总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError异常。

直接内存可以通过-XX:MaxDirectmemorySize来控制大小,如果不指定大小,则默认与Java堆最大值(-Xmx)一致。

PS:由直接内存导致的内存溢出,一个明显的特征是在HeapDump文件中不会看到有什么明显的异常情况。如果发现内存溢出之后产生的Dump文件很小,而程序中又间接使用了DirectMemory(典型的间接使用就是NIO),那就可以重点检查下直接内存方面的原因。

2、对象创建过程

  1. 代码执行到new指令位置时,首先去检查这个指令的参数能否在常量池定位到一个类的符号引用,并检查这个类的符号是否已被加载,如果没有加载将执行相应的类加载过程。
  2. 在类加载检查通过后,进行内存分配。为对象分配空间等同于将一块固定大小的内存从Java堆上划分开来。内存分配的方式有两种:
    1. 指针碰撞方式:假设Java堆是规整的,已经使用的内存放到一边,没使用的放到另一边,中间放着一个指针作为分界点的指示器。那么分配内存就是将指针向空闲的一方移动一段与对象大小相等的距离。这种分配方式为“指针碰撞”。
    2. 空闲列表:如果Java堆是不规整的。那么就需要一个列表存储哪些内存是已经使用的,哪些是没使用的。内存分配就是从空闲列表中找到一块足够大小的区域划分出与对象相同大小的内存区域,并更新列表上的记录。

选择哪种方式取决于Java堆是否规整,而Java堆是否规整,取决于采用的垃圾收集器是否带有空间压缩整理功能。因此当采用Serial、ParNew等带有压缩整理功能的收集器时,系统采用的就是指针碰撞方法;而当使用CMS这种基于清除算法的收集器时,系统采用的就是空闲列表方法。
除如何划分空间外,还有一个问题需要考虑:对象创建在虚拟机是非常频繁的操作,即使是移动指针的方式,在多线程下也不是线程安全的。虚拟机采用两种解决方案一种是采用CAS和失败重试的方法来保证更新操作的原子性;另一种是把内存分配的动作按照线程划分到不同的空间之中进行。即每个线程在java堆中先预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)哪个线程要分配内存,就在哪个线程中的本地缓冲区中分配只有当本地缓冲区用完了,分配新的缓存区时,才会进行同步锁定。虚拟机是否使用TLAB,通过**-XX:+/-UseTLAB**参数来设定。

  1. 内存分配之后,虚拟机将分配到的内存空间(不包括对象头)初始化零值,如果使用了TLAB的话,这一步也可以提前到TLAB执行。这步操作,保证Java对象的实例字段在不赋初始值时就能使用。读取到的就是各个数据类型的零值。
  2. 接下来,Java虚拟机对对象进行必要的设置,比如这个对象是哪个类的实例如何才能找到元数据信息对象的哈希码(实际上延迟到真正调用Object::hashCode()方法才会进行计算)、对象的GC分代年龄等。
  3. 到这一步,从Java虚拟机的角度来看,一个新的对象已经产生,但是从程序的角度来看,对象创建才刚刚开始——构造函数。即Class文件的**()**方法还未执行,这时对象的所有字段为默认的零值,对象需要的其他信息还未按照约预定的意图构造好。等执行()方法之后,一个真正的对象才算完全构造出来。

3、对象的内存布局

在HotSpot虚拟机中,对象在堆内存中的存储布局可以划分为三部分对象头(Header)实例数据(Instance Data)对齐填充(Padding)

3.1、对象头

HotSpot虚拟机对象头部分主要包括两部分数据:一个是Mark Word(标记字段),另一个就是类型指针(Klass Point)。其中如果Java对象是一个数组的话,那么还需要一块内存用来存储数组的长度。虚拟机介意通过普通Java对象的元数据信息确定Java对象的大小,但是如果数组的长度是不确定的,将无法通过元数据中的信息确定数组的大小。对象头的结构如下图:

长度内容说明
32/64位对象头存储hashCoe或者锁信息等
32/64位类型指针存储到对象类型数据的指针
32/32位数组长度数组的长度

3.1.1、Mark Word

Mark Word 用来存储对象自身运行时的数据,如哈希码GC分代年龄锁状态标志线程持有的锁偏向锁线程ID偏向时间戳等。这部分的数据在32位和64位的虚拟机(未开启压缩指针)中的长度分别为32位和64位。官方称之为“Mark Word”。在32位的虚拟机中Mark Word结构如下:
image.png
image.png

3.1.2、类型指针

类型指针即对象指向它的类型元数据的指针,Java通过这个指针来确定这个对象属于哪个类的实例。并不是所有虚拟机的实现都必须在对象数据上保留类型指针。

3.2、实例数据

实例数据部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的还是子类中定义的字段都必须记录下来。这部分的存储顺序会受到虚拟机分配策略参数(-XX:FieldsAllocationStyle参数)和字段在Java源码中定义的顺序有关。HotSpot虚拟机默认的分配顺序为:longs/doubles、ints、shorts/chars、bytes/booleans、oops,从以上默认分配策略中可以看到,相同宽度的字段总是被分配到一起。在满足这个条件的情况下,在父类中定义的字段会出现子类之前。

3.3、对齐填充

对齐填充并不是必然存在的,也没有特别的含义,只是起着占位符的作用。由于HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,话句话说就是任何对象的大小都必须是8字节的整数倍

4、对象的访问定位

创建对象自然是为了访问对象,我们Java程序通过栈上的reference数据来操作堆上的具体对象。由于reference类型在《Java虚拟机规范》里面只规定了它是一个指向对象的引用,并没有定义这个引用应该通过什么方式去定位、访问到堆中对象的具体位置。因此对象的访问方式也是有虚拟机进行实现。主流的访问方式主要有:使用句柄直接指针

  • 使用句柄:如果使用句柄访问的话,Java堆中将可能会划分出一块内存作为句柄池reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据类型数据各自的具体地址信息
  • 直接指针:如果使用直接指针的方式的话,Java堆中对象的内存布局就必须考虑如何放置访问类型的的相关信息,reference中存储的直接就是对象的地址,如果只是访问对象本身的话,就不要多一次间接访问的开销。

image.png
通过句柄访问对象
image.png
直接指针访问对象
以上两种访问方式各有优势。使用句柄的方式的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要改变
使用指针访问的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,因此此类开销积小成多也是一项可观的成本。HotSpot虚拟机主要使用的就是此种方式访问对象

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

OpenAI下周将发布ChatGPT搜索引擎,挑战谷歌搜索!

目前,多方位消息证实,OpenAI将会在5月9日上午10点公布该消息,大约是北京时间周五的凌晨2点。 5月3日,前Mila研究员、麻省理工讲师Lior S爆料,根据OpenAI最新的SSL证书日志显示,已经创建了search.chatgpt.c…

Java集合排序

1. 集合排序API 1.1 集合排序概述 集合排序是指对一个集合中的元素按照特定规则进行重新排列,以使得集合中的元素按照预定义的顺序呈现。 在集合排序中,通常需要定义一个比较规则,这个比较规则用于决定集合中的元素在排序后的顺序。元素之间…

KIE基于图模型的关键信息抽取源码详解

1.数据集准备 下载数据集 https://download.openmmlab.com/mmocr/data/wildreceipt.tar WildReceiptOpenset 准备好 WildReceipt。 转换 WildReceipt 成 OpenSet 格式: # 你可以运行以下命令以获取更多可用参数: # python tools/dataset_converters/kie/closeset_to_opens…

程序的机器级表示——Intel x86 汇编讲解

往期地址: 操作系统系列一 —— 操作系统概述操作系统系列二 —— 进程操作系统系列三 —— 编译与链接关系操作系统系列四 —— 栈与函数调用关系操作系统系列五 —— 目标文件详解操作系统系列六 —— 详细解释【静态链接】操作系统系列七 —— 装载操作系统系列…

java下乡扶贫志愿者招募管理系统springboot-vue

计算机技术在现代管理中的应用,使计算机成为人们应用现代技术的重要工具。能够有效的解决获取信息便捷化、全面化的问题,提高效率。 技术栈 前端:vue.jsElementUI 开发工具:IDEA 或者eclipse都支持 编程语言: java 框架&#xff1…

c++ 红黑树学习及简单实现

1. 了解红黑树 1.1. 概念 红黑树,是一种二叉搜索树,但在每个节点增加一个存储位表示节点的颜色,可以是红色,或是黑色,通过对任何一条从根到叶子的路径上各个节点的着色方式进行限制,红黑树确保没有一条路…

Dockerfile镜像实例

目录 一、构建SSH镜像 1. 建立工作目录 2. 生成镜像 3. 启动容器并修改root密码 二、systemctl镜像 1. 建立工作目录 2. 生成镜像 3. 运行镜像容器 ​编辑 4. 测试容器systemct 三、Nginx镜像 1. 建立工作目录 2. 编写Dockerfile脚本 3. 编写run.sh启动脚本 4. …

IDEA启动Tomcat启动失败:jar包未部署【部署jar包】

IDEA启动Tomcat报错java.lang.ClassNotFoundException:org.springframework.web.context.ContextLoaderListener:jar包未部署【部署jar包】 学习java,开始跟着教程的步伐学习maven下载jar包,tomcat启动项目,发现项目未启动成功也…

虾皮(Shopee)商品详情API接口:轻松获取商品深度信息

API接口概述 虾皮的商品详情API接口是专为商家和开发者提供的服务接口,通过该接口,您可以快速、准确地获取指定商品的详细信息。这些信息包括但不限于商品标题、价格、库存、描述、图片、规格参数等,为您的商品展示、比价、推荐等场景提供有…

C++设计模式-结构型设计模式

写少量的代码来应对未来需求的变化。 单例模式 定义 保证一个类仅有一个实例,并提供一个该实例的全局访问点。——《设计模式》GoF 解决问题 稳定点: 类只有一个实例,提供全局的访问点(抽象) 变化点&#xff1a…

SpringCloud微服务:Eureka 和 Nacos 注册中心

共同点 都支持服务注册和服务拉取都支持服务提供者心跳方式做健康检测 不同点 Nacos 支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时(永久)实例采用主动检测模式Nacos 临时实例心跳不正常会被剔除,非临时实…

【uniapp】H5+、APP模拟浏览器环境内部打开网页

前言 今天将智能体嵌入到我的项目中&#xff0c;当作app应用时&#xff0c;发现我使用的webview组件&#xff0c;无论H5怎么登录都是未登录&#xff0c;而APP却可以&#xff0c;于是进行了测试&#xff0c;发现以下几种情况&#xff1a; 方法<a>标签webviewAPP✅✅网页…

YOLOv5改进之bifpn

目录 一、原理 二、代码 三、在YOLOv5中的应用 一、原理 论文链接:

课题学习(二十三)---三轴MEMS加速度计芯片ADXL372

声明&#xff1a;本人水平有限&#xff0c;博客可能存在部分错误的地方&#xff0c;请广大读者谅解并向本人反馈错误。 一、基础配置 测量范围-200g-200g&#xff0c;分辨率为12位&#xff0c; V s 、 V D D I / O V_s、V_{DDI/O} Vs​、VDDI/O​范围为1.6V-3.5V 1.1 引脚配…

【银角大王——Django课程——用户表的基本操作2】

用户表的基本操作2 编辑用户按钮删除按钮入职日期——不显示时分&#xff0c;只显示年月日——使用DataField函数不使用DateTimeField修改models记得重新执行命令&#xff0c;更新数据库结构修改前修改后 编辑用户按钮 点击编辑&#xff0c;跳转到编辑页面&#xff08;将编辑的…

CrossOver支持的软件多吗 CrossOver支持软件列表 crossover兼容性查询

如果你是一个喜欢在Mac上工作的用户&#xff0c;但又不想放弃一些Windows上的优秀软件&#xff0c;那么可以考虑使用一些兼容工具来运行Windows程序。其中&#xff0c;CrossOver就是一款功能强大且受欢迎的兼容工具。那么&#xff0c;CrossOver到底能支持哪些Windows软件呢&…

JVM笔记2--垃圾收集算法

1、如何确认哪些对象“已死” 在上一篇文章中介绍到Java内存运行时的各个区域。其中程序计数器、虚拟机栈、本地方法栈3个区域随着线程而生&#xff0c;随线程而灭&#xff0c;栈中的栈帧随着方法的进入和退出而有条不紊的执行着入栈和出栈操作。每个栈帧中分配多少内存基本上…

VMvare如何更改虚拟机内共享文件夹的挂载点

更改虚拟机内共享文件夹的路径 进入目录 /etc/init.d ,并找到vmware-tools文件 里面有配置项 vmhgfs_mnt"/mnt/hgfs" 将引号内的内容更改为你需要挂载的路径,重启即可 注意挂载的路径不能是 “/”&#xff0c;必须根目录下的某个文件夹&#xff0c;或者其子文件夹 …

定时器编程前配置和控制LED隔一秒亮灭

1.配置定时器 0 工作模式16位计时 2.给初值&#xff0c;定一个10ms出来 3.开始计时

环形链表的判断方法与原理证明

&#xff08;题目来源&#xff1a;力扣&#xff09; 一.判读一个链表是否是环形链表 题目&#xff1a; 解答&#xff1a; 方法&#xff1a;快慢指针法 内容&#xff1a;分别定义快慢指针&#xff08;fast和slow&#xff09;&#xff0c;快指针一次走两步&#xff0c;慢指…
最新文章