JDK-JVM

news2025/2/27 10:23:19

JVM

  • JDK
    • JDK内部体系结构:
    • JVM 与 跨平台
    • JVM在程序运行过程中的运行细节,内存分配 和 流转模型。
      • JVM结构体系
        • 1. 虚拟机栈
        • 2. 线程栈
          • 2.1. 栈帧
          • 2.2. 数据结构栈 与 线程栈 的关系:
          • 2.3.栈帧的内部结构:
          • 2.4 方法中的数据 在栈帧中的流转过程:
        • 3. 程序计数器
          • 3.1 字节码执行引擎与程序计数器和方法区的关系
        • 4. 方法区:
        • 5. 本地方法栈
        • 6. 堆:
          • 6.1 堆的组成
          • 6.2 GC
          • 6.3 GC过程
            • 6.3.1 GC Root
            • 6.3.2 复制算法
            • 6.3.3 可达性分析算法
            • 6.3.4 分代年龄
            • 6.3.5 对象的组成 和 对象头
            • 6.3.6 jvisualvm工具 与 GC过程验证
        • 7. JVM调优:
  • .参考文献

JDK

JDK内部体系结构:

在这里插入图片描述
整个jdk内部体系结构,大体可分为两部分:命令工具JRE

  1. jdk自带了一些命令工具。比如java、javac、javap 和 jar命令等

javac:用于将java源代码编译成字节码文件,也就是 通过Java的编译器(Windows下的javac.exe),对源文件进行错误排查后,形成后缀名为**.class**的字节码文件。

字节码文件:是一种与任何具体的"机器环境" 和 “操作系统环境” 无关的中间代码,同时也是一个二进制文件。

java:用于让JVM执行字节码文件。

  1. JRE(Java运行时环境):包括 类库JVM
    (1) 支撑Java程序运行的核心类库。比如I/O,网络(NetWorking),并发(Concurrency),lang 和 util包等。
    (2) Java虚拟机JVM(Java Virtual Machine)

JVM 与 跨平台

Java最大的特性跨平台,就是由JVM帮我们实现的。

  • JVM 从软件层面 屏蔽了 不同操作系统 在底层 硬件 与 指令 上的区别。也就是,JVM兼容了不同的操作系统,因为我们在下载jdk的时候,下载了不同的基于这个操作系统 特定的jdk版本,它里面就有 针对 特定平台的JVM。
  • 实际上,计算机底层只认识0101的二进制机器码,但是由于不同操作系统 不同硬件平台上 指令集 的区别,同一份代码的功能实现,在不同操作系统底层 对应的二进制机器码 是不一样的,而JVM可以生成 基于这个操作系统 特定的 机器可执行的 二进制机器码。比如,一个加法功能的实现,在Windows上生成的是010101,而在 Linux上生成的 可能是110001…。
    在这里插入图片描述
  • 如果我选择了 基于Windows版本的jdk,那它对应给我们运行代码的JVM,就是基于这个Windows操作系统,做了一些特定的实现。然后,我们写了一份java源代码,比如Helloword.java。然后通过javac指令编译成Helloword.class字节码文件,再通过java指令(java Helloword.class),将字节码文件丢到JVM里去运行。需要注意的是,我们只写了一份代码,生成了一份字节码(没有平台限制),最终由 JVM 生成基于这个操作系统 特定的 可执行的二进制机器码。
  • 因为 Java是先编译成**.class**字节码文件,然后再利用JVM进行解释执行的,是一种在编译基础上进行解释运行的语言。所以,Java即可以说成编译型,也可以说成解释型语言。
  • 简而言之,跨平台就是,代码一次编写,到处运行。开发人员 只需写一份代码,最终可以放到各大平台去运行,比如Windows,Linux,Unix 和 Mac等。

JVM在程序运行过程中的运行细节,内存分配 和 流转模型。

JVM结构体系

  • 一个完整的JVM结构体系 由3部分组成:类装载子系统运行时数据区字节码执行引擎。他们的作用和联系,也就是字节码文件在JVM中的 执行过程 与 运行原理。
  • 当我们用java指令 去运行一个字节码文件时(比如java Math.class),JVM就开始运行了。字节码文件会首先通过JVM的第一块区域 类装载子系统,它会把我们的字节码文件装载(load)到JVM的第二块区域 运行时数据区(也叫内存模型/内存区域),最终,由我们JVM的第三块区域 字节码执行引擎,来执行我们内存里面的一行一行的代码。
  • 在整个JVM的组成中,最为重要的部分,其实就是内存区域,JVM调优实际上调的就是这个区域的相关部分,而其中的堆内存是最为主要的调优部分。
  • 内存区域oracle官方文档的JVM规范 里面,有一些详细的定义和介绍,
  • 内存区域在规范里面的官方名称,叫做运行时数据区(Run-Time Data Areas),它的内部又包含5个部分:
  • 第一部分是程序计数器(The pc Register)
  • 第二部分是Java虚拟机栈(Java Virtual Machine Stacks)
  • 第三部分是堆(heap)
  • 第four部分是方法区(Method Area),方法区中又包括常量池(Run-Time Constant Pool)
  • 第五部分是本地方法栈(Native Method Stacks)
  • 这些部分都属于JVM的内存区域,是用来存储数据的,但它们的作用和存储的内容各不相同。
1. 虚拟机栈
  • 虚拟机栈 这块区域主要是用来 存放 我们的线程运行过程中,它的那些局部变量,比如在我们java代码中,(包括main方法在内)每一个方法,都有属于自己的局部变量。这些变量就是存放在我们的"栈"内存区域。
2. 线程栈
  • 因为 Java虚拟机中的 每个线程 都有一个私有的虚拟机栈,与线程同一时间被创建出来。也就是说,在我们的java代码中,只要有一个线程开始运行代码了,JVM就会马上给这个线程,在整个一大块虚拟机栈的内存区域中,分配一小块属于这个线程 自己独立的内存空间,用来存放 我们这个线程(自己运行过程中)内部 的局部变量。所以对于线程而言,每个线程 自己私有的虚拟机栈 也可以更形象的称之为线程栈
