【Java八股文面试系列】JVM-内存区域

news2024/11/22 16:30:50

目录

Java内存区域

运行时数据区域

线程独享区域

程序计数器

Java 虚拟机栈

StackFlowError&OOM

本地方法栈

线程共享区域

GCR-分代回收算法

字符串常量池

方法区

运行时常量池

HotSpot 虚拟机对象探秘

对象的创建

对象的内存布局

句柄


Java内存区域

运行时数据区域

Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。

JDK 1.8 和之前的版本略有不同,我们这里以 JDK 1.7 和 JDK 1.8 这两个版本为例介绍。

JDK1.7运行时数据区域:

image-20240130135118481

JDK1.8运行时数据区域:

image-20240130135235723

Java的运行时区域可以分成两个大的部分,一个是所有线程共享的区域,一个是当前线程独享的区域。

线程独享的区域:

生命周期与线程相同,随着线程创建而创建,随着线程死亡而死亡

  • 程序计数器(相当于一个程序运行的光标,标记了程序的运行位置)

  • 虚拟机栈(使用了栈的存储结构,将方法的调用顺序进行记录,同时记录了方法的一些信息)

  • 本地方法栈

线程共享区域:

  • 堆(保存数组和对象的位置,但是数组其实也是对象)

  • 方法区

  • 直接内存(除了JVM定义的内存之外,运行的主机的运行内存上申请的内存)

Java 虚拟机规范对于运行时数据区域的规定是相当宽松的。以堆为例:堆可以是连续空间,也可以不连续。堆的大小可以固定,也可以在运行时按需扩展 。虚拟机实现者可以使用任何垃圾回收算法管理堆,甚至完全不进行垃圾收集也是可以的

线程独享区域

程序计数器

是什么?

是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。

有什么用?

字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。

在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

Java 虚拟机栈

是什么?

栈除了一些 Native 方法调用是通过本地方法栈实现的(后面会提到),其他所有的 Java 方法调用都是通过栈来实现的(也需要和其他运行时数据区域比如程序计数器配合)。

Native方法指的是指使用本地代码实现的方法。这些方法是在Java代码中声明的,但实际的实现代码是由非Java语言(如C、C++或其他语言)编写的,并且通常在Java虚拟机(JVM)外部运行。

使用native方法可以提供更高的性能和更低级别的控制,因为本地代码可以直接与操作系统和硬件交互。然而,这也增加了代码的复杂性和维护难度,因为本地代码需要单独编译和测试。

方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。

栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。和数据结构上的栈类似,两者都是先进后出的数据结构,只支持出栈和入栈两种操作。

image-20240130141256561

局部变量表:主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。

操作数栈:主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。

动态链接主要服务一个方法需要调用其他方法的场景。Class 文件的常量池里保存有大量的符号引用比如方法引用的符号引用。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用,这个过程也被称为 动态连接

public class Main {
    public static void main(String[] args) {
        int i = 10;
        int a = i++;
        int b = ++i;
    }
    public void a() {
        System.out.println("aa");
        b();
    }
    public void b() {
        System.out.println("bbb");
    }
}

通过反编译可以看到方法在常量池都会有一个符号引用

image-20240130142048572

方法A中调用了B方法,所以方法A中会有方法B的应用,并且这里的引用#5都是和前面的常量池能够对上的

image-20240130142248290

StackFlowError&OOM

栈空间虽然不是无限的,但一般正常调用的情况下是不会出现问题的。不过,如果函数调用陷入无限循环的话,就会导致栈中被压入太多栈帧而占用太多空间,导致栈空间过深。那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。

Java 方法有两种返回方式,一种是 return 语句正常返回,一种是抛出异常。不管哪种返回方式,都会导致栈帧被弹出。也就是说, 栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。

除了 StackOverFlowError 错误之外,栈还可能会出现OutOfMemoryError错误,这是因为如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

简单总结一下程序运行中栈可能会出现两种错误:

  • StackOverFlowError 若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。

  • OutOfMemoryError 如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

img

本地方法栈

和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。

方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowErrorOutOfMemoryError 两种错误。

线程共享区域

Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代;再细致一点有:Eden、Survivor、Old 等空间(空间的大小比为8:1:1)。进一步划分的目的是更好地回收内存,或者更快地分配内存。

image-20240130142954881

在 JDK 7 版本及 JDK 7 版本之前,堆内存被通常分为下面三部分:

  1. 新生代内存(Young Generation)

  2. 老生代(Old Generation)

  3. 永久代(Permanent Generation)

JDK 8 版本之后 PermGen(永久代) 已被 Metaspace(元空间) 取代,元空间使用的是本地内存。 (我会在方法区这部分内容详细介绍到)。

GCR-分代回收算法

上面的分代,也是涉及到了垃圾回收算法中的分代回收算法。

