垃圾收集器(CMS)

1. CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。CMS收集器就非常符合这类应用的需求。

CMS GC 的设计目标是避免在老年代垃圾收集时出现长时间的卡顿,主要通过两种手段来达成此目标:

  • 第一,不对老年代进行整理,而是使用空闲列表(free-lists)来管理内存空间的回收。
  • 第二,在 mark-and-sweep(标记---清除)阶段的大部分工作和应用线程一起并发执行。

也就是说,在这些阶段并没有明显的应用线程暂停。但值得注意的是,它仍然和应用线程争抢 CPU 时间。默认情况下,CMS 使用的并发线程数等于 CPU 核心数的 1/4。

通过以下选项来指定 CMS 垃圾收集器:

ruby 复制代码
-XX:+UseConcMarkSweepGC

当服务器配备多核CPU,并且优化的主要焦点在于减轻垃圾收集(GC)停顿带来的系统延迟,那么使用 CMS 是个很明智的选择。CMS通过缩短每次GC停顿的时间跨度,往往能直接增进系统的响应速度与用户体验。尽管在这一过程中,CPU资源的一部分会持续被GC活动占用,尤其在CPU资源本就紧张的场景下,CMS相较于并行GC(如Parallel GC)可能展现出较低的总体吞吐量。不过,对绝大多数应用而言,这种吞吐量与延迟的微弱变化并不构成显著影响。

实际情况中,执行老年代的并行回收阶段时,系统可能会穿插进行多次年轻代的minor GC操作。这会导致在记录full GC的日志条目中,出现minor GC事件的频繁交织,从而使得日志分析略显复杂。为确保精确理解和调试GC行为,细致地区分并解析这些日志信息变得尤为重要。

CMS GC 的几个阶段

下面我们来看一看 CMS GC 的几个阶段。

阶段 1:Initial Mark(初始标记)

这个阶段伴随着 STW 暂停。初始标记的目标是标记所有的根对象,包括根对象直接引用的对象,以及被年轻代中所有存活对象所引用的对象(老年代单独回收)。

为什么 CMS 不管年轻代了呢?前面不是刚刚完成 minor GC 嘛,再去收集年轻代估计也没什么效果。

  • 目标:快速标记所有直接可达的根对象,这是STW(Stop-The-World)操作,意味着整个应用会暂停。
  • 重要性:建立老年代垃圾回收的起点,快速识别直接关联到GC Roots的对象

阶段 2:Concurrent Mark(并发标记)

在此阶段,CMS GC 遍历老年代,标记所有的存活对象,从前一阶段"Initial Mark"找到的根对象开始算起。"并发标记"阶段,就是与应用程序同时运行,不用暂停的阶段。请注意,并非所有老年代中存活的对象都在此阶段被标记,因为在标记过程中对象的引用关系还在发生变化。

在上面的示意图中,"当前处理的对象"的一个引用就被应用线程给断开了,即这个部分的对象关系发生了变化(下面会讲如何处理)。

  • 行为:在应用继续运行的同时,CMS遍历老年代,标记所有可达对象。此阶段可以较长时间运行,且由于应用状态仍在变化,标记可能不完全准确。
  • 挑战:处理并发标记期间对象引用关系的变化,通过卡片标记(Card Marking)机制记录变化。

阶段 3:Concurrent Preclean(并发预清理)

此阶段同样是与应用线程并发执行的,不需要停止应用线程。

因为前一阶段"并发标记"与程序并发运行,可能有一些引用关系已经发生了改变。如果在并发标记过程中引用关系发生了变化,JVM 会通过"Card(卡片)"的方式将发生了改变的区域标记为"脏"区,这就是所谓的"卡片标记(Card Marking)"。

在预清理阶段,这些脏对象会被统计出来,它们所引用的对象也会被标记。此阶段完成后,用以标记的 card 也就会被清空。

此外,本阶段也会进行一些必要的细节处理,还会为 Final Remark 阶段做一些准备工作。