2.1. 栈帧
  • 线程栈 的内部其实还有很复杂的结构,比如说每个线程栈的内部,都是由一系列的方法栈帧组成。那其实在 线程栈的内部,只要这个线程 开始运行了一个方法,那么,马上会给这个方法,在这个线程栈上面,又分配出一块 自己独立的 内存空间,用来存放 方法内部的局部变量,因为 局部变量的作用域 只在当前方法,所以,方法运行结束,这个方法中的局部变量 也肯定会被销毁。如果这个方法内部,又调用了另一个方法,那么,被调用的方法 也会在当前 线程栈 的内存区域中,再分配一块 内存空间,去 存放自己方法内部的局部变量。
  • 那么,线程中的每个方法,在 线程栈的 内存区域中 都会有属于自己方法 所使用的 独立的一块内存空间,用来存放自己方法内部的局部变量。对于方法而言,一个方法 在线程栈中,所对应分配出的 属于自己方法的 一小块独立的 内存空间,就被称之为 线程栈 中的一个 栈帧。也就是说,在我们整个JVM内部,一个方法开始运行,就会有对应的一块栈帧内存空间,分配给这个方法,存放方法自己内部的局部变量。
  • 概括来说,虚拟机栈,线程栈和栈帧之间的关系:虚拟机栈是属于JVM中一整块大的内存区域;线程栈属于线程,是从虚拟机栈中 为当前线程分配出的一小块内存空间;栈帧属于方法,是从方法对应的线程栈中 为当前方法分配出的 更小的一块内存空间。
2.2. 数据结构栈 与 线程栈 的关系:
  • 线程栈 实际上使用的就是 数据结构中的栈存储结构。那么为什么要用数据结构中的栈,来实现线程栈。
  • 因为 栈的特点是FILO(First In Last Out),先进后出(先进去的元素反而后出来)。与 我们程序代码中的方法 嵌套调用 执行的先后顺序,非常吻合。
  • 当我们程序开始执行一个方法的时候,这个方法(暂且称之为主方法) 会在线程栈中分配出对应的一块栈帧内存空间,也就是主方法入栈;
  • 如果主方法在执行过程中,又调用了另一个方法(暂且称之为局部方法),那么JVM也会为这个局部方法 在线程栈中 对应分配出一块独立的栈帧内存空间,也就是局部方法入栈;
  • 当局部方法调用结束,那局部方法内部的局部变量 全部都会消失,也就是局部方法对应的栈帧内存空间会被释放掉,也就是局部方法出栈,回到主方法。
  • 然后,主方法继续执行未完成的代码,当主方法再执行结束,主方法对应的栈帧内存空间也会被销毁,也就是主方法出栈。
  • 也就是,先执行的方法,反而会后结束;后调用的方法,反而会先执行完。这与数据结构栈的先进后出原则,非常吻合。
2.3.栈帧的内部结构:
  • 栈帧是用来存放方法的局部变量的内存区域,但栈帧的作用远不止于此。栈帧的内部结构也比较复杂,但最最主要的,有4块区域:

(1) 首先栈帧内部有一个局部变量表,用来存放方法的局部变量。

  • 如果 局部变量的值 是一个对象,或者,new了一个对象,而new出来的对象,都是存放在我们的内存中的。那么 赋值为对象的局部变量,和堆里面 对应的对象,它们之间是引用关系。
  • 比如方法中有一行代码,Math math = new Math();,局部变量表中的局部变量,实际上就是一块内存空间,这块空间被JVM标识成 名为math的名称,用以知道 这块空间 是给我们math这个局部变量去使用,而这块空间中存放的是 局部变量的值对象 在堆里面的内存地址,从而 JVM 根据这个内存地址,就可以找到 堆里面 对应的对象。而地址,实际上 就是一个引用,一个指针。
  • 那么,栈里面可能有很多很多线程,每一个线程都有一些自己的方法栈帧,而每个栈帧有会很多很多自己的局部变量,这些局部变量,如果它们的值,恰好又是对象类型,那么这些对象实际上,是分配在堆中的,意味着这些局部变量里面,存放的都是堆中对应对象的地址引用,栈 和 堆 之间的关系就出来了。
  • 栈帧中的另外3部分是,操作数栈,动态链接 和 方法出口。

(2) 操作数栈:我们程序在运行过程中,程序代码赋值等号右侧的数值,就是操作数。那操作数栈,就是给我们操作数,做操作运算(加减乘除运算)的时候,临时存放的一块 中转的内存空间。

(3) 动态链接:会把 符号引用,转变为 直接引用。比如,我们多态在程序运行过程中,找到对应的方法,其实就算一种动态链接。实际上,方法是加载到方法中的,方法区也是内存空间,也有内存地址。

  • 当我们的程序 在动态执行到 某个方法时,要调用这个方法,需要知道 这个方法 有哪些代码,这时 就会去解析 这个方法的符号引用,去找 这个符号 对应的 具体的JVM指令码,这些指令码 就存放在 我们的方法区 内存区域中。
  • 动态链接,可以简单的理解为 存放 方法入口的内存地址。或者说,动态链接 会记录 这个方法 指向 在方法区中 具体代码的 内存地址(就是 方法的符号引用)。从而就可以 根据 动态链接中 记录的方法符号引用(方法具体代码的内存地址) 找到 对应的直接引用(在内存里面存放具体代码的位置)。
    (4) 方法出口:当一个方法(主方法),动态执行到 另一个方法时,程序计数器 会返回 那个程序计数器 执行到当前 主方法 哪一行代码的 位置信息,就存放在 方法出口 里,实际上 不止这一点点,但可以 大概这么来认为。从而 根据 方法出口 里放的那些数据,可以知道 被调用方法 调用结束后,应该回到 主方法 的执行哪一行代码 继续往下去执行。
  • 那栈帧的内部这些区域,与java的字节码文件,有着密不可分的联系。一个**.class**字节码文件,实际上是一个二进制编码的文件,它包含了很多底层的程序 JVM运行的一些细节逻辑,那这些底层的逻辑实际上,就是栈帧中各组成部分之间的运行细节。如果我们通过记事本打开一个字节码文件,展现给我们的是一个 以每4位为一组的 有规则的16进制的编码集合,那么每2组编码字符串,在oracle官方网址上面的字节码手册中,都有对应的含义可以查询。但是以这样的方式 分析字节码文件,体验还是比较差的。oracle官方还提供了 另外一种更友好的字节码文件形式。可以通过jdk自带的javap命令对二进制的字节码文件,进行反汇编,得到一份可阅读的更加友好的字节码文件形式。但它们的本质上都是同一个东西,都是我们java代码更底层的一些JVM指令码。
  • 我们可以利用IDEA开发工具,在target/classes包下找到自己想分析的**.class**字节码文件,然后鼠标右键选择"Open in Terminal",打开一个命令窗口,输入javap回车,即可看到javap指令的相关介绍和使用。其中有一个"-c"的参数,对代码进行反汇编,可以帮我们把二进制的字节码文件,反汇编成JVM的指令码文件,或者叫JVM的汇编指令。比如我们对某个Xxx.class字节码文件进行反汇编,并把它输出打印到一个记事本文件中,如下指令:javap -c Xxx.class > Xxx.txt。那么在 .class字节码 的同级目录下,就会生成一个记事本形式的对应的JVM指令码文件。
