JVM第二课:一文讲透运行时数据区

JVM第二课:一文讲透运行时数据区

运行时数据区(Runtime Data Area)

内存是非常重要的系统资源,是硬盘和CPU的中间仓库和桥梁,承载着操作系统和应用程序的实时运行,

运行时数据区

虚拟机启动时,一个虚拟机对应一个进程,一个进程有多个线程

堆,堆外内存(元空间和代码缓存)线程之间共享,一个进程有个一份

虚拟机栈,本地方法栈,程序计数器,一个线程有一份

线程

在Hotspot JVM中,每个线程都与操作系统的本地线程直接映射

当一个Java线程准备好执行以后,此时一个操作系统的本地线程也同时创建,Java线程停止后,本地线程也会回收

操作系统负责所有线程的调度,安排到任何一个可用的cpu上,一旦本地线程初始化完成,它就会调用Java线程的run()方法

JVM系统线程

虚拟机线程,周期任务线程,GC线程,编译线程,信号调度线程

  1. 程序计数器(PC寄存器)
    每一个线程都有它自己的程序计数器,是线程私有的,生命周期和线程的生命周期保持一致

是唯一没有OOM错误的区域

CPU需要不停的切换线程,切换回来,需要知道从哪里开始继续执行,JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么字节码指令

2.Java虚拟机栈

虚拟机栈出现的背景

由于Java跨平台的设计,Java的指令都是根据栈来设计的,不同平台的CPU架构不同,不能设计为根据寄存器的

优点是跨平台,缺点是性能下降,指令多

内存中的栈和堆

栈是运行时的单位,而堆是存储的单位

栈解决程序运行问题,程序如何运行,或者如何处理数据

堆解决的是如何存储数据,数据怎么放,放在哪

Java虚拟机栈是什么

每个线程在创建的时候,都会创建一个虚拟机栈(Java Virtual Machine Stack),其内部保存一个个栈帧(Stack Frame),对应着一次次Java方法调用,方法的调用,就是入栈和出栈

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RxVa0M7K-1759971726664)(/Users/temp/Library/Application Support/typora-user-images/image-20201108171030217.png)

是线程私有的,生命周期和线程一致

栈的访问速度仅次于程序计数器

对于栈来说没有垃圾回收问题,是存在OOM的

栈中常见的异常

Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的

采用固定大小的虚拟机栈,如果线程请求分配的栈容量超过Java虚拟机栈允许的容量,Java虚拟机会抛出StackOverflowError异常,递归的时候,常出现

设置栈大小:-Xss10m

栈的大小可以决定方法调用的深度

如果虚拟机栈可以动态扩展,并且在尝试扩展时无法申请到足够的内存,或者在创建线程时没有足够的内存创建虚拟机栈,就会抛出OutOfMemoryError,创建的线程过多,可能会出现

栈的存储单位

栈中的数据都是以栈帧的格式存在的

在线程中的每个方法都各自对应一个栈帧

栈帧是一个内存区域,是一个数据集,维系着方法执行过程中的各种数据信息

对栈的操作只有两个:入栈和出栈,栈:先进后出

一个线程在执行过程中,只有一个栈帧在执行,成为当前栈帧,对应的方法是当前方法,对应的类是当前类

执行引擎运行的所有字节码指令只针对当前栈帧进行操作

栈帧的内部结构

局部变量表(Local Variables)

定义一个数字数组,用于存储方法参数和方法内定义的变量,这些数据类型包括各种基本数据类型,对象引用,以及returnAddress类型

局部变量表所需的容量大小是在编译期确定下来,并保存在方法的code属性下的maximum local variables数据项中,在方法运行时不会修改局部变量表的大小

局部变量表的基本单位是Slot(变量槽)

在局部变量表中,32位以内的类型占一个槽,64位的类型(Long和Double)占用两个槽

byte,short,char,float都转为int,boolean 0是false,1是true,都占一个槽

JVM会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引可以局部变量表中指定的局部变量值

当一个实例方法被调用时,该方法的参数和局部变量都会按顺序被复制到局部变量中中的每一个slot上