阶段 4:Concurrent Abortable Preclean(可取消的并发预清理)

此阶段也不停止应用线程。本阶段尝试在 STW 的 Final Remark 阶段 之前尽可能地多做一些工作。本阶段的具体时间取决于多种因素,因为它循环做同样的事情,直到满足某个退出条件(如迭代次数,有用工作量,消耗的系统时间等等)。

此阶段可能显著影响 STW 停顿的持续时间,并且有许多重要的配置选项和失败模式。

阶段 5:Final Remark(最终标记)

最终标记阶段是此次 GC 事件中的第二次(也是最后一次)STW 停顿。

本阶段的目标是完成老年代中所有存活对象的标记. 因为之前的预清理阶段是并发执行的,有可能 GC 线程跟不上应用程序的修改速度。所以需要一次 STW 暂停来处理各种复杂的情况。

通常 CMS 会尝试在年轻代尽可能空的情况下执行 Final Remark 阶段,以免连续触发多次 STW 事件。

在 5 个标记阶段完成之后,老年代中所有的存活对象都被标记了,然后 GC 将清除所有不使用的对象来回收老年代空间。

阶段 6:Concurrent Sweep(并发清除)

此阶段与应用程序并发执行,不需要 STW 停顿。JVM 在此阶段删除不再使用的对象,并回收它们占用的内存空间。

  • 无停顿:与应用并发执行,清除已被标记为不再使用的对象,回收空间。

  • 特点:不进行内存压缩,可能导致内存碎片。

阶段 7:Concurrent Reset(并发重置)

此阶段与应用程序并发执行,重置 CMS 算法相关的内部数据,为下一次 GC 循环做准备。

总之,CMS 垃圾收集器在减少停顿时间上做了很多复杂而有用的工作,用于垃圾回收的并发线程执行的同时,并不需要暂停应用线程。当然,CMS 也有一些缺点,其中最大的问题就是老年代内存碎片问题(因为不压缩),在某些情况下 GC 会造成不可预测的暂停时间,特别是堆内存较大的情况下。

CMS优缺点

CMS最主要的优点在名字上已经体现出来:并发收集、低停顿,一些官方公开文档里面也称之为"并发低停顿收集器"(Concurrent Low Pause Collector)。CMS收集器是 HotSpot虚拟机追求低停顿的第一次成功尝试,但是它还远达不到完美的程度,至少有以下三个明显的缺点:

  1. CMS收集器对处理器资源非常敏感。事实上,面向并发设计的程序都对处理器资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程而导致应用程序变慢,降低总吞吐量。CMS默认启动的回收线程数是(处理器核心数量+3)/4,也就是说,如果处理器核心数在四个或以上,并发回收时垃圾收集线程只占用不超过25%的处理器运算资源,并且会随着处理器核心数量的增加而下降。但是当处理器核心数量不足四个时,CMS对用户程序的影响就可能变得很大。如果应用本来的处理器负载就很高,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然大幅降低。
  2. 由于CMS收集器无法处理"浮动垃圾"(Floating Garbage),有可能出现"Concurrent Mode Failure"失败进而导致另一次完全"Stop The World"的Full GC的产生。在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为"浮动垃圾"。同样也是由于在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间提供给用户线程使用,因此CMS收集器不能像其他收集器那样等待到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。在JDK5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在实际应用中老年代增长并不是太快,可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高CMS的触发百分比,降低内存回收频率,获取更好的性能。到了JDK6时,CMS收集器的启动阈值就已经默认提升至92%。但这又会更容易面临另一种风险:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次"并发失败"(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用SerialOld收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了。所以参数-XX:CMSInitiatingOccupancyFraction设置得太高将会很容易导致大量的并发失败产生,性能反而降低,用户应在生产环境中根据实际应用情况来权衡设置。
  3. CMS是一款基于"标记-清除"算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次FullGC的情况。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认是开启),用于在CMS收集器不得不进行FullGC时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,(在Shenandoah和ZGC出现前)是无法并发的。这样空间碎片问题是解决了,但停顿时间又会变长,因此虚拟机设计者们还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction(此参数从JDK9开始废弃),这个参数的作用是要求CMS收集器在执行过若干次(数量由参数值决定)不整理空间的FullGC之后,下一次进入FullGC前会先进行碎片整理(默认值为0,表示每次进入FullGC时都进行碎片整理)。
  • 吞吐量低: 低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
  • 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
  • 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。

