Class加载流程和运行时区域

目录

    • jvm是什么
    • .class加载过程
      • 干预.class
      • .class文件内容
      • 1 加载
      • 2-1 连接:验证(class字节流的校验)
      • 2-2 连接:准备(分配内存,初始化默认值)
      • 2-3 连接:解析
      • 3 class 初始化
        • 什么时候需要对类进行初始化?
        • 附:static执行时机?
      • 4. 类实例化new Object()
        • 附:`Object o = new Object();`的汇编指令
      • 5. 运行时内存(java8为例)
        • 程序计数器(PC)
        • 虚拟机栈(线程stack,计算逻辑)
        • 本地方法栈(native method stack)
        • 堆(heap)(存储功能)
          • Thread Local Allocation Buffer,线程本地分配缓存
        • 方法区->元空间(不属于JVM,使用堆外内存)
          • 元空间
          • 为什么去掉JDK1.7的永久代
      • 对象存储
        • `new Object()`占用多少字节?16字节?
        • 对象头之markword
          • 锁升级过程

  1. 重点掌握.class到运行时的整个流程
  2. 掌握运行时内存分布图
  3. 知道new Object()的对象头信息

后续将分析类加载过程中的常见问题,特别是初始化问题、双亲委派加载问题、运行时各区域的情况。这些都需要专门的例子来详细说明,才能加深理解和记忆。

jvm是什么

JVM组成:

  • 类加载子系统
  • 运行时数据区(方法区,堆,thread stack, native statck, pc registor等)
  • 执行引擎(interpreter, jit compiler, garbage collector; native method interface/Library等)

jvm oracale官方文档

.class加载过程

class文件是一组以8位字节为基础单位的二进制流,任何一个Class文件都对应唯一一个类或接口的定义信息

类加载过程为:

加载(即加载class文件)=> 连接 ( 验证 =》 准备 =》 解析)=> 初始化=> 使用=> 卸载

在这里插入图片描述

干预.class

在这里插入图片描述
类似学习Spring框架的BeanFactoryPostProcessor干预BeanDefinition一样,如上的这些技术可以干预.class。 .class不同显然就能代理出不同的东西出来

.class文件内容

文件包含如下信息:

在这里插入图片描述

1 加载

即由类加载器(ClassLoader)执行,通过一个类的全限定名来获取其定义的二进制字节流(Class字节码),将这个字节流所代表的静态存储结构转化为运行时(Runtime data area)区域的入口,根据字节码在Java中生成一个代表这个类的java.lang.Class对象。

2-1 连接:验证(class字节流的校验)

验证是连接阶段的第一步,这一步主要的目的是确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。验证阶段主要包括四个检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。

2-2 连接:准备(分配内存,初始化默认值)

准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。

public static int value = 12;

变量value在准备阶段过后的初始值为0而不是12,因为这时候尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器<clinit>()方法之中,所以把value赋值为12的动作将在初始化阶段才会被执行。

相对于一些特殊的情况,如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量value就会被初始化为ConstantValue属性所指定的值,例如上面类变量value定义为:

public static final int value = 123;

编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value设置为123。

2-3 连接:解析

解析阶段是虚拟机常量池内的符号引用替换为直接引用的过程。

解析后的信息存储在ConstantPoolCache类实例中,如

  • 类或接口的解析
  • 字段解析
  • 方法解析
  • 接口方法解析

3 class 初始化

初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。到了初始阶段,才开始真正执行类中定义的Java程序代码。

初始化阶段是执行类构造器<clinit>方法的过程(注意<clinit>不是类的构造函数)。

clinit方法是由编译器自动收集类中的类静态变量的赋值操作和静态语句块中的语句合并而成的。JVM会保证clinit方法执行之前,父类的clinit方法已经执行完毕。

什么时候需要对类进行初始化?
  1. 使用new该类实例化对象的时候
  2. 读取或设置类静态字段的时候(但被final修饰的字段,在编译时就被放入常量池;连接-准备阶段会赋予变量常量值;所以(static final)的静态字段除外)
  3. 调用类的静态方法的时候
  4. 使用反射Class.forName("xxx")对类进行反射调用的时候,该类需要初始化;
  5. 初始化一个类的时候,有父类,先初始化父类(注:1. 接口除外,父接口在调用的时候才会被初始化;2.子类引用父类静态字段,只会引发父类初始化);
  6. 被标明为启动类的类(即包含main()方法的类)要初始化;
  7. 当使用JDK1.7的动态语言支持时,如果一个java.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