2.4 方法中的数据 在栈帧中的流转过程:
  • 比如我们分析的java代码中有一个compute方法,方法中的数据 在栈帧中的流转过程:
public int compute() {
	int a = 1;
	int b = 2;
	int c = (a + b) * 10;
	return c;
}
  • 那么这段代码对应的JVM指令码(JVM执行的代码)如下:
public int compute();
	Code:
	   0: iconst_1
	   1: istore_1
	   2: iconst_2
	   3: istore_2
	   4: iload_1
	   5: iload_2
	   6: iadd
	   7: bipush		10
	   9: imul
	  10: istore_3
	  11: iload_3
	  12: ireturn

首先我们可以发现,越底层的代码,它对应的行数越多;越高级的语言,它的代码量越少。那么对于JVM指令码中的每一行到底是什么以上,在oracle官方网址上面有一些手册,比如JVM指令手册(再复杂的java代码,要分析它底层的一些运作机制,以及它底层的一个内存流转 及分配模型,只要根据JVM指令手册,都可分析出来),可以去查询。那么这段JVM指令码,根据JVM指令手册,每一条指令对应的解释如下:

iconst_1 将int类型常量1压入 操作数栈
istore_1 将int类型值存入局部变量1
iconst_2 将int类型常量2压入栈
istore_2 将int类型值存入局部变量2
iload_1 从局部变量1中装载int类型值
iload_2 从局部变量2中装载int类型值
iadd 执行int类型的加法
bipush 将一个8位带符号整数压入栈
imul 执行int类型的乘法
istore_3 将int类型值存入局部变量3
iload_3 从局部变量3中装载int类型值
ireturn 从方法中返回int类型的数据

  • 那么,我们逐条来解释一下compute方法的JVM指令码。

(1) compute方法的第一行java代码int a = 1;它对应的JVM指令码及含义:

  • iconst_1 将int类型常量1压入操作数栈:这里的常量1,指的是操作数栈中的 第一个存储位置,而不是java代码中的1这个数值。更为精确的表达:把java代码中1这个数值,压入我们在操作数栈中 分配出的第一个常量内存空间。
  • istore_1 将int类型值存入局部变量1:这里的局部变量1,数字1指的不是第一个局部变量,它是我们局部变量表的一个索引。这个局部变量表的索引不是从1开始的,是从0开始的。局部变量表,它还有0的一个位置istore_0,放的是this这个变量,也就是,调用这个方法的对象。在JVM指令手册中,还有 istore_2 和 istore_3。我们可以把局部变量表理解为,类似一个数组,这些数字,就是数组的下标和索引。实际上,我们的一个成员方法对应的栈帧的局部变量表中,局部变量0都是定死了的,都是this,也就是调用这个方法的对象。
  • 那么局部变量1,也就是 JVM在局部变量表中 分配了一块内存空间 叫局部变量1,并把这块内存空间标识为 我们java代码中的局部变量a。实际上当istore_1这条指令执行的时候,JVM底层会干这几件事;JVM首先 会在 局部变量表,给我们的局部变量1分配出一块内存空间,作为局部变量a 的存储位置;然后,JVM会把 刚刚压入 操作栈中的 常量数值1 出栈,放到局部变量表中a对应的这一块内存空间里。这时,局部变量表中,局部变量a这块内存空间,实际上就已经有一个值了,就是我们的int类型的常量数值1。
  • 当iconst_1和istore_1这两行代码执行完后,局部变量表中标识为a内存空间,就已经被赋值为1了。也就是变量a的值就变成1了。

(2) compute方法的第二行java代码int b = 2;它对应的JVM指令码及含义:

  • iconst_2 将int类型常量2压入栈:这里的常量2,同样的,是指操作数栈中的第二个常量的存储位置,也就是,把java代码中2这个数值,压入我们在操作数栈中分配出的第二个常量内存空间。
  • istore_2 将int类型值存入局部变量2:也就是,在局部变量表中,局部变量2的存储位置,标识为我们java代码中的局部变量b,并将刚刚压入操作数栈中的数值2,放到局部变量表b对应的这一块内存空间里。
  • 与iconst_1 和 istore_1一样,当iconst_2 和 istore_2执行完之后,同样的,局部变量表中标识为b内存空间,就存入2这个数值。变量b的值就变成2了。

(3) compute方法的第三行java代码int c = (a + b) * 10;,它对应的JVM指令码及含义:

  • iload_1 从局部变量1中装载int类型值:局部变量1就是变量a,那把a的值给它装载出来,那就是把1的值装载出来,压入我们的操作数栈中。
  • iload_2 从局部变量2中装载int类型值:局部变量2就是变量b,那把b的值给它装载出来,那就是把2的值装载出来,压入我们的操作数栈中。
  • iadd 执行int类型的加法:要执行加法,它会先把操作数栈里的元素弹出来,做操作运算。也就是2+1等于3,运算做完后,它又会把运算结果 重新压回操作数栈。
  • bipush 将一个8位带符号整数压入栈:那么bipush 10,就是将后面这个数字10,压入操作数栈里面。
  • imul 执行int类型的乘法:和执行iadd方法是的类似,从操作数栈顶弹出两个最新的元素,做操作运算。也就是3*10等于30,运算做完后,把结果重新压回操作数栈。
  • istore_3 将int类型值存入局部变量3:局部变量3就是compute方法中的局部变量c。首先JVM会在局部变量表中,给我们局部变量c分配一块内存空间,然后会把操作数栈中的30 出栈,放到局部变量表中局部变量对应的那块内存空间里。