首先,所有新创建的对象,在一开始都会进入到新生代的Eden区(如果是大对象会被直接丢进老年代),在进行新生代区域的垃圾回收时,首先会对所有新生代区域的对象进行扫描,并回收那些不再使用对象:

image-20240129140902070

接着,在一次垃圾回收之后,Eden区域没有被回收的对象,会进入到Survivor区。在一开始From和To都是空的,而GC之后,所有Eden区域存活的对象都会直接被放入到From区,最后From和To会发生一次交换,也就是说目前存放我们对象的From区,变为To区,而To区变为From区:

image-20240129141220032

接着就是下一次垃圾回收了,操作与上面是一样的,不过这时由于我们To区域中已经存在对象了,所以,在Eden区的存活对象复制到From区之后,所有To区域中的对象会进行年龄判定(每经历一轮GC年龄+1,如果对象的年龄大于默认值为15,那么会直接进入到老年代,否则移动到From区)

image-20240129141234946

最后像上面一样交换To区和From区,之后不断重复以上步骤。

字符串常量池

字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。字符串常量池从1.7开始就是从方法去移动到了堆中,并且静态变量都保存在里面

为什么移到堆中?

在JDK 1.7之前,字符串常量池存放在方法区中,字符串常量在编译时分配在常量池中。然而,这种方法存在一些限制,例如方法区的大小是固定的,如果方法区溢出,会导致OutOfMemoryError错误。此外,由于方法区的内存分配和回收相对较慢,只有FULL GC才会垃圾回收,因此可能会影响程序的性能。

HotSpot 虚拟机中字符串常量池的实现是 src/hotspot/share/classfile/stringTable.cpp ,StringTable 可以简单理解为一个固定大小的HashTable ,容量为 StringTableSize(可以通过 -XX:StringTableSize 参数来设置),保存的是字符串(key)和 字符串对象的引用(value)的映射关系,字符串对象的引用指向堆中的字符串对象。

方法区

当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据

方法区和永久代以及元空间是什么关系呢?

方法区和永久代以及元空间的关系很像 Java 中接口和类的关系,类实现了接口,这里的类就可以看作是永久代和元空间,接口可以看作是方法区,也就是说永久代以及元空间是 HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式。并且,永久代是 JDK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现变成了元空间。

为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?

1、整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是本地内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。

当元空间溢出时会得到如下错误:java.lang.OutOfMemoryError: MetaSpace

你可以使用 -XX:MaxMetaspaceSize 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。-XX:MetaspaceSize 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。

2、元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 MaxPermSize 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。

3、在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了。

方法区常用参数有哪些?

JDK 1.8 之前永久代还没被彻底移除的时候通常通过下面这些参数来调节方法区大小。

-XX:PermSize=N //方法区 (永久代) 初始大小
-XX:MaxPermSize=N //方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen

相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了。

JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是本地内存。下面是一些常用参数:

-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小

与永久代很大的不同就是,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。

运行时常量池

运行时常量池的功能类似于传统编程语言的符号表,尽管它包含了比典型符号表更广泛的数据。

既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 错误

HotSpot 虚拟机对象探秘

HotSpot虚拟机是Java虚拟机的一种实现,它是Sun Microsystems公司开发的一款高性能、动态类型的计算机编程语言虚拟机。HotSpot虚拟机采用了即时编译(JIT)技术,可以将Java字节码转换为本地机器代码,从而提高程序的执行效率。

对象的创建

Java 对象的创建过程——五步

  1. 检查类是否已经被加载

    new关键字时创建对象时,首先会去运行时常量池中查找该引用所指向的类有没有被虚拟机加载,如果没有被加载,那么会进行类的加载过程。(加载将class文件读取到内存中,使用加载器进行加载)

  2. 为对象分配内存空间

    当类加载检查通过后,虚拟机会为新生对象分配内存空间,对象所需内存空间的大小在类加载完成后就已经确定了。为新生对象分配内存空间其实就是在Java堆中划分出一块确定大小的内存分配给新生对象。分配内存的方式有“指针碰撞”和“空闲列表”两种,选择哪种分配方式取决于Java堆内存是否规整。

    内存分配的两种方式

    • 指针碰撞 使用场合:堆内存规整(即没有内存碎片)的情况下。 实现原理:将用过的内存都整合到一边,没有用过的内存放到另一边,中间有一个分界指针,当需要为新对象分配内存空间时,只需要将分界指针向没有用过的内存一侧移动对象内存大小位置即可。

    • 空闲列表 使用场合:堆内存不规整的情况下。(JDK8默认的GR垃圾回收方式,是不规整的方式) 实现原理:虚拟机会维护一个列表,该列表记录了哪些内存是可用的,当需要为新对象分配内存空间时,只需要在列表中找一块足够大小的内存分配给对象实例,然后更新列表记录。 选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器垃圾采用的垃圾收集算法

  3. 对象初始化零值

    内存分配完成后,虚拟机需要将新分配的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段可以在Java代码中可以不赋初始值就直接使用,程序能够访问这些实例字段的数据类型所对应的零值。

  4. 设置对象头

    初始化零值之后,虚拟机需要对对象头进行必要的设置,例如这个对象是哪个类的实例,如何才能找到这个类的元数据信息,对象的哈希码、GC 分代年龄、锁标志位、偏向锁标志位、线程持有的锁信息、对象是否可用等信息会存放到对象头中。另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

  5. 执行init方法

    执行完new指令后会接着执行init方法,将对象按照程序员的需求来进行初始化,这样一个真正可用的对象才算完全产生出来。