如果需要访问的局部变量表中的一个64位的局部变量值时,只需要使用前一个索引即可

如果当前帧是由构造方法或实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余按顺序往下排

slot是可以重复利用的,有局部变量过了作用阈,之后申明的局部变量就可以用过期局部变量的slot位,达到节省资源

在栈帧中,与性能调优关系最密切的就是局部变量表

其表中的变量是重要的垃圾回收根节点,只要是局部变量表中直接或间接引用的对象都不会被回收

操作数栈(Operand Stack)

栈可以使用数组或链表实现,操作数栈使用数组实现的,即满足栈的特点,还有数组的特性,按照顺序存放,有索引

每一个独立的栈帧中,还包含一个后进先出的操作数栈

操作数栈,在方法执行的过程中,根据字节码指令,往栈中写入数据和提取数据,即入栈和出栈

操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中的变量临时的存储空间

操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧会创建,这个方法的操作数栈是空的

每一个操作数栈都会拥有明确的深度用于存储数值,其所需的最大深度在编译期就确定了,max_stack值就是

操作数栈并非使用索引进行访问,而是只能通过标准的入栈或出栈来完成访问

操作数栈代码追踪

bipush:b是byte类型

istore:保存的是int类型

定义的int类型是-125-126之间的数,操作数栈是byte类型,再大一些,就是short类型

动态链接

动态链接:指向运行时常量池的方法引用

包含这个引用的目的是为了支持当前方法的代码能够实现动态链接

在Java源文件被编译到字节码文件中时,所有的变量和方法引用都做了符号引用,保存在class文件的常量池中

比如:描述一个方法调用了另外的其他方法,就是通过常量池中指向方法的符号引用来表示

动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用

虚方法和非虚方法

如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的,这样的方法是非虚方法

静态方法, final修饰的,私有方法,实例构造器,父类方法都是非虚方法

其他方法都是虚方法

虚拟机中提供了以下几条方法调用指令:

普通调用指令:

invokestatic:调用静态方法,解析阶段确定唯一方法版本

invokespecial:调用方法,私有及父类方法,解析阶段确定唯一方法版本

invokevirtual:调用所有虚方法

invokeinterface:调用接口方法

imvokestatic和invokespecial指定调用的方法是非虚方法,invokevirtual(final修饰的方法除外)和invokeinterface是虚方法

动态调用指令

invokedynamic:动态解析需要调用的方法, 然后执行(java7中增加),在java8 Lambda表达式中,有了invokedynamic

动态语言和静态语言

区别是对类型的检查是在编译期还是运行期

静态语言是判断变量自身的类型信息,动态语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息

Java:String name = "mubiao";

Python: name = 132

虚方法表

在面向对象编程过程中,会频繁的使用动态分派,为了提高性能,JVM在类的方法区建立一个虚方法表(Virtual method table),使用索引表来代替查找

每个类都有一个虚方法表,表中存放着方法的实际入口

虚方法表会在类加载的链接阶段创建并初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完成

方法执行后有两种方式退出:

执行引擎遇到方法返回的字节码指令(return)

在字节码指令中,返回指令包含ireturn(返回类型是boolean,byte,short,char,int类型时),lreturn,dreturn,areturn(引用数据类型),void返回的是return

异常退出

方法执行过程中抛出异常时的异常处理,存储在一个异常处理表

局部变量表中有异常出现的字节码指令数和异常出现时需要执行的字节码指令位置

关于栈的面试题

垃圾回收会涉及到栈吗?不会

栈空间越大越好吗?不是,分配的大,出现StackOverflowError概率小了,但是资源有限,占用的内存大了,线程可能就少了

方法中定义的局部变量是线程安全的吗?

3.本地方法栈

本地方法

非java语言编写的方法,native修饰的方法

本地方法栈用于管理本地方法的调用

本地方法栈,也是线程私有的

可以固定大小或动态扩展

4.堆

运行时数据区最重要的区域

堆和方法区,一个进程中的多个线程共享

一个JVM实例只有一个堆内存,堆也是Java内存管理的核心区域