以上情况称为对一个类进行主动引用,且有且只要以上几种情况是需要对类进行初始化:

  • 所有类变量静态初始化语句和静态代码块都会在编译时被前端编译器放在收集器里头,存放到一个特殊的方法中,这个方法就是<clinit>方法,即类/接口初始化方法,该方法只能在类加载的过程中由JVM调用;

  • 编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量;

  • 如果超类还没有被初始化,那么优先对超类初始化,但在<clinit>方法内部不会显示调用超类的<clinit>方法,由JVM负责保证一个类的<clinit>方法执行之前,它的超类<clinit>方法已经被执行。

  • JVM必须确保一个类在初始化的过程中,如果是多线程需要同时初始化它,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其它线程。(所以可以利用静态内部类实现线程安全的单例模式)

  • 如果一个类没有声明任何的类变量,也没有静态代码块,那么可以没有类<clinit>方法;

附:static执行时机?

static块的执行发生在"初始化"的阶段(注意不是类实例初始化过程)。类初始化阶段,jvm会完成对静态变量的初始化,静态块执行等工作。

是否执行static块的几种情况:

  • 第一次new A()会;因为这个过程包括了初始化

  • 第一次Class.forName("A")会;因为这个过程相当于Class.forName("A",true,this.getClass().getClassLoader());