上述为无父类的对象创建过程。对于有父类的对象创建过程,还需满足如下条件:

  1. 先加载父类;再加载本类;

  2. 先执行父类的实例的初始化方法init(成员变量、构造代码块),父类的构造方法;执行本类的实例的初始化方法init(成员变量、构造代码块),本类的构造方法。

对象的内存布局

在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:对象头实例数据对齐填充

image-20240122173034582

对象头:虚拟机的对象头包括两部分信息第一部分用于存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

实例数据:实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。

对齐填充:仅仅起占位作用,方便进行读取和运算

句柄

如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息。

对象的访问定位-使用句柄

对象的访问定位-使用句柄

如果使用直接指针访问,reference 中存储的直接就是对象的地址。

对象的访问定位-直接指针

对象的访问定位-直接指针

这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。

参考文章

JavaGuide-JVM内存区域 

青空霞光-Jvm内存管理

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

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

相关文章

BUG:docker启动之后直接退出问题

示例如下: 问题排查: 启动命令 sudo docker run --privilegedtrue --runtimenvidia --shm-size80g -v /mmm_data_center:/mmm_data_center -v /imagecenter_new/:/imagecenter_new -v /data1:/data1 -v /mnt/offline_data/:/mnt/offline_data/ --neth…

蓝桥杯---生日蜡烛

某君从某年开始每年都举办一次生日party,并且每次都要吹熄与年龄相同根数的蜡烛,现在算起来,他一共吹熄了236根蜡烛。请问,他从多少岁开始过生日party的? 请填写他开始过生日 party的年龄数。 注意:你提交的应该是一个整数,不要…

记elasticsearch CPU负载100%问题

记elasticsearch CPU负载100%问题 环境:问题表现:初步排查:日志查询hot_thread 深入查询当前elasticsearch正在运行的Task查看Task详情解决问题对导致问题的原因的几个猜测问题复现:导致问题的原因。json导入规则问题json导入规则…

ElementUI Form:Form表单

ElementUI安装与使用指南 Form表单 点击下载learnelementuispringboot项目源码 效果图 el-form.vue&#xff08;Form表单&#xff09;页面效果图 项目里 el-form.vue代码 <script> export default {name: el_form,data() {var checkAge (rule, value, callback…

2月4号作业

编写程序实现二叉树的创建&#xff0c;三种遍历自己销毁 #include <myhead.h>#define TRUE 1 #define FALSE 0 #define OVERFLOW -2 #define OK 1 #define ERROR 0#define INIT_SIZE 20 #define INCREMENT_SIZE 5typedef int Status; typedef int TElemType; //存储结构…

Linux权限【超详细】

&#x1f4d9; 作者简介 &#xff1a;RO-BERRY &#x1f4d7; 学习方向&#xff1a;致力于C、C、数据结构、TCP/IP、数据库等等一系列知识 &#x1f4d2; 日后方向 : 偏向于CPP开发以及大数据方向&#xff0c;欢迎各位关注&#xff0c;谢谢各位的支持 目录 扩展知识&#xff1a…

Linux项目自动化构建工具之make/Makefile演示gcc编译

文章目录 一、背景二、如何使用&#xff1f;三、原理四、关于make的问题五、再次理解/编写makefile依赖关系依赖方法 六、原理讲解项目清理makefile是支持变量的取消执行make后显示命令依赖方法可以多行 一、背景 会不会写makefile&#xff0c;从一个侧面说明了一个人是否具备…

C语言进阶之文件操作

一、什么是文件 磁盘上的文件是文件。 但是在程序设计中&#xff0c;我们一般谈的文件有两种&#xff1a;程序文件、数据文件&#xff08;从文件功能的角度来分类的&#xff09;。 1&#xff09;程序文件 包括源程序文件&#xff08;后缀为.c&#xff09;,目标文件&#xff…

VueFire:一个一流的 Vue 和 Firebase 体验,包括对 Nuxt 的支持,现在已经稳定了