JVM启动,堆就创建了,空间大小也确定了,是JVM管理的最大的一块内存空间

堆内存空间是可以调整的,

堆可以处于物理上不连续的存储空间(有虚拟内存存在,把不连续的数据,都读到连续的虚拟内存中), 但在逻辑上应该被视为连续的

所有的线程共享堆空间,但在堆中还可以划分线程私有缓冲区(Thread Local Allocation Buffer, TLAB)

《Java虚拟机规范》中对堆的描述是:所有的对象实例和数组都应当在运行时分配在堆上,但是有逃逸分析,会出现在栈上分配

堆中的对象不会马上被删除,是GC重点区域

堆的核心:内存细分

Java7分为:新生代,老年代,永久代

Java8分为:新生代,老年代,元空间

设置的初始堆空间大小值,和最大堆空间大小值:-Xms10m -Xmx10m,是新生代和老年代的总和

堆空间的大小设置

Java堆用于存储Java对象实例,堆的大小在JVM启动时就已经设定好了,通过"-Xms和-Xmx"进行设置

-Xms:堆的起始内存,等价于:-XX:InitialHeapSize

-X是Java运行参数,ms是Momory Start

-Xmx:堆的最大内存,等价于:-XX:MaxHeapSize

一旦堆区的内存超过了-Xmx所设置的最大内存值,将会抛出OutOfMomoryError异常

通常会将-Xms和-Xmx的值设置相同,可以避免在垃圾回收时,内存的重新分配,导致的。。。

默认堆内存大小:

默认初始堆内存大小:-Xms是物理电脑内存的1/64

默认最大堆内存大小:-Xmx是物理电脑内存的1/4

public static void main(String[] args) { long xms = Runtime.getRuntime().totalMemory()/1024/1024; long xmx = Runtime.getRuntime().maxMemory()/1024/1024; System.out.println(xms + "M"); System.out.println(xmx + "M"); } 245M 电脑的内存是16G,245*64=15.3125,没有到16G,这是因为啥?后面分析 3641M

手动设置初始堆内存和最大堆内存

分析堆中的内存分配情况

设置了堆初始内存大小是600m,最大堆内存是600m

运行上面的代码,得到的结果是

实际上只有575M,是因为什么?

分析:S0和S1分配到内存,只有同一时间只有一个能用到

-Xms600m -Xmx600m -XX:+PrintGCDetails

也可以分析看内存空间

新生代和老年代

存储在JVM中的对象可以划分为2类:

一类是生命周期很短,这类对象创建和销毁都很迅速

另一类对象生命周期很长,极端情况下可以和JVM的生命周期一样

堆进一步划分为:新生代和老年代,其中新生代分为:伊甸园区(Eden),Survivor0(From区)和Survivor1(To区)

新生代和老年代的分配比例

新生代和老年代默认比例是1比2,设置的堆内存是600M,新生代是200M,老年代是400M

配置新生代和老年代在堆中的占比

默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占堆内存的1/3

一般不会调整这个值,除非发现生命周期长的对象占比很多,可以调整NewRatio大一些

配置伊甸园区和S0,S1的比例

在Hotspot中,Eden空间和另外两个Survivor空间缺省比例是8:1:1

200M的新生代,伊甸园区应该是160M,S0和S1应该是20M,但是实际上发现是:6:1:1

这是因为有自适应分配内存(-XX:-UseAdaptiveSizePolicy)设置取消自适应分配,没有变化,就需要显式设置大小

通过 -XX:ServivorRatio=8

设置新生代的空间大小(了解)

-Xmn,如果比例也设置了,-Xmn也设置,最终按-Xmn的值为准

对象在堆中的过程

图解对象分配过程

一般过程

new的对象先放伊甸园区

伊甸园区满了,触发(Minor GC),将伊甸园区不再被其他对象所引用的对象进行销毁,再加载新的对象到伊甸园区

然后将伊甸园区剩余的对象加载到Survivor0(幸存者0区)区

如果再次触发垃圾回收,此时上次幸存在S0区的对象如果没有回收,会放到S1中

S0和S1交替