@CallerSensitive
public static Class<?> forName(String className)throws ClassNotFoundException {Class<?> caller = Reflection.getCallerClass();return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
  • 第一次Class.forName("A",false,this.getClass().getClassLoader())不会。因为false指明了装载类的过程中,不进行类初始化;没有类初始化,则不会执行static块。

  • 类似getSystemClassLoader().loadClass("com.other.Hello");也不会。

/*** Loads the class with the specified <a href="#name">binary name</a>.* This method searches for classes in the same manner as the {@link* #loadClass(String, boolean)} method.  It is invoked by the Java virtual* machine to resolve class references.  Invoking this method is equivalent* to invoking {@link #loadClass(String, boolean) <tt>loadClass(name,* false)</tt>}.** @param  name*         The <a href="#name">binary name</a> of the class** @return  The resulting <tt>Class</tt> object** @throws  ClassNotFoundException*          If the class was not found*/
public Class<?> loadClass(String name) throws ClassNotFoundException {return loadClass(name, false);
}

4. 类实例化new Object()

当用new XXX()创建对象时,首先在堆上为对象分配足够的存储空间

这块存储空间会被清零,这就自动地将对象中的所有基本类型数据都设置成了缺省值(对数字来说就是0,对布尔型和字符型也相同),而引用则被设置成了null。

执行所有出现于字段定义处的初始化动作(非静态对象的初始化

然后执行构造器

附:Object o = new Object();的汇编指令

Object o = new Object();的汇编指令

0 new #2 <java/lang/Object>
3 dup
4 invokespecial #1 <java/lang/Object.<init>>
7 astore_1
8 return

隐含一个对象创建的过程:(记住3步,不是原子操作)

  1. 堆内存中申请了一块内存(new指令)【半初始化状态,成员变量初始化为默认值】
  2. 这块内存的构造方法执行(invokespecial指令)
  3. 栈中变量建立连接到这块内存(astore_1指令)

5. 运行时内存(java8为例)

在这里插入图片描述

程序计数器(PC)
  • 指向下一条需要执行的字节码;记录当前线程的位置便于线程切换与恢复;
  • 唯一 一个不会出现 OOM 的区域
虚拟机栈(线程stack,计算逻辑)

描述了Java方法执行的内存模型,创建栈帧,保存该本地方法的局部变量表、操作数栈、动态链接、出口信息。

  1. Java虚拟机栈是线程私有的,它的生命周期与线程相同(随线程而生,随线程而灭)
  2. 栈帧包括局部变量表、操作数栈、动态链接、方法返回地址和一些附加信息
  3. 每一个方法被调用直至执行完毕的过程,就对应这一个栈帧在虚拟机栈中从入栈到出栈的过程

在这里插入图片描述

本地方法栈(native method stack)

描述native方法执行,会创建栈帧(本地方法栈):也保存了该本地方法的局部变量表、操作数栈、动态链接、出口信息。

native会调用本地方法库的本地方法接口(JNI:Java Native Interface)

能允许JAVA程序调用C/C++写的程序,扩展性功能

堆(heap)(存储功能)

主要用于存放对象;Java8之前有【方法区】的大部分被移到堆中了,所以,堆中还放有:运行时常量池,字符串常量池

Thread Local Allocation Buffer,线程本地分配缓存

JVM在内存新生代Eden Space中开辟了一小块线程私有的区域TLAB(Thread-local allocation buffer),TLAB也仅作用于新生代的Eden Space。在Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享也适合被快速GC,所以对于小对象通常JVM会优先分配在TLAB上,并且TLAB上的分配由于是线程私有,所以没有锁开销。也就是说,Java中每个线程都会有自己的缓冲区称作TLAB,在对象分配的时候不用锁住整个堆,而只需要在自己的缓冲区分配即可。

方法区->元空间(不属于JVM,使用堆外内存)

静态变量,常量,类信息(构造方法,接口定义),运行时的常量池存在方法区中,但是实例变量存在堆内存中

  1. 方法区主要存放的是 Class,而堆中主要存放的是实例化的对象

  2. 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域

  3. 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。

  4. 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。

  5. 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:

java.lang.OutofMemoryError:PermGen space(JDK7及之前)或者java.lang.OutOfMemoryError:Metaspace(JDK8及之后)

元空间

类的元数据:如方法、字段、类、包的描述信息,这些信息可以用于创建文档、跟踪代码中的依赖性、执行编译时检查

线程栈中要new对象,从元空间能获取到class信息,然后在堆中分配内存,并与栈中变量建立引用关系

Metaspace由两大部分组成:Klass Metaspace和NoKlass Metaspace。

  1. klass Metaspace就是用来存klass的,就是class文件在jvm里的运行时数据结构,是一块连续的内存区域,紧接着Heap
  2. NoKlass Metaspace专门来存klass相关的其它的内容,比如method,constantPool等,可以由多块不连续的内存组成
为什么去掉JDK1.7的永久代

永久代是方法区的实现,使用堆内存,不好分配大小。JVAVA 8开始,使用元空间取代了永久代。JDK 1.8后,元空间存放在堆外内存中(因此,默认情况下,元空间的大小仅受本地内存限制)。

gc问题,OOM,应用越来越大(调优不友好)

  • JVM加载的Class的总数,方法的大小等都很难确定,因此对永久代大小的指定难以确定。太小的永久代容易导致永久代内存溢出,太大的永久代则容易导致虚拟机内存紧张。
  • ASM,Cglib动态生成,也导致永久代大小的指定难以确定

对象存储

  • 对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

在这里插入图片描述

hotspot 相关术语表

术语英文说明中文解释
mark wordThe first word of every object header. Usually a set of bitfields including synchronization state and identity hash code. May also be a pointer (with characteristic low bit encoding) to synchronization related information. During GC, may contain GC state bits.用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等
klass pointerThe second word of every object header. Points to another object (a metaobject) which describes the layout and behavior of the original object. For Java objects, the “klass” contains a C++ style “vtable”.是对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说查找对象的元数据信息并不一定要经过对象本身。
new Object()占用多少字节?16字节?
java -XX:+PrintCommandLineFlags -version-XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
java version "1.8.0_171"
Java(TM) SE Runtime Environment (build 1.8.0_171-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.171-b11, mixed mode)
  • +UseCompressedClassPointers:64bit机器,一个指针8个字节,如果使用压缩会只有4个字节
  • -XX:+UseCompressedOops:普通对象指针,如果压缩也是4个字节
public class Main {public static void main(String[] args) throws Exception {Object o = new Object();System.out.println(ClassLayout.parseInstance(o).toPrintable());}
}
/*
java.lang.Object object internals:OFFSET  SIZE   TYPE DESCRIPTION                               VALUE0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes totalmarkword:8个字节
_kclass:4个字节
没有成员变量:instance data:0字节
紧接着4个字节,是对齐要使用4个字节,即凑成8个字节即共16个字节
*/
对象头之markword

markword共8个字节,64bit,包括:锁信息gc信息identity hashcode

在这里插入图片描述

  • 无锁例子
public class Main {public static void main(String[] args) throws Exception {Object o = new Object();int hashCode = o.hashCode();int b = hashCode % 2;System.out.println(hashCode + " " + Integer.toBinaryString(hashCode) + " " + b);System.out.println(ClassLayout.parseInstance(o).toPrintable());}
}
/*2007328737 1110111101001010110011111100001 1
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
java.lang.Object object internals:OFFSET  SIZE   TYPE DESCRIPTION                               VALUE0     4        (object header)                           01 e1 67 a5 (00000001 11100001 01100111 10100101) (-1519918847)4     4        (object header)                           77 00 00 00 (01110111 00000000 00000000 00000000) (119)8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes totalmarkword 64bit,如下00000000 000000 00000000 01110111 10100101 01100111 11100001 00000001根据无锁(new),64bit 具体如下unused:25bit         |     identity hashcode:31bit        |unused | age  | biased_lock  | lock
00000000 000000 00000000 0 | 1110111 10100101 01100111 11100001 | 0     | 0000 |     0        |   01|                                    |       |      |              |
*/
锁升级过程

无锁态(new) =》 偏向锁 =》 轻量级锁,自旋锁,无锁 =》 重量级锁

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

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

相关文章

ESP32开发学习记录---》GPIO

she 2025年2月5日&#xff0c;新年后决定开始充电提升自己&#xff0c;故作此记,以前没有使用过IDF开发ESP32因此新年学习一下ESP32。 ESPIDF开发环境配置网上已经有很多的资料了&#xff0c;我就不再赘述&#xff0c;我这里只是对我的学习经历的一些记录。 首先学习一个…

pycharm集成通义灵码应用

在pycharm中安装通义灵码 1、打开files-settings 2、选中plugins-搜索”TONGYI Lingma“&#xff0c;点击安装 3.安装完成后在pycharm的右侧就有通义灵码的标签 4、登录账号 5、查看代码区域代码&#xff0c;每一个方法前面都多了通义灵码的标识&#xff0c;可以直接选择…

Git--使用教程

Git的框架讲解 Git 是一个分布式版本控制系统&#xff0c;其架构设计旨在高效地管理代码版本&#xff0c;支持分布式协作&#xff0c;并确保数据的完整性和安全性。 Git 的核心组件&#xff1a; 工作区&#xff08;Working Directory&#xff09;&#xff1a; 工作区是你在本…

力扣.270. 最接近的二叉搜索树值(中序遍历思想)

文章目录 题目描述思路复杂度Code 题目描述 思路 遍历思想(利用二叉树的中序遍历) 本题的难点在于可能存在多个答案&#xff0c;并且要返回最小的那一个&#xff0c;为了解决这个问题&#xff0c;我门则要利用上二叉搜索树中序遍历为有序序列的特性&#xff0c;具体到代码中&a…

7个国内能打开的AI绘画网站!新手福音!

以下是我收集的国内能打开的AI绘画网站。 1、6pen 网址&#xff1a;https://6pen.art/ 2、文心大模型 网址&#xff1a;https://wenxin.baidu.com/moduleApi/ernieVilg 3、Draft 网址&#xff1a;https://draft.art/ai- art/drawing 4、nightcafe 网址&#xff1a;https:/…

Redis数据库篇 -- Pipeline

一. 什么是Pipeline 在传统的请求-响应模式中&#xff0c;客户端与服务器之间的通信流程如下&#xff1a; 客户端发送一个命令到服务器。服务器接收命令并执行。服务器将执行结果返回给客户端。客户端接收结果后&#xff0c;发送下一个命令 在这种传统的模式下&#xff0c;…

Baumer工业相机堡盟相机的相机传感器芯片清洁指南

Baumer工业相机堡盟相机的相机传感器芯片清洁指南 Baumer工业相机1.Baumer工业相机传感器芯片清洁工具和清洁剂2.Baumer工业相机传感器芯片清洁步骤2.1、准备步骤2.2、清洁过程1.定位清洁工具2.清洁传感器3&#xff0e;使用吹风装置 Baumer工业相机传感器芯片清洁的优势设计与结…

【OS】AUTOSAR架构下的Interrupt详解(下篇)

目录 3.代码分析 3.1中断配置代码 3.2 OS如何找到中断处理函数 3.3 Os_InitialEnableInterruptSources实现 3.4 Os_EnableInterruptSource 3.5 DisableAllInterrupts 3.5.1Os_IntSuspendCat1 3.5.2 Os_InterruptDisableAllEnter 3.5.3 Disable二类中断 3.5.4 Disable一…

ASP.NET Core中间件Markdown转换器

目录 需求 文本编码检测 Markdown→HTML 注意 实现 需求 Markdown是一种文本格式&#xff1b;不被浏览器支持&#xff1b;编写一个在服务器端把Markdown转换为HTML的中间件。我们开发的中间件是构建在ASP.NET Core内置的StaticFiles中间件之上&#xff0c;并且在它之前运…

idea 找不到或者无法加载主类

idea项目&#xff0c;之前一直是正常运行的&#xff0c;放假了之后再回来就遇到启动不了的问题。 WebApplication这个类右键运行的时候&#xff0c;也提示找不到主类。 对于这种之前运行没有问题&#xff0c;突然出问题的项目。 我的点是没有改动代码和数据的情况下项目就跑不起…

DeepSeek R1 Distill Llama 70B(免费版)API使用详解

DeepSeek R1 Distill Llama 70B&#xff08;免费版&#xff09;API使用详解 在人工智能领域&#xff0c;随着技术的不断进步&#xff0c;各种新的模型和应用如雨后春笋般涌现。今天&#xff0c;我们要为大家介绍的是OpenRouter平台上提供的DeepSeek R1 Distill Llama 70B&…

基于SpringBoot养老院平台系统功能实现六

一、前言介绍&#xff1a; 1.1 项目摘要 随着全球人口老龄化的不断加剧&#xff0c;养老服务需求日益增长。特别是在中国&#xff0c;随着经济的快速发展和人民生活水平的提高&#xff0c;老年人口数量不断增加&#xff0c;对养老服务的质量和效率提出了更高的要求。传统的养…

新能源产业的质量革命:六西格玛培训如何重塑制造竞争力

在新能源行业狂飙突进的今天&#xff0c;企业若想在全球供应链中占据高地&#xff0c;仅靠技术突破已远远不够。制造效率的毫厘之差&#xff0c;可能成为市场话语权的千里之距。某光伏巨头曾因电池片良率低于行业均值1.5%&#xff0c;导致年损失超2.3亿元——这恰恰印证了六西格…

额外题目汇总2-链表

链表 1.24. 两两交换链表中的节点 力扣题目链接(opens new window) 给定一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后的链表。 你不能只是单纯的改变节点内部的值&#xff0c;而是需要实际的进行节点交换。 思路 使用虚拟头结点会很方便&#xff…

pytest-xdist 进行多进程并发测试!

在软件开发过程中&#xff0c;测试是确保代码质量和可靠性的关键步骤。随着项目规模的扩大和复杂性的增加&#xff0c;测试用例的执行效率变得尤为重要。为了加速测试过程&#xff0c;特别是对于一些可以并行执行的测试用 例&#xff0c;pytest-xdist 提供了一种强大的工具&…

化学-基础知识一

文章目录 1、物质分类2、离子反应3、氧化还原反应4、物质的量5、电子排布式6、元素周期表 化学基础知识&#xff0c;物质分类、离子反应、氧化还原反应、物质的量、电子排布式、元素周期表 1、物质分类 物质广泛分为混合物和纯净物&#xff0c;纯净物是主要研究对象&#xff1b…

Pycharm调试Deepseek API

本文主要是使用pycharm工具测试调用DeepSeek API 1、deepseek官网注册账号 DeepSeek 2、创建API key&#xff08;注意&#xff1a;复制保存好API key&#xff0c;因为出于安全原因&#xff0c;你将无法通过 API keys 管理界面再次查看它&#xff09; 3、pycharm创建新项目和c…

Java使用aspose实现pdf转word

Java使用aspose实现pdf转word 一、下载aspose-pdf-21.6.jar包【下载地址】&#xff0c;存放目录结构如图&#xff1b;配置pom.xml。 <!--pdf to word--> <dependency><groupId>com.aspose</groupId><artifactId>aspose-pdf</artifactId>…

检索式知识库问答相关研究调研

基于信息检索的知识库问答存在以下问题 一、问题解析阶段 复杂问题解析 1.问题中包括多个实体&#xff1a;(i)使用卷积操作捕获每个词的上下文特征&#xff1b;(ii)使用大语言模型对问题进行凝练&#xff0c;保留关键信息&#xff1b;(iii)采用思维链的方式对问题进行分解&am…

Python基于Django的课堂投票系统的设计与实现【附源码】

博主介绍&#xff1a;✌Java老徐、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&…