(4) compute方法的第four行java代码return c;,它对应的JVM指令码及含义:

  • iload_3 从局部变量3中装载int类型值:把刚刚存入局部变量3中内存位置的局部变量c,把c的值给它装载出来。也就是把30这个值装载出来,压入到我们的操作数栈中。
  • ireturn 从方法中返回int类型的数据:把刚刚装载出来的变量c,也就是到压入我们操作数栈中的int类型的数值30,把它返回给我们的主方法(调用者方法)里面去。
3. 程序计数器
  • 程序计数器,每一个线程都会有一个自己的程序计数器,线程私有。也就是,当你一个线程在开始运行的时候,Java虚拟机除了给它分配栈内存空间之外,还会给它分配一块自己独立的程序计数器内存空间,那程序计数器的作用,就是用来存放当前这个线程,正在运行到哪一行代码的执行位置,或者说内存地址,或者说一个指示标志位。
  • 在我们java代码中,从一个方法所对应的JVM指令码可以看到,每一条指令码的前面,都有一个数字,从0开始有序增加,那么我们就把这个数字理解为标识着,我们这一行代码,在我们方法中的 一个大概的标识位置,或者说一个行号。而程序计数器,实际上存放的就是,程序运行当前方法中,马上要运行的那一行代码,所对应的一个行号位置的值。
  • 那么JVM的开发人员,为什么要设计这么一个程序计数器。因为java是多线程运行的,如果我当前线程 正准备执行某个方法的某行代码,突然它的CPU时间片 被其他的优先级更高的线程,给它抢占过去了,那是不是意味着,我们当前线程可能要挂起。直到其他线程运行完毕之后,CPU才恢复之前挂起的线程去执行。那它恢复这个线程执行,它怎么知道要继续去执行 这个线程中的 哪个方法的 哪一行代码,就是根据程序计数器里面,存放的 当前这个线程 正在运行到 哪一行代码的内存地址,去恢复这个线程当时运行的时候的 那个状态,继续往下去执行。这就是程序计数器设计的一个初衷。
3.1 字节码执行引擎与程序计数器和方法区的关系
  • 那程序计数器存放的当前线程 运行到哪一行的值是如何变化的,实际上,是通过我们的字节码执行引擎去动态修改的。那字节码执行引擎,为什么能修改程序计数器的值,因为我们把一个字节码文件,加载到我们JVM内存区域中,实际上,最主要的是加载到方法区中。那这个方法区中的字节码代码,其实就是由我们的字节码执行引擎去执行的。字节码执行引擎去执行的代码,当然知道你这个代码现在运行到哪一行代码,下一次要运行哪一行代码。所以修改我们程序计数器的决策,当然是我们的字节码执行引擎。
4. 方法区:
  • Java的字节码文件 实际就是加载到方法区 中的,方法区 也是由很多部分组成,比如在oracle官网JVM规范中提到的常量池。简单来说,方法区中存放的数据,有如下几部分:常量,静态变量 和 类元信息(类信息)。比如,在我们的java代码的一个类中,
  • 如果 有一个常量,比如 public static final int initData = 666;,那这个常量就是存放在 方法区 的。
  • 如果 还有一个静态变量,而恰好 这个静态变量的值,又是一个对象,比如 public static User user = new User,这个对象,肯定也是存放在堆中的。那么 方法区中 静态变量中user 和 堆中对应的User对象 的关系,同栈 和 堆 之间的关系是一样的。方法区中的静态变量 实际上就是一块内存空间,这块空间被JVM标识成 名为user的名称,用以知道 这块空间 是给我们user这个静态变量去使用,而 这块空间中存放的 是静态变量的值对象 在堆里面的内存地址,从而 JVM 根据这个内存地址,就可以找到 堆里面 对应的对象。那么 方法区 和 堆 之间的关系,就出来了。
  • 在我们的java类中 可能会有很多的静态变量,如果 这些对象的值,恰好又是对象类型的,而对象的值,我们一般是分配在堆上面的,那么 这些 静态变量 里面存放的 就是 堆中对应对象 的内存地址,也就是指针。
  • 元空间的概念,方法区在jdk1.8之前,对 元空间 叫做永久代 或 持久代,在jdk1.8之后,改了下名字,叫元空间。实际上,都是方法区的具体实现。这个元空间在jdk1.8及之后,是非堆内存,用的直接内存,也就是我们操作系统的内存。或者说,方法区是JVM的一个规范,JVM规范中定义了有这么一个方法区,但底层实际上是由不同的虚拟机厂商来实现的。而这里的元空间就由是oracle厂商给出的具体实现。
  • 《Java虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。”但对于 HotSpotJVM 而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。
  • Metaspace(元空间)和 PermGen(永久代)都是对 JVM规范中方法区的一种落地实现只是 Oracle 从 JDK7 就开始就逐步的将永久代移除,慢慢的用元空间代替。直到JDK8 的发布才宣告 PermGen(永久代)的彻底移除。
  • jdk7开始已经对字符串常量池等从永久代移除,所以永久代的移除过程跨越了jdk7和jdk8两个大版本。是渐进式的移除。