VueFire&#xff0c;一个一流的 Vue 和 Firebase 体验 — 包括对 Nuxt 的支持&#xff0c;现在已经稳定了。 Vue 和 Firebase 现在比以往任何时候都更好了。 构建更好的VueFire 去年&#xff0c;我们宣布与 Eduardo San Martin Morote 合作&#xff0c;构建一个成熟的 Vue 和…

《数字乡村标准化白皮书(2024)》正式发布 智汇云舟参编

近日&#xff0c;全国信标委数字乡村标准研究组2023年度全体会议在北京召开。会议期间&#xff0c;智汇云舟参编的《数字乡村标准化白皮书&#xff08;2024&#xff09;》&#xff08;简称《白皮书》&#xff09;正式发布&#xff01; 发布仪式 白皮书研究背景 作为国家基础制…

交换机ARP学习异常,看网工大佬是如何处理的?

晚上好&#xff0c;我的网工朋友。 在复杂多变的网络环境中&#xff0c;网工就没有不遇到问题的&#xff0c;习惯了&#xff0c;兵来将挡水来土掩。 那ARP异常你遇到过吗&#xff1f;ARP作为网络中的基础协议之一&#xff0c;它的稳定性对整个网络的性能和可靠性至关重要。 …

【MongoDB】跨库跨表查询(python版)

MongoDB跨表跨库查询 1.数据准备&#xff1a;2.跨集合查询3.跨库查询应该怎么做&#xff1f; 讲一个简单的例子&#xff0c;python连接mongodb做跨表跨库查询的正确姿势 1.数据准备&#xff1a; use order_db; db.createCollection("orders"); db.orders.insertMan…

电路分析2

7 等效电路的思想&#xff0c;简化不是目的&#xff0c;这个电路说明一切&#xff01;_哔哩哔哩_bilibili 高中知识&#xff0c;翻笔记 8 什么时候用Y型&#xff1f;看到有有相同数字的时候&#xff0c;就可以考虑用 9 10 输入电阻还有没学完的 ok 11

海外多语言盲盒开发:打破语言障碍,连接全球消费者

随着全球化的加速和互联网的普及&#xff0c;语言障碍成为了影响跨国交流和商业活动的重要因素。为了满足跨国市场的需求&#xff0c;海外多语言盲盒开发成为了一个新兴的领域。本文将探讨海外多语言盲盒开发的意义、现状和未来发展。 一、海外多语言盲盒开发的意义 在全球化…

【C++】运算符重载详解

&#x1f497;个人主页&#x1f497; ⭐个人专栏——C学习⭐ &#x1f4ab;点击关注&#x1f929;一起学习C语言&#x1f4af;&#x1f4ab; 目录 导读 1. 为什么需要运算符重载 2. 运算符重载概念 3. 运算符重载示例 3.1 运算符重载 3.2 >或<运算符 4. 运算符重…

提前祝大家新年好!来看看社区 2023 都得了哪些奖吧

大噶好&#xff01;转眼马上就是“龙”历新年啦&#xff0c;不知道大家这周的工作热情怎么样呢&#xff1f;小陈的心已经在殷切期盼回家过年了&#xff5e; RTE 开发者社区预祝诸位&#xff1a; 2024 年 &#x1f432;龙年添财气&#xff0c;万事皆胜意&#xff01; 回顾过去…

c语言封装继承详解

模块化编程结构 函数头文件 c语言头文件代码 #ifndef __Object_H_ #define __Object_H_// 继承 struct person {int id;char name[20];int gender;const char* (*getGender)(struct person* s);void (*setGender)(struct person* s, const char* strGender); }; struct teache…

C语言问题汇总

指针 #include <stdio.h>int main(void){int a[4] {1,2,3,4};int *p &a1;int *p1 a1;printf("%#x,%#x",p[-1],*p1);} 以上代码中存在错误。 int *p &a1; 错误1&#xff1a;取a数组的地址&#xff0c;然后1&#xff0c;即指针跳过int [4]大小的字节…

Flink1.14新版KafkaSource和KafkaSink实践使用(自定义反序列化器、Topic选择器、序列化器、分区器)

前言 在官方文档的描述中&#xff0c;API FlinkKafkaConsumer和FlinkKafkaProducer将在后续版本陆续弃用、移除&#xff0c;所以在未来生产中有版本升级的情况下&#xff0c;新API KafkaSource和KafkaSink还是有必要学会使用的。下面介绍下基于新API的一些自定义类以及主程序的…

【JavaEE进阶】 图书管理系统开发日记——伍

文章目录 &#x1f38b;前言&#x1f332;需求分析&#x1f384;约定前后端交互接口&#x1f333;实现服务器代码&#x1f6a9;控制层&#x1f6a9;业务层&#x1f6a9;数据层 &#x1f343;修改前端代码⭕总结 &#x1f38b;前言 这次我们来实现图书管理系统的增加图书模块。…