CMS触发的CMS GC

CMS垃圾收集器的触发条件,主要包括foreground collector和background collector两种模式。

  1. 如果没有设置-XX:+UseCMSInitiatingOccupancyOnly,虚拟机会根据收集的数据决定是否触发(建议线上环境带上这个参数,不然会加大问题排查的难度)。
  2. 老年代使用率达到阈值 CMSInitiatingOccupancyFraction,默认92%。
  3. 永久代的使用率达到阈值 CMSInitiatingPermOccupancyFraction,默认92%,前提是开启 CMSClassUnloadingEnabled。
  4. 新生代的晋升担保失败。

CMS触发的Full GC

虽然CMS设计目标是尽量减少应用程序的停顿时间,但在某些情况下,它仍然可能触发Full GC,这些情况包括:

  1. 内存不足: 当老年代空间不足,且无法通过CMS收集足够的空间来满足新的内存分配需求时,可能会触发Full GC。这种Full GC实际上是串行收集器执行的,会导致较长的应用暂停时间。
  2. 并发模式失败(Concurrent Mode Failure): 如果在并发清除阶段,老年代的增长速度过快,以至于CMS垃圾收集器无法跟上,就会发生并发模式失败。这时为了防止内存耗尽,JVM会放弃当前的CMS周期,转而使用Serial Old收集器进行一次Full GC。
  3. 元数据空间满: 虽然不直接与CMS收集老年代相关,但如果永久代(Java 7及以前)或元数据区(Java 8及以后)空间不足,也会触发Full GC。这是因为这些区域的回收通常与老年代回收绑定在一起进行。

CMS GC要决定是否在full GC时做压缩,会依赖几个条件。其中:

  1. 第一种条件,UseCMSCompactAtFullCollection 与 CMSFullGCsBeforeCompaction 是搭配使用的;前者目前默认就是true了,也就是关键在后者上。
  2. 第二种条件是用户调用了System.gc(),而且DisableExplicitGC没有开启。
  3. 第三种条件是young gen报告接下来如果做增量收集会失败;简单来说也就是young gen预计old gen没有足够空间来容纳下次young GC晋升的对象。
  4. 上述三种条件的任意一种成立都会让CMS决定这次做full GC时要做压缩。

CMSFullGCsBeforeCompaction 说的是,在上一次CMS并发GC执行过后,到底还要再执行多少次full GC才会做压缩。默认是0,也就是在默认配置下每次CMS GC顶不住了而要转入full GC的时候都会做压缩。 把CMSFullGCsBeforeCompaction配置为10,就会让上面说的第一个条件变成每隔10次真正的full GC才做一次压缩(而不是每10次CMS并发GC就做一次压缩,目前VM里没有这样的参数)。这会使full GC更少做压缩,也就更容易使CMS的old gen受碎片化问题的困扰。 本来这个参数就是用来配置降低full GC压缩的频率,以减少某些full GC的暂停时间。CMS回退到full GC时用的算法是mark-sweep-compact,但compaction是可选的,不做的话碎片化会严重些,但这次full GC的暂停时间会短些;这是个取舍。

我们在 GC 调优时应该尽可能的避免压缩式的 Full GC,因为其使用的是 Serial Old GC 类似算法,它是单线程对全堆以及 metaspace 进行回收,STW 的时间会特别长,对业务系统的可用性影响比较大。