5. 本地方法栈
  • 本地方法栈,在我们的java中,实际上用的不多了。本地方法栈 是 属于本地方法的。
  • 本地方法,是被native所声明的方法,在我们的java中有很多很多,比如,new Thread.start();,Thread的start()方法源码内部,它就有一个本地方法。当我们调用Thread的start()方法的时候,它最终会调用start0();这行代码,而 start0()方法,就是用native 修饰的一个本地方法接口,public native void start0();,当我们java代码调用到 start0 这行代码的时候,它的底层的代码实现,不是用java实现的,是C 或 C++ 语言实现的。
  • 那为什么 会有这么一个本地方法呢,这里面有一定的历史,因为我们java语言是95年真正的开始面世,对外发布的。其实95年之前,就有一些java语言的前身了。在当年,也就是95年之前的很多年,那个时候实际是C语言,或者说汇编语言的天下,也就是当时,一个公司99%的系统,可能要么用C语言,要么就是用汇编语言去写的,或者说其他的语言。但是要知道的是,Java语言95年一面世,就一鸣惊人,成为了非常牛逼的一门语言,除了跨平台,还有它非常牛B的面向对象特性。
  • 因为 C 或 C++语言,里面有一个非常麻烦的东西,就是很多很多的内存,我们需要自己分配,自己回收,也就是自己管理内存,要是稍微不小心,就会导致内存泄漏,但是对于Java语言来说,JVM已经都帮我们把这些事情给做了,所以我们根本就不用操心,对于我们Java开发人员来说,大大的降低了程序开发者的难度。所以,95年之后,Java语言一面世,就从此,它的发展趋势,一发不可收拾。早就已经变成我们整个的,所有语言里面的霸主地位,现在互联网公司 基本上后端,都是Java,一系列的全是Java,包括开源的世界里面,都是Java。那Java的地位(2020)几乎是不可撼动的,包括未来的一二十年,依然是Java语言的天下。
  • 那为什么会有这个本地方法,95年之后,很多公司可能意识到Java语言开发这么方便啊!那我们就用Java语言开发呗!那在当年一个新系统,你用Java语言开发,而公司这么多老系统,都是用C 或 C++ 语言开发的,那么,这些系统之间 就免不了,可能会有一些接口交互。在当年Java语言 和 C语言,做跨语言交互的时候,就是通过这种native本地方法。
  • 也就是Java代码在调用到native这样的代码的时候,它会通过我们的字节码执行引擎,去到我们的操作系统的底层的库函数,比如说,在Windows操作系统下面,它会去C 或 C++语言的库函数,也就是xxx.dll文件。这个东西,就和我们Java语言里面的jar包一样,里面有很多很多公用代码的一些实现,会去到这样的dll库函数文件里面,找start0()这行代码的实现,那这就是本地方法。
  • 现在,我们早就不再用这种本地方法,做跨语言交互了,现在我们跨语言交互手段多的是,什么app接口,远程调用,各种微服务框架等等。有了本地方法的概念,那么,本地方法栈,就是指,不管用什么语言写的本地方法,只要线程有在运行到本地方法,JVM也会给 这个线程 分配一块独立专属的内存空间,用来存放本地方法,在运行过程中需要用到的数据信息。
6. 堆:
  • 堆 是由 老年代 和 年轻代 两大部分组成。其中又有很多的细节,比如OOM,GC,调优等。oracle官网的JVM规范中,对堆的介绍:
  • The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads. The heap is the run-time data area from which memory for all class instances and arrays is allocated.
6.1 堆的组成
  • 堆 是由 年轻代 和 老年代组成。年轻代里面 分为 伊甸园(Eden) 和 Survivor区;Survivor区又分为S0 和 S1区。我们new出来一个对象,一般是放到伊甸园区的(伊甸园:亚当和夏娃造人的地方,很形象,new出来的对象,就是新生的东西)。那我现在假设有一个7x24小时不间断运行的web应用系统,那它会不断的产生对象往伊甸园区放。假设这些对象 在伊甸园区都放满了,放不下去了;而程序还在运行,还在继续生产对象,往伊甸园区放,Java虚拟机底层就会执行 GC(garbage collector)
6.2 GC
  • GC分为 minor gc(或 young gc)full gc。这里执行的gc就是young gc。那 GC实际上,是我们 字节码执行引擎,它在后台 开启的一个垃圾收集线程,专门收集我们堆区域的这些垃圾对象,那minor gc,其实 主要是收集我们的年轻代。
6.3 GC过程
  • GC底层 它收集垃圾的一个过程,或者说,GC怎么知道 堆中的哪些对象是垃圾,哪些对象该回收,以及垃圾收集大体的一个过程。
6.3.1 GC Root
  • 要想理解GC执行过程,这里有一个很重要的概念,叫GC Root。GC Roots根节点,是像一些 线程栈的本地变量、静态变量、本地方法栈的变量等,都可以叫做 GC Root。那整个GC的执行过程,实际上,它就会从那些GC Roots根节点的 那些变量 出发,找所有能引用到的对象。比如,线程栈中的本地变量的值 是一个对象,或 方法区中的静态变量的值 是一个对象,那么它们在堆中 就都有 指向的对象,所以,这些变量都可以作为GC Root。

  • 所以整个GC的过程,实际上就是从 方法区,从线程栈上面,找所有的本地变量,还有静态变量,只要它们的直接引用是对象类型的,就把它们全部找出来,然后从GC Roots这些变量出发,逐个的去找它们 引用的对象,比如说,有一个线程栈中 有一个成员变量math,Math math = new Math();,这个Math对象中的成员变量,又引用了一个对象,引用的对象 又引用了 另一个对象,依此找下去,直到找到最后那个对象,它的成员对象 不再引用任何其他对象。

6.3.2 复制算法
  • 凡是在从GC Root这个根节点出发,这个对象引用链条上的所有堆中的对象,它都会把它标记为非垃圾对象。一旦这些对象被标记为非垃圾对象,那么 这些对象就会通过 复制算法,被复制到Survivor中的 第一块区域S0,而剩下的 伊甸园里面的所有对象,都会被当做垃圾 直接清理掉。当然GC的过程很复杂,根据设置不同的垃圾收集器,也会有所区别,但大体的流程基本相似。

  • 那假设第一次minor gc做完了,程序还在不间断的运行,继续生产对象,伊甸园区又被放满了,放满了之后,又会触发minor gc,但这回触发的minor gc,不仅回收伊甸园区,同时还会回收那块非空的Suirvivor区域S0,把这两块区域 同时给回收,回收结束后,GC又会把伊甸园 和 非空的Survivor区域S0,这两块区域 剩余的存活对象,都复制到Survivor的另一块空的区域S1,然后清空 另外一块Survivor区域S0 和 伊甸园区,这两块区域中的 没有被GC Root 直接或间接引用的 垃圾对象。

6.3.3 可达性分析算法
  • 这里 在从GC Roots出发,去辨别一个对象,是否为垃圾对象的过程,用到了一个算法,叫可达性分析算法。也就是 将 "GC Roots"对象 作为起点,从这些节点 开始向下搜索 引用对象,找到的对象 都被标记为 非垃圾对象,其余未标记的对象 都是垃圾对象。
6.3.4 分代年龄
  • 在GC的过程中,还有一个重要的概念,叫分代年龄,也就是一个对象,每经历一次minor gc之后,如果 这个对象没有被清理掉,那么它的 分代年龄 就会加1。而这个 分代年龄,是存储在我们 对象头中的内容。