啥时候到老年代中?可以设置次数,默认是15次,-XX:MaxTenuringThreshold=进行设置

总结:

只有在伊甸园区满的时候,才会触发Mirror GC,顺便清理一下S0和S1区

S1和S2复制之后有交换,谁空谁是To

详细过程

Minor GC,Major GC 与Full GC

针对Hotspot VM的实现,它里面的GC按照区域分为两大种类型:一种是部分收集(Partial GC) ,一种是整堆收集(Full GC)

部分收集:

新生代收集(Minor GC)

老年代收集(Major GC)

只有CMS GC有单独收集老年代的行为

注意:很多时候Major GC和Full GC会混淆,需要辨别是老年代回收还是整堆回收

汇合收集

只有G1 GC会有这种行为

整堆收集(Full GC)

收集整个堆和方法区的垃圾收集,应尽量避免

Minor GC

Minor GC触发条件:当伊甸园区满的时候,就会触发, 幸存者0区和1区满,不会触发Minor GC

Minor GC触发比较频繁,因为对象大都存活很短时间

Minor GC会引发STW(Stop The World),GC过程中,用户线程会停止

Major GC

出现Major GC往往会伴随着至少一次Minor GC(并非绝对),老年代空间不足时,会触发Minor GC,如果空间还是不足的话,则触发Major GC

Major GC的速度一般会比Minor GC慢10倍以上,STW时间更长

如果Major GC后,内存还不足,就报OOM

动态年龄判断:如果Survivor区中相同年龄的对象大小的总和大于Survivor空间的一半,大于或等于这个年龄的对象直接进入到老年代

TLAB

堆区是线程共享区域,任何线程都可以访问堆区中的共享数据,并发的情况下,堆区中的内存空间是线程不安全的

什么是TLAB

堆Eden区进行划分,JVM为每个线程分配了一个私有的缓存区域,它包含在Eden空间中

多个线程同时分配内存时,使用TLAB可以避免一系列的线程安全问题

这种分配内存方式称为快速分配策略

尽管不是所有的对象都能够在TLAB中成功分配内存,但JVM确实将TLAB当作内存分配的首选

通过:-XX:UseTLAB 设置是否开启TLAB空间

默认TLAB空间占Eden区的1%,通过- XX:TLABWasteTargetPercent设置占比

堆空间设置的参数

/** * -XX:+PrintFlagsInitial:查看所有参数的默认值 * -XX:+PrintFlagsFinal:查看所有参数的最终值 * 查看某个参数具体的值 jps ---> 进程号 * jinfo -flag 参数名 进程号 * -Xms:堆的初始值(默认物理内存的1/64) * -Xmx:堆的最大值(默认物理内存的1/4) * -Xmn:新生代大小 * -XX:NewRatio:分配新生代和老年代在堆中的占比 * -XX:MaxTenuringThreshold:设置新生代中垃圾的最大年龄 * -XX:+PrintGCDetails:输出详细的GC处理日志 * * * */ public class HeapArgsDemo { public static void main(String[] args) { System.out.println(args); } }

空间分配担保策略

参数:HandlePromotionFailure设置是否允许担保,JDK7以后都设置为True

处理逻辑是:老年代剩余空间是否满足需要晋升到老年代的数据

堆是分配对象存储的唯一选择吗?

《深入理解Java虚拟机》中描述:随着JIT编译器和逃逸分析技术的成熟,栈上分配,标量替换优化技术将会导致:所有对象在堆中分配变得不是那么绝对了

JIT:即时编译编译器 just in time

如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话, 可能被优化在栈上分配,就不用在堆上分配了,就不用垃圾回收了,这是最常见的堆外存储技术

逃逸分析概述

通过逃逸分析,Java Hotspot编译器能够分析出一个对象的引用的使用范围,从而决定是否将这个对象分配到栈上

逃逸分析的基本行为就是分析对象的作用域

当一个对象在方法中被定义后,对象只在方法中使用,就是没有发生逃逸

当一个对象在方法中被定义后,它被外部方法引用,则认为发生了逃逸,例如被当作参数调用了其他方法