CMS相关参数及其说明:

  1. 启用CMS收集器
    • -XX:+UseConcMarkSweepGC:启用CMS垃圾收集器。
  1. 设置初始堆大小和最大堆大小
    • -Xms:设置JVM初始堆大小。
    • -Xmx:设置JVM最大堆大小。这两个参数间接影响CMS的行为,因为堆大小直接影响垃圾回收的频率和效率。
  1. CMS相关调优参数
    1. JDK1.5时,CMSInitiatingOccupancyFraction的默认值是68;JDK1.6时,默认值调高为92。
    • -XX:CMSInitiatingOccupancyFraction=:指定老年代空间使用率达到多少百分比时启动CMS收集,减小该值可以使CMS更早地触发,避免内存不足的情况,但可能导致更多的CPU时间用于GC。
    • -XX:+UseCMSInitiatingOccupancyOnly:启用该参数后,上述设置的CMSInitiatingOccupancyFraction值将严格遵守,即使发生晋升失败也不会自动降低阈值。
  1. 并发收集线程数
    • -XX:ParallelCMSThreads=:设置CMS并发收集时使用的线程数。根据你的CPU核心数量适当调整,以平衡GC效率和应用性能。
  1. CMS收集过程中的参数
    • -XX:CMSFullGCsBeforeCompaction=0:控制多少次CMS GC后进行一次内存压缩(默认是0,意味着不进行压缩,因为CMS不自带压缩功能)。但可以通过与其他参数结合模拟压缩效果。
  1. 年轻代GC策略
    • 虽然不是直接CMS参数,但年轻代的配置(如-XX:NewRatio, -XX:SurvivorRatio, -XX:NewSize, -XX:MaxNewSize等)也会影响整体GC性能,包括CMS的触发频率。
  1. 其他高级调优参数
    • -XX:CMSMaxAbortablePrecleanTime=:设定CMS预清理阶段的最大时间。
    • -XX:CMSWaitDuration=:设置CMS线程等待更多老年代空间变为空闲的时间。

周期性Old GC,执行的逻辑也叫Background Collect,对老年代进行回收,在GC日志中比较常见,由后台线程ConcurrentMarkSweepThread循环判断(默认2s)是否需要触发。

CMSInitiatingOccupancyFraction 与UseCMSInitiatingOccupancyOnly

  1. CMSInitiatingOccupancyFraction JDK1.5时,默认值是68;JDK1.6时,默认值调高为92。
  2. -XX:CMSInitiatingOccupancyFraction=:指定老年代空间使用率达到多少百分比时启动CMS收集,减小该值可以使CMS更早地触发,避免内存不足的情况,但可能导致更多的CPU时间用于GC。
  3. -XX:+UseCMSInitiatingOccupancyOnly:启用该参数后,上述设置的CMSInitiatingOccupancyFraction值将严格遵守,即使发生晋升失败也不会自动降低阈值。
ruby 复制代码
-XX:CMSInitiatingOccupancyFraction=70 -XX:+UseCMSInitiatingOccupancyOnly 

CMSInitiatingOccupancyFraction=70是指设定CMS在对内存占用率达到70%的时候开始GC(因为CMS会有浮动垃圾,所以一般都较早启动GC),UseCMSInitiatingOccupancyOnly 只是用设定的回收阈值(上面指定的70%),如果不指定,JVM仅在第一次使用设定值,后续则自动调整.

-XX:+CMSScavengeBeforeRemark

在执行CMS(Concurrent Mark-Sweep)垃圾收集之前,主动触发一次针对年轻代(Young Generation)的垃圾收集(简称YGC),其主要目的是为了削减老年代(Old Generation)对年轻代的引用数量。这样做可以有效减轻CMS过程中的重新标记(Remark)阶段负担,因为减少的跨代引用有助于提升标记过程的效率。值得注意的是,CMS垃圾回收周期中大约80%的总时间常常消耗在重新标记阶段,因此,采取此策略对于优化整体垃圾收集性能、缩短停顿时间尤为重要。

CMSInitiatingOccupancyFraction默认值问题

JVM CMS垃圾收集器相关一个重要参数CMSInitiatingOccupancyFraction,默认值具体是多少,众说纷纭。

-XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发CMS GC,百分比格式。

1. 计算CMSInitiatingOccupancyFraction

具体还是要看openJDK的代码(基于JDK8):

scss 复制代码
// hotspot/src/share/vm/gc_implementation/concurrentMarkSweep/concurrentMarkSweepGeneration.cpp

// The field "_initiating_occupancy" represents the occupancy percentage
// at which we trigger a new collection cycle.  Unless explicitly specified
// via CMSInitiatingOccupancyFraction (argument "io" below), it
// is calculated by:
//
//   Let "f" be MinHeapFreeRatio in
//
//    _intiating_occupancy = 100-f +
//                           f * (CMSTriggerRatio/100)
//   where CMSTriggerRatio is the argument "tr" below.
//
// That is, if we assume the heap is at its desired maximum occupancy at the
// end of a collection, we let CMSTriggerRatio of the (purported) free
// space be allocated before initiating a new collection cycle.
//
void ConcurrentMarkSweepGeneration::init_initiating_occupancy(intx io, uintx tr) {
  assert(io <= 100 && tr <= 100, "Check the arguments");
  if (io >= 0) {
    _initiating_occupancy = (double)io / 100.0;
  } else {
    _initiating_occupancy = ((100 - MinHeapFreeRatio) +
                             (double)(tr * MinHeapFreeRatio) / 100.0)
                            / 100.0;
  }
}

其中:

io参数是:输入的CMSInitiatingOccupancyFraction值

tr参数是:CMSTriggerRatio的值,JDK8默认是80

代码逻辑比较简单:

  • 若指定的CMSInitiatingOccupancyFraction为正值,例如80,则老年代到达80%就会触发FullGC。
  • CMSInitiatingOccupancyFraction默认为-1,走下面的分支。基于JDK8,结合其余2个参数MinHeapFreeRatio默认值40、CMSTriggerRatio默认值80,计算出的默认百分比是92。若MinHeapFreeRatio默认值0、CMSTriggerRatio默认值80,则计算出的默认百分比是100。
2. MinHeapFreeRatio的默认值到底是多少?0还是40?

这里有个疑问点:MinHeapFreeRatio 的默认值到底是多少?0还是40?

通过-XX:+PrintFlagsFinal参数打印出来的MinHeapFreeRatio默认值是0:

ini 复制代码
uintx MinHeapFreeRatio                          = 0                                   {manageable}

再借助jinfo -flag MinHeapFreeRatio PID打印出来的MinHeapFreeRatio默认值都是0。

ini 复制代码
jinfo -flag MinHeapFreeRatio 18896
-XX:MinHeapFreeRatio=0

但是从源码上看MinHeapFreeRatio默认值从JDK6开始默认值就一直是40了。

ruby 复制代码
// hotspot/src/share/vm/runtime/globals.hpp
manageable(uintx, MinHeapFreeRatio, 40,                                   \
        "The minimum percentage of heap free after GC to avoid expansion."\
        " For most GCs this applies to the old generation. In G1 and"     \
        " ParallelGC it applies to the whole heap.")                      \
                                                                          \
manageable(uintx, MaxHeapFreeRatio, 70,                                   \
        "The maximum percentage of heap free after GC to avoid shrinking."\
        " For most GCs this applies to the old generation. In G1 and"     \
        " ParallelGC it applies to the whole heap.")   

说明MinHeapFreeRatio在哪里进行修改了!

查了一下openJDK相关的代码,发现有这个UseAdaptiveSizePolicy配置,打开时,会默认将MinHeapFreeRatio设置为0,MaxHeapFreeRatio设置为100。

而UseAdaptiveSizePolicy默认就是开启的!这也就能解释为什么代码中默认值是40,而实际运行PrintFlagsFinal时则是0。