6.3.5 对象的组成 和 对象头
  • 一个类,或者说 一个对象,不仅仅是由 代码中表面的 那些成员变量 组成。一个完整的对象,除了成员变量 组成的 对象实例数据 之外,还有 对齐填充 和 对象头。对象头 是对象中 非常重要的组成部分,它结构比较复杂,但大体可以分为3部分,Mark Word,Klass Pointer 和 数组长度。而 分代年龄,就是存放在Mark Word当中,Mark Word在对象头中 是非常重要的一块信息,主要存放 对象运行时数据,其中除了存放 GC分代年龄,还有存放 对象的一些锁,线程持有锁;线程加锁的一些状态,锁状态标志;哈希值等。

  • 那随着我们程序的继续运行,伊甸园区 可能又会被放满,这时又会触发minor gc,那minor gc触发之后,除了回收 伊甸园区 之外,还会回收 非空的那块Survivor区,然后 又会把存活的对象复制到 另一块空的Survivor区中,这样一个对象 如果一直存活,每进行一次minor gc,复制一次,这个对象的 分代年龄 就会加1,而当一个对象 分代年龄,如果加到了15,也就是经历了15次minor gc,还没有被干掉,那这样的对象,会被复制到老年代去。

  • 那老年代放满了之后,又会发生什么,OOM内存溢出,但它不会马上OOM,而是先 触发一次full gc,那么full gc 和 minor gc一样,也是由我们字节码执行引擎,它在后台开启的一个垃圾回收线程,但是full gc,它是回收 我们整个堆内存中的 所有垃圾,那如果 老年代中的对象 依然被gc root引用着,full gc 实际上是,回收不到 垃圾对象的,也就是 老年代中 没有垃圾对象,那老年代 回收不到垃圾,又继续有 新的对象 被挪到老年代,最终JVM会发生OOM,程序报错OutOfMemory。

  • 那么,可以知道 我一个web应用系统,最终 哪些类型的对象 可能会被挪到老年代,比如有,静态变量引用的对象,缓存,数据库连接池对象,对象池,Spring容器里面的bean对象。

6.3.6 jvisualvm工具 与 GC过程验证
  • 这就是一个对象,怎么从伊甸园区,然后通过很多次gc,最终挪到老年代 的完整过程。这个过程是可以通过一段代码去验证的。
public class HeapTest{
	byte[] a = new byte[1024 * 100];	//100KB
	public static void main(String[] args) throws InterruptedException {
		ArrayList<HeapTest> heapTests = new ArrayList<>();
		while(true){
			heapTests.add(new HeapTest());
			Thread.sleep(10);
		}
	}
}
  • 首先,我在main方法中初始化了一个ArrayList,然后 又加了一个 while(true)死循环,往我们这个ArrayList中 来放新new出来的HeapTest对象。这个代码 最终执行 肯定会内存溢出,那 这里面为什么会内存溢出,就是因为 在while(true)死循环里 生成出来的对象,都被ArrayList引用着,放在这个List集合里面,也就是List内部肯定有一些指针 引用着这些对象。而这个ArrayList,又是被heapTests,这个main方法的局部变量引用着,heapTests这个变量,就是gc root根节点。

  • 我们这些被gc root引用的 对象,最开始会放到伊甸园区,伊甸园区放满之后,会触发minor gc,那么,gc会从gc root根节点出发,找到它的 所有引用对象,而在 while(true)死循环中 又不断new出 新的对象,都会被gc root引用着,所以 gc不会对 这些对象 进行回收,这样的对象 最终都会被挪到 老年代。

  • 那现在 我们把这个代码运行起来,证明一下 这个gc垃圾回收的完整流程。当代码运行起来后,就会执行到while(true)死循环里,它会执行一会,还没有那么早的内存溢出。然后,我们通过cmd打开Windows命令窗口,再通过命令jvisualvm,打开一个jdk安装好后,自带的jvm调优诊断工具。这个工具启动之后,它会自动在本机 找所有的jvm进程。

  • 比如说,我们正在运行的HeapTest程序 运行的进程,找到后,双击进去看一下里面有什么东西,进去之后,会有我们当前进程的JVM参数,系统参数,包括这个进程使用的CPU情况,类的加载情况,堆的使用情况,元空间的使用情况,还有一些线程的使用情况,等等。那这些东西,其实就可以帮我们去调优诊断 整个JVM的运行情况。

  • 那我们这里主要看的是 Visual GC这个视图:

  • 哎这个图像有一点点那个,有一点点不对,这个图像可能还不是特别清晰啊。我把这个地方给它执行的快点,可能能执行的比较慢,看看我们这边可能设置了一个参数,打开Run/Debug Configurations,在VM options: -XX:-UseAdaptiveSizePolicy,这个VM options输入框中的参数去掉,然后重新运行一下代码,标准的没有任何参数 执行这个代码的时候,它是这样的一个效果,重新打开jvisualvm,它的Visual GC视图,左侧是我们的伊甸园区,和两块Survivor区,老年代的内存增长过程,Eden是我们的伊甸园区,我们新new出来的对象,会往伊甸园区放,所以伊甸园区,它会慢慢的增长,增长到一定程度,突然没有了,但是两块Survivor区域中一块,突然增长了一块,两块Survivor区域S0和S1,一会S0有对象被存放,一会S1有对象被存放,但是老年代Old区域,是一直往上增加的。然后 右侧的视图是每个区域,详细的一个使用的曲线图,这个曲线图实际上就是我们当前程序正在运行的内存分配模型的流转。一开始Eden区的曲线图就是慢慢增长,增长到放满了,触发一次GC,而我们当前程序的对象都是被gc root引用着,非垃圾对象,回收不了,就会挪到我们Survivor区域,所以Survivor区域一开始没有,突然增长了一大块区域,当然是从伊甸园区域挪过来的,伊甸园被清空了。那下一次又会往伊甸园区放,伊甸园区被放满,又会触发一次gc,而这回触发的gc,不仅回收伊甸园区域,还会回收上一次被占用的Survivor区域,然后把回收到的非垃圾对象挪到,另一块上次没有被占用的Survivor区域,上次被占用的Survivor区域和伊甸园区被清空了。那如果Suvivor区域放不下,另一块Suvivor区域和伊甸园区复制过来的非垃圾对象时,就会把多余的非垃圾对象挪到老年代,所以老年代有时也会同时增长一块区域,当然老年代的增长相对比较平稳,但是突然会增长,这个增长除了Survivor区域放不下之外,可能还会积累了一大批分代年龄达到15的对象,这批对象也会被挪到老年代,同时Survivor区域就会增长过程中,又马上减少,就是分代年龄达到15的对象被移到老年代了。这个程序执行过程中,对象流转的模型就是visual GC所展示的曲线图。

  • 那随着程序的不断运行,老年代的非垃圾对象不断增长,总有一天会被放满,当老年代区域被放满时,会触发full gc,full gc和minor gc一样,也是由字节码执行引擎开启的后台线程 做垃圾收集这件事,但是full gc,它实际上是回收我们整个堆里面的所有垃圾,但是,在我们当前HeapTest程序,即便挪到老年代,这些老年代中的对象依然被gc root引用着,full gc其实是回收不了什么垃圾对象的,也就是我们这个HeapTest当前程序没有什么垃圾,回收不了垃圾,又继续往老年代复制对象进去,那最终jvm就会发生OOM,报错OutOfMemory。