快速判断是否发生了逃逸:new的对象实体,是否有可能在方法外被调用

使用版本和结论

从JDK7开始,Hotspot默认就已经开启了逃逸分析

配置:-XX:-DoEscapeAnalysis

在开发中,能使用局部变量的,就不要在方法外定义变量

逃逸分析:代码优化

栈上分配

如果一个对象没有逃逸,就可能被优化成栈上分配,就没有垃圾回收了

示例:

/** * 是否需要逃逸分析,默认是有逃逸分析的 * -XX:-DoEscapeAnalysis */ public class EscapeAnalysisDemo { public static void main(String[] args) { long start = System.currentTimeMillis(); for (int i = 0; i<10000000; i++){ accept(); } long end = System.currentTimeMillis(); System.out.println(end-start); try { Thread.sleep(10000000); } catch (InterruptedException e) { e.printStackTrace(); } } private static void accept() { User user = new User(); } static class User{} }

设置成不使用逃逸分析的时候:

-XX:-DoEscapeAnalysis

执行完需要用到的时间是:

设置成使用逃逸分析的话:

默认就是逃逸分析

-XX:+DoEscapeAnalysis

执行完需要用到的时间是:

极大的提高了效率

同步省略

如果一个对象被发现只能从一个线程被访问到,对于这个对象的操作就可以不考虑同步了

在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能被一个线程访问而没有发布到其他线程,如果没有,JIT编译器会取消这部分的同步,以提高并发和性能,也叫锁消除

分离对象/标量替换

有的对象可能不需要一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不保存在内存中,而是保存在cpu的寄存器中

标量:一个无法分解成更小数据的数据,java中基本数据类型就是标量

聚合量:可以分解的数据,Java中的对象就是聚合量

在JIT阶段,如果经过逃逸分析,发现一个对象,不会被外界访问,那么经过JIT优化,就会把这个对象拆解成多个标量来代替,这个过程就是标量替换

标量可以在栈中分配,就没有垃圾回收了

目前Hotspot并没有实现真正意义的栈上分配,而是有标量替换

方法区

栈,堆,方法区的交互关系

从线程共享与否的角度来看

对象在堆中,对象的引用在栈,类型在方法区

方法区的理解

Method Area

《Java虚拟机规范》:尽管所有的方法区在逻辑上属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾回收和压缩

HotSpot:方法区还有一个别名:Non-Heap(非堆),目的就是和堆分开

方法区是看作是一块独立于Java堆的内存空间

方法区和堆一样,是各个线程共享的内存区域

方法区在JVM启动的时候创建

方法区的大小是可以固定或可扩展的

方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出:OOM:MetaSpace

关闭JVM就会释放这部分区域

HotSpot方法的演进

在JDK7之前,方法区称为永久代,JDK8之后称为元空间

在JDK8,HotSpot废弃了永久代的概念,改用和JRockit,J9一样的在本地内存中实现的元空间(Metaspace)来代替

永久代和元空间,内部结构也调整了,静态变量,字符串常量池等

设置方法区大小的参数

方法区的大小,jvm可以根据应用的需要动态调整

JDK7及以前

通过-XX:PermSize来设置永久代的初始分配空间,默认是20.75M

通过-XX:MaxPermSize设置永久代最大可分配空间,默认32位机器是64M,64位机器是82M

JDK8

元数据区的大小可以通过参数:-XX:MetaspaceSize和-XX:MaxMetaspaceSize指定

默认值依赖于平台,windows下MetaspaceSize的值是21M,MaxMetaspaceSize的值是-1,没有限制大小

-XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=100m

-XX:MetaspaceSize设置初始的元空间大小,64位操作系统的默认值是21m,这是初始的高水位线,一旦触及这个水位线,Full GC将会触发并卸载没有用的类,然后这个高水位线将会重置,新的水位线的值取决于GC后释放了多少元空间,如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值,如果释放空间过多,调低该值

为了避免多次GC,建立适当提高-XX:MetaspaceSize的值

方法区的内部结构

方法区用于存储已被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存

类型信息

对每个加载的类型(类class,接口interface,枚举enum,主角annotation),JVM必须在方法区存储一下类型信息:

这个类型的完整有效名称(全名=包名.类名)

这个类型的直接父类的完整有效名(interface和java.lang.Object没有父类)

这个类型的修饰符(public,abstract,final的某个子集)

这个类型直接接口的一个有序列表

域信息(属性信息)

域名称,域类型,域修饰符

方法信息

方法名称,方法的返回类型,方法的参数数量和类型,方法的修饰符,方法的字节码,异常表

public class MethodAreaContainerDemo extends Object implements Comparable, Serializable { public int num = 10; private static String str = "测试方法区内部结构"; //构造器 //方法 public void test1(){ int count=10; System.out.println("count=" + count); } public static int test2(){ try { int i = 10; }catch (Exception e){ e.printStackTrace(); } return 20; } @Override public int compareTo(String o) { return 0; } }

class文件中的运行池常量池的理解

字节码文件,内部包含了常量池

常量池在class文件的位置

一个有效的class文件除了包含类的版本信息,字段,方法以及接口的描述信息外,还包含常量池表,包含各种字面量和对类型,域,方法的符号引用

为什么需要常量池?

一个java源文件中的类,接口,编译后产生一个字节码文件,字节码文件需要数据支持,通常这种数据会很大不能直接存储在字节码文件中,可以存在常量池中,字节码文件包含了执行常量池的引用,在动态链接的时候,会用到运行时常量池

运行时常量池

运行时常量池时方法区的一部分

常量池用于存放编译期生成的各种字面量和符号引用,这部分将在类加载后存放到方法区的运行时常量池

JVM为每一个类或接口都维护了一个常量池,池中的数据项和数组一样,通过索引进行访问

运行时常量池相比较常量池的重要特征是:具备了动态性

方法区的演进过程

java8中,出现了元空间,替换了永久代,类型信息,字段,方法,常量保存在元空间,本地内存中,字符串常量池和静态变量保存在堆中

StringTable放在堆中:因为永久代的回收效率低,在fullGC的时候才会回收,导致Stringtable的回收效率不高,而开发中会有大量的String被创建,放到堆中,可以及时回收

静态变量也保存在堆中 static Object o = new Object(); o保存在堆中,new Object()肯定也是在堆中

为什么去掉永久代:

Oracle收购了JRockit,进行融合,JRockit没有永久代,Hotspot就把永久代去掉了

元空间使用的是系统内存,改动是有必要的,因为:

为永久代设置空间大小是很难确定的,分配的不合适,容易产生OOM

Full GC时,永久代判断对象是否是垃圾,也不好判断

方法区的垃圾回收

Java虚拟机规范:提到可以不要求虚拟机在方法区中实现垃圾回收

方法区的回收比较困难,对类型的回收,条件比较苛刻,但又是必要的

方法区的垃圾回收主要回收两部分:常量池中废弃的常量和不再使用的类型

判断一个常量是否废弃相对比较简单,判断一个类是否属于不再使用的类,条件比较多:

满足上面三个条件,才允许被回收,不是必然被回收,

运行时数据区总结

相关推荐
阳光明媚sunny4 小时前
Room持久化库中,@Transaction注解的正确使用场景是?
android·数据库
北极糊的狐4 小时前
MySQL常见报错分析及解决方案总结(15)---Can’t connect to MySQL server on ‘localhost‘ (10061)
数据库·mysql
濑户川4 小时前
Django5 与 Vue3 表单交互全解析:从基础到实战
数据库
weixin_438077494 小时前
langchain官网翻译:Build a Question/Answering system over SQL data
数据库·sql·langchain·agent·langgraph
Elsa️7465 小时前
个人项目开发(1):使用Spring Secruity实现用户登录
java·后端·spring
-雷阵雨-5 小时前
MySQL——数据库操作攻略
数据库·mysql
krielwus5 小时前
Oracle ORA-01653 错误检查以及解决笔记
数据库·oracle
Wadli5 小时前
csdn| MySQL
数据库·mysql
麦芽糖02195 小时前
springboot集成ZeroMQ
java·spring boot·后端