ini 复制代码
bool UseAdaptiveSizePolicy                     = true                                {product}
scss 复制代码
// hotspot/src/share/vm/runtime/arguments.cpp
if (UseAdaptiveSizePolicy) {
  // We don't want to limit adaptive heap sizing's freedom to adjust the heap
  // unless the user actually sets these flags.
  if (FLAG_IS_DEFAULT(MinHeapFreeRatio)) {
    FLAG_SET_DEFAULT(MinHeapFreeRatio, 0);
    _min_heap_free_ratio = MinHeapFreeRatio;
  }
  if (FLAG_IS_DEFAULT(MaxHeapFreeRatio)) {
    FLAG_SET_DEFAULT(MaxHeapFreeRatio, 100);
    _max_heap_free_ratio = MaxHeapFreeRatio;
  }
}

关闭UseAdaptiveSizePolicy,然后再查看MinHeapFreeRatio:

ruby 复制代码
java -XX:+PrintFlagsFinal -XX:-UseAdaptiveSizePolicy -version | grep MinHeapFreeRatio

结果符合预期了:

ini 复制代码
uintx MinHeapFreeRatio                          = 40                                  {manageable}
3. UseAdaptiveSizePolicy和CMS

从网上查阅到的信息是,对于JDK8,当启用CMS时,无论UseAdaptiveSizePolicy设置成什么值,都会关闭UseAdaptiveSizePolicy,自然而然MinHeapFreeRatio就不会被调整为0了。

ruby 复制代码
java -XX:+PrintFlagsFinal -XX:+UseConcMarkSweepGC -version | grep -E "MinHeapFreeRatio | UseAdaptiveSizePolicy"
java -XX:+PrintFlagsFinal -XX:+UseConcMarkSweepGC -XX:+UseAdaptiveSizePolicy -version | grep -E "MinHeapFreeRatio | UseAdaptiveSizePolicy"

结果符合预期:UseAdaptiveSizePolicy关闭,且MinHeapFreeRatio变为默认值40。

ini 复制代码
uintx MinHeapFreeRatio                          = 40                                  {manageable}
 bool UseAdaptiveSizePolicy                     = false                               {product}
arduino 复制代码
Java HotSpot(TM) 64-Bit Server VM warning: disabling UseAdaptiveSizePolicy; it is incompatible with UseConcMarkSweepGC.
uintx MinHeapFreeRatio                          = 40                                  {manageable}
 bool UseAdaptiveSizePolicy                    := false                               {product}
4. 结论

对于JDK6/JDK7/JDK8,CMS的参数:CMSInitiatingOccupancyFraction,肯定需要启用CMS(-XX:+UseConcMarkSweepGC),此时UseAdaptiveSizePolicy会强制关闭,所以MinHeapFreeRatio的默认值就是40,CMSInitiatingOccupancyFraction默认值就是92,百分比类型。不存在100%这样的情况。

相关推荐
小蜗牛慢慢爬行1 小时前
如何在 Spring Boot 微服务中设置和管理多个数据库
java·数据库·spring boot·后端·微服务·架构·hibernate
wm10432 小时前
java web springboot
java·spring boot·后端
龙少95433 小时前
【深入理解@EnableCaching】
java·后端·spring
溟洵5 小时前
Linux下学【MySQL】表中插入和查询的进阶操作(配实操图和SQL语句通俗易懂)
linux·运维·数据库·后端·sql·mysql
SomeB1oody8 小时前
【Rust自学】6.1. 定义枚举
开发语言·后端·rust
SomeB1oody8 小时前
【Rust自学】5.3. struct的方法(Method)
开发语言·后端·rust
啦啦右一9 小时前
Spring Boot | (一)Spring开发环境构建
spring boot·后端·spring
森屿Serien9 小时前
Spring Boot常用注解
java·spring boot·后端
盛派网络小助手11 小时前
微信 SDK 更新 Sample,NCF 文档和模板更新,更多更新日志,欢迎解锁
开发语言·人工智能·后端·架构·c#
∝请叫*我简单先生11 小时前
java如何使用poi-tl在word模板里渲染多张图片
java·后端·poi-tl