7. JVM调优:
  • JVM调优的目的,主要 就是 减少full gc的次数 和 执行时间,因为只要JVM执行垃圾收集gc,就会发生STW(Stop The Word),停止整个世界,它是在我们JVM只要开始执行gc了,gc实际是由后台垃圾收集线程去执行的,只要开始执行gc,JVM会停掉所有用户线程,比如说一个电商网站,用户正在点击了一个购物车按钮,点击了一个下单按钮,在点击了这些按钮之后,后端都有一些程序的线程要执行,而凡是又用户操作发起,得到的这些线程的执行,那么这些线程就叫做用户线程,那如果把这些用户线程停掉,对用户来说,比如 用户刚点了一个加入购物车按钮,突然网站卡顿了一下,隔一会又好了,STW实际上就是停掉你用户的操作,对用户的感知就是突然卡了。也就是后台的老年代满了,发生了full gc,如果这个full gc执行的时间比较长,那么用户在前端点击按钮,就可能会突然卡一下,底层就是做了STW,把用户线程给停掉了。

  • 那么为什么JVM的开发人员要设计这么一个STW机制,难道就不能去掉这个机制么?也就是full gc的时候,不暂停用户线程,用户线程与我们垃圾线程一起去执行,这样程序性能会不会提高一点。那我们用反证法来推演一下,假设JVM在执行full gc的时候,不进行STW,也就是说用户线程不暂停,那gc就会不断的去从gc root寻找引用的对象,当一个线程执行结束后,那这个线程的gc root变量也会被销毁,内存空间被释放,那么gc之前通过gc root在地址找到的那些被标记为非垃圾的对象,就动态变成了垃圾对象,而我们堆内部可能有 成千上百万个对象,gc执行过程中,刚刚被标记为非垃圾的对象,马上又变成了垃圾对象,此时gc还没做完垃圾回收,难道还要从头再开始标记一次么。如果不从头再进行gc,那么gc实际上就回收到的对象 就是 垃圾对象 和 非垃圾对象 的混合体,就乱套了。会导致一系列更为的复制系统问题,比如分代年龄标记混乱,对内存空间空间需要更大的存储要求,等等。也就是如果不做STW,就会导致堆中的对象状态时刻发生改变,这时就根本不知道如何去标记和回收,所以JVM的设计人员索性就在执行gc的时候,让关键的地方进行STW,让JVM专心的一次性把所有非垃圾对象都标记出来。那这样设计后,gc的执行时间还比较短,系统性能还更高一点。所以对用户的感知而言,虽然有一点STW,但执行时间很短,大多数的用户是感知不到的。

  • 虚拟机栈,程序计数器,本地方法栈 为线程独享;堆 和 方法区 为所有线程共享。


垃圾回收是Java中一项重要的语言特性,它负责自动管理内存并释放不再使用的对象。Java虚拟机(JVM)的垃圾回收机制使用了一种称为"Stop-The-World"(STW)的技术。本篇文章将详细解析Java垃圾回收和JVM中的STW。

  1. Java垃圾回收(Garbage Collection)

在Java中,垃圾回收是自动进行的,无需开发人员手动管理内存。垃圾回收器会周期性地扫描堆内存,标记并释放不再使用的对象所占用的内存空间。这样,开发人员可以专注于业务逻辑而不必过多关注内存管理。

  1. 垃圾回收算法

Java虚拟机中的垃圾回收器使用了不同的垃圾回收算法,例如标记-清除算法、复制算法、标记-整理算法等。这些算法的选择取决于应用程序的特点和需求。

  1. Stop-The-World(STW)

STW 是Java垃圾回收机制中的一种策略。当垃圾回收器开始执行时,它会暂停应用程序的所有线程,使得整个应用程序进入停顿状态。在这个过程中,垃圾回收器可以安全地执行垃圾回收操作,而不会受到应用程序线程的干扰。

STW对于应用程序的性能和响应时间有一定的影响,特别是在大型内存的应用中。因此,优化垃圾回收和减少STW的时间是提高应用程序性能的重要考虑因素。

.参考文献

1.Java垃圾回收和JVM中的STW解析.

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

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

相关文章

MyBatis第三课

目录 回顾 #和$区别 #&#xff08;预编译SQL&#xff09;和$&#xff08;即时SQL&#xff0c;它是进行的字符串拼接&#xff09;的区别&#xff0c;其中之一就是预编译SQL和即时SQL的区别 原因&#xff1a; 回顾 两者的共同点 MaBits可以看作是Java程序和Mysql的沟通桥梁&…

网页设计与网站建设作业html+css+js,一个简易的游戏官网网页

一个简易的游戏网页 浏览器查看 目录结构 部分代码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport&…

【c++】利用嵌套map创建多层树结构

通常树的深度都大于1&#xff0c;即树有多层&#xff0c;而树结构又可以用c的map容器来实现&#xff0c;所以&#xff0c;本文给出了一种多层树结构的实现思路&#xff0c;同时也给出了相应的c代码。 整体思路概述 首先定义一个节点类Node类&#xff0c;要包括children&#x…

EI论文复现:考虑多能互补的综合能源系统/虚拟电厂/微电网优化运行程序代码!

本程序参考EI论文《基于多能互补的热电联供型微网优化运行》&#xff0c;文章通过储能设备解耦热电联系&#xff0c;建立基于多能互补的综合能源系统/虚拟电厂/微电网优化运行模型。模型包含系统供给侧的多能互补协调与需求侧的综合能源响应两个方面&#xff0c;使供给侧通过能…

基于springboot时间管理系统源码和论文

在Internet高速发展的今天&#xff0c;我们生活的各个领域都涉及到计算机的应用&#xff0c;其中包括时间管理系统的网络应用&#xff0c;在外国时间管理系统已经是很普遍的方式&#xff0c;不过国内的管理系统可能还处于起步阶段。时间管理系统具有时间管理功能的选择。时间管…

XTuner 大模型单卡低成本微调实战

文章目录 配置环境微调部署与测试自定义微调 XTuner 大模型单卡低成本微调 原理可查看 XTuner 大模型单卡低成本微调原理 配置环境 创建一个名为xtuner&#xff0c;python3.10版本虚拟环境 conda create --name xtuner0.1.9 python3.10 -y创建一个xtuner019文件夹&#xff0c…

WebGL在虚拟现实(VR)的应用

WebGL在虚拟现实&#xff08;VR&#xff09;领域的应用日益增多&#xff0c;它为在Web浏览器中创建交互式的虚拟现实体验提供了强大的支持。以下是一些WebGL在VR领域的应用示例&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&am…

Word插件-大珩助手-手写电子签名

手写签名 支持鼠标写&#xff0c;支持触摸屏写&#xff0c;点击画笔按钮切换橡皮擦&#xff0c;支持清空画板重写&#xff0c;点击在word中插入签名&#xff0c;可插入背景透明的签字图 素材库-保存签名 将写好的签字图复制粘贴到素材库中&#xff0c;以便永久使用&#xff…

Visual Studio Code1.67版本已正式发布,新增Rust指南

Visual Studio Code1.67版本已正式发布&#xff0c;该版本包含大量增强生产力的更新项&#xff1a; 资源管理器文件嵌套 通过这次更新&#xff0c;用于浏览和管理文件和文件夹的Visual Studio Code的资源管理器工具现在支持基于名称嵌套相关文件。 资源管理器现在支持根据文…

【Linux操作】国产Linux服务管理操作

【Linux操作】国产Linux服务管理操作 前言SAMBA配置服务器端1. 安装相关包2. 配置/etc/samba/smb.conf&#xff0c;在此文件末尾添加如下内容&#xff0c;并保存退出。3. 创建/home/share并更改权限4. 启动samba服务 客户端• Windows客户端• 麒麟客户端 Telnet1、telnet语法2…

机器学习中的线性回归

线性回归 概念 利用 回归方程(函数) 对 一个或多个自变量(特征值)和因变量(目标值)之间 关系进行建模的一种分析方式。 分类 一元线性回归&#xff1a;y wx b 目标值只与一个因变量有关系 多元线性回归&#xff1a; y w_1x_1 w_2x_2 w_3x_3 … b 目标值只与多个…

Linux Ubuntu搭建我的世界Minecraft服务器实现好友远程联机MC游戏

文章目录 前言1. 安装JAVA2. MCSManager安装3.局域网访问MCSM4.创建我的世界服务器5.局域网联机测试6.安装cpolar内网穿透7. 配置公网访问地址8.远程联机测试9. 配置固定远程联机端口地址9.1 保留一个固定tcp地址9.2 配置固定公网TCP地址9.3 使用固定公网地址远程联机 前言 Li…

SpringSecurity6 | 登录失败后的跳转

✅作者简介:大家好,我是Leo,热爱Java后端开发者,一个想要与大家共同进步的男人😉😉 🍎个人主页:Leo的博客 💞当前专栏: 循序渐进学SpringSecurity6 ✨特色专栏: MySQL学习 🥭本文内容: SpringSecurity6 | 失败后的跳转 📚个人知识库: Leo知识库,欢迎大家…

Qt/QML编程学习之心得:使用camera摄像头(35)

汽车应用中,camera起到了越来越多的作用,数字化的作用,这点无可争议,而作为GUI设计工具,如何让Camera类的应用能更好的发挥作用呢? You can use Camera to capture images and movies from a camera, and manipulate the capture and processing settings that get appl…

C#MQTT编程03--连接报文

1、报文回顾 MQTT协议中一共有14个报文&#xff0c;如下图 从上图看&#xff0c;我们要特别要注意以下3个点&#xff1a; (1)值&#xff0c;14个报文的排列&#xff0c;不是随意的&#xff0c;每个报文都有自己的值&#xff0c;而值在报文中是要用到的。后面例子会介绍到。 …

【含完整代码】Java定时任务之xxl-job[超详细]

前言 个人博客&#xff1a;www.wdcdbd.com 在Java中使用定时任务是一件很常见的事情&#xff0c;比如使用定时任务在什么时间&#xff0c;什么时候&#xff0c;去发布一些信息&#xff0c;或者去查询一些日志等相关的代码。这时&#xff0c;我们就要开发定时任务这中功能来实现…

理解TCP/IP协议

一、协议 在计算机网络与信息通讯领域里&#xff0c;人们经常提及 “协议” 一词。互联网中常用的协议有HTTP、TCP、IP等。 协议的必要性 简单来说&#xff0c;协议就是计算机与计算机之间通过网络通信时&#xff0c;事先达成的一种 “约定”。这种“约定”使不同厂商的设备…

精通业务:资深程序员的核心优势

在IT行业&#xff0c;我们常常听到关于技术实力、项目经验、团队协作等方面的讨论&#xff0c;但有一个重要因素常常被忽视&#xff0c;那就是对业务的了解。 对于资深程序员来说&#xff0c;懂业务和不懂业务之间的区别&#xff0c;犹如一道深邃的鸿沟&#xff0c;决定着他们…

【MySQL】:探秘主流关系型数据库管理系统及SQL语言

&#x1f3a5; 屿小夏 &#xff1a; 个人主页 &#x1f525;个人专栏 &#xff1a; MySQL从入门到进阶 &#x1f304; 莫道桑榆晚&#xff0c;为霞尚满天&#xff01; 文章目录 &#x1f4d1;前言一. MySQL概述1.1 数据库相关概念1.2 主流数据库1.3 数据模型1.3.1 关系型数据库…

QT DAY5作业

1.QT基于TCP服务器端 头文件 #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QTcpServer> //服务器类 #include <QMessageBox> //消息对话框类 #include <QTcpSocket> //客户端类 #include <QList> //链表容器类namespace …