Golang GC 介绍

文章目录

  • 0.前言
  • 1.发展史
  • 2.并发三色标记清除和混合写屏障
    • 2.1 三色标记
    • 2.2 并发标记问题
    • 2.3 屏障机制
      • Dijkstra 插入写屏障
      • Yuasa 删除写屏障
      • 混合写屏障
  • 3.GC 过程
  • 4.GC 触发时机
  • 5.哪里记录了对象的三色状态?
  • 6.如何观察 GC?
    • 方式1:GODEBUG=gctrace=1
    • 方式2: go tool trace
    • 方式3:debug.ReadGCStats
    • 方式4:runtime.ReadMemStats
  • 参考文献

0.前言

GC 全称 Garbage Collection,目前主流的垃圾回收算法有两类,分别是追踪式垃圾回收算法(Tracing garbage collection)和引用计数法( Reference counting )。

Golang 使用的三色标记法属于追踪式垃圾回收算法的一种。

追踪式算法的核心思想是判断一个对象是否可达,因为一旦这个对象不可达就可以立刻被 GC 回收了。

1.发展史

v1.1 标记清除法,整个过程都需要 STW。gc pause 数百 ms 级。

v1.3 标记清除法,标记过程仍需要 STW,清除过程并行化。gc pause 百 ms 级。

v1.5 并发三色标记清除和写屏障。仅在堆空间启动插入写屏障,全部扫描后需要 STW 重新扫描栈空间。gc pause 10 ms 级。

v1.8 并发三色标记清除和混合写屏障。仅在堆空间启动插入写屏障,全部扫描后不需要 STW 重新扫描栈空间。gc pause 0.5 ms 级。

混合写屏障指 Dijkstra 插入写屏障和 Yuasa 删除写屏障。

2.并发三色标记清除和混合写屏障

2.1 三色标记

三色标记算法将程序中的对象分成白色、黑色和灰色。

  • 白色对象(可能死亡):未被回收器访问到的对象。在回收开始阶段,所有对象均为白色,当回收结束后,白色对象均不可达。
  • 灰色对象(波面):已被回收器访问到的对象,但回收器需要对其中的一个或多个指针进行扫描,因为他们可能还指向白色对象。
  • 黑色对象(确定存活):已被回收器访问到的对象,其中所有字段都已被扫描,黑色对象中任何一个指针都不可能直接指向白色对象。

回收器首先将所有对象标记成白色,然后从根对象集合出发,逐步把所有可达的对象变成灰色再到黑色,最终所有的白色对象都是不可达对象,即垃圾对象。

具体实现:

  • 将所有对象标记为白色。
  • 从根节点集合出发,将第一次遍历到的节点标记为灰色放入集合列表中。
  • 遍历灰色集合,将灰色节点遍历到的白色节点标记为灰色,并把灰色节点标记为黑色。
  • 重复,上一步骤,直到灰色对象队列为空。
  • 剩下的所有白色对象都是垃圾对象。

在这里插入图片描述

根对象是垃圾回收器在标记过程最先检查的对象,包括:

  • 全局变量:程序在编译期就能确定的那些存在于程序整个生命周期的变量。
  • 执行栈:每个 goroutine 都包含自己的执行栈,这些执行栈上包含栈上的变量及指向分配的堆内存区块的指针。
  • 寄存器:寄存器的值可能表示一个指针,参与计算的这些指针可能指向某些赋值器分配的堆内存。

2.2 并发标记问题

在垃圾回收过程中,标记操作与程序的执行可以同时进行,故称为并发三色标记

并发标记可以提高程序性能,但是存在问题。

假设有三个对象,A、B 和 C,标记过程中状态如下:

在这里插入图片描述
赋值器并发地将黑色对象 C 指向了白色对象 B,并移除灰色对象 A 对白色对象 B 的引用。

在这里插入图片描述
然后继续扫描灰色对象 A,那么白色对象 B 永远不会被标记为黑色对象了(回收器不会重新扫描黑色对象),进而对象 B 被误回收。

在这里插入图片描述

因为漏标记导致回收了仍在使用的对象。

垃圾回收的原则是不应出现对象的丢失(内存泄漏),也不应错误地回收还不需要回收的对象(漏标记)。如果同时满足下面两个条件会破坏回收器的正确性:

  • 条件 1: 赋值器修改对象,导致某一黑色对象引用白色对象。
  • 条件 2: 从灰色对象出发,到达白色对象且未经访问过的路径被赋值器破坏。

上面的例子就是因为同时满足了条件 1 和条件 2 导致并发标记过程漏标了仍在使用的 B 对象。

可能的解决方法: 整个过程 STW,因为这种做法对用户程序影响较大,由此引入了屏障机制。

2.3 屏障机制

使用屏障机制可以使得用户程序和三色标记过程并发执行,我们只需要达成下列任意一种三色不变性:

  • 强三色不变性:黑色对象永远不会指向白色对象。
  • 弱三色不变性:黑色对象指向的白色对象至少包含一条由灰色对象经过白色对象的可达路径。

Go 使用写屏障避免漏标记对象。

这里的写屏障是指由编译器生成的一小段代码,在 GC 时对指针操作前执行的一小段代码(和 CPU 中维护内存一致性的写屏障不太一样)。

写屏障有两种:Dijkstra 插入写屏障和 Yuasa 删除写屏障

Dijkstra 插入写屏障

Dijkstra 插入写屏障避免了前面提到的条件 1,黑色对象不会引用白色对象。

当一个对象引用另外一个对象时,将另外一个对象标记为灰色。

// Dijkstra 插入屏障
func DijkstraWritePointer(slot *unsafe.Pointer, ptr unsafe.Pointer) {shade(ptr) // 先将新下游对象 ptr 标记为灰色*slot = ptr
}

尽管 Dijkstra 插入写屏障可以实现垃圾回收和用户程序的并发执行,但是它存在两个缺点。

一方面它是一种比较保守的垃圾回收方法,在一次回收过程中可能会残留一部分对象没有回收成功,只有在下一个回收过程中才会被回收。

以下图为例,用户程序 Mutator 将对象 A 原本指向 B 对象的指针改成指向 C 对象,尽管在修改后 B 对象已经是一个垃圾对象,但是它在本轮垃圾回收过程中不会被回收。
在这里插入图片描述
另外一个缺点在于栈上的对象也是根对象,Dijkstra 插入写屏障要么在用户程序执行内存写操作时为栈对象插入写屏障,要么在一轮三色标记完成后使用 STW 重新对栈对象进行三色标记。前者会降低栈空间的响应速度,后者会暂停用户程序。

Go 1.5 选择使用 STW 重新对栈对象进行三色标记。

Yuasa 删除写屏障

Yuasa 删除写屏障避免了前面提到的条件2,防止丢失灰色对象到白色对象的可达路径。

// 黑色赋值器 Yuasa 屏障
func YuasaWritePointer(slot *unsafe.Pointer, ptr unsafe.Pointer) {shade(*slot) // 先将旧下游对象 slot 标记为灰色*slot = ptr
}

为了防止丢失从灰色对象到白色对象的路径,在 ptr 被赋值到 *slot 前,先将 *slot 标记为灰色。一句话解释就是当删除对象 A 指向对象 B 的指针时,将被删除的对象 B 标记为灰色。

下图简单绘制了 Yuasa 删除写屏障是如何保证用户程序 Mutator 和垃圾回收器 Collector 的并发执行的:

  • 第二步中 Mutator 将对象 A 原本指向对象 B 的指针指向 C,由于对象B本身就是灰色的,因此不需要对它重新着色。
  • 第三步中 Mutator 删除了对象 B 指向对象 C 的指针,删除写屏障将下游对象 C 标记为灰色。
    在这里插入图片描述
    Yuasa 删除写屏障和 Dijkstra 插入写屏障相比优点在于不需要在一轮三色标记后对栈空间上的对象进行重新扫描。缺点在于Collector 会悲观地认为所有被删除的对象都可能被黑色对象引用,所以将被删除的对象置灰。

混合写屏障

在 Go 1.8 引入混合写屏障(Hybrid Write Barrier)之前,由于 GC Root 对象包括了栈对象,如果运行时在所有 GC Root 对象上开启插入写屏障意味着需要在数量庞大的 Goroutine 的栈上都开启 Dijkstra 写屏障从而严重影响用户程序的性能。

之前的做法是标记阶段结束后暂停整个程序,对栈上对象重新进行三色标记。如果 Goroutine 较多的话,对栈对象 re-scan 这一步需要耗费 10~100 ms。

Go 1.8 为了减少标记终止阶段对栈对象的重扫成本,将 Dijkstra 插入写屏障和 Yuasa 删除写屏障进行混合,形成混合写屏障。

// 混合写屏障
func HybridWritePointerSimple(slot *unsafe.Pointer, ptr unsafe.Pointer) {shade(*slot)shade(ptr)*slot = ptr
}

注意:混合写屏障也是仅在堆空间启动的,防止降低栈空间的运行效率。

混合写屏障逻辑如下:

  • GC 开始时将栈上所有对象标记为黑色,无须 STW
  • GC 期间在栈上创建的新对象均标记为黑色
  • 将被删除的下游对象标记为灰色
  • 将被添加的下游对象标记为灰色

3.GC 过程

Golang GC 分为四个阶段:清除终止、标记、标记终止和清除。

(1)清除终止(Sweep Termination)

  • 暂停程序,所有处理器在这时会进入安全点(Safe point)。
  • 如果当前 GC 是强制触发的,还需要处理未被清理的内存管理单元。

(2)标记(Mark)

  • 将状态切换至_GCmark、开启写屏障、用户程序协助(Mutator Assists)并将根对象入队。
  • 恢复执行程序,标记进程和用于协助的用户程序会开始并发标记内存中的对象,写屏障会将被覆盖的指针和新指针都标记成灰色,而所有新创建的对象都会被直接标记成黑色。
  • 开始扫描根对象,包括所有 Goroutine 的栈、全局对象以及不在堆中的运行时数据结构,扫描 Goroutine 栈期间会暂停当前处理器。
  • 依次处理灰色队列中的对象,将对象标记成黑色并将它们指向的对象标记成灰色。
  • 使用分布式的终止算法检查剩余的工作,发现标记阶段完成后进入标记终止阶段。

(3)标记终止(Mark Termination)

  • 暂停程序,将状态切换至_GCmarktermination并关闭辅助标记的用户程序。
  • 清理处理器上的线程缓存。

(4)清除(Sweep)

  • 将状态切换至_GCoff,关闭混合写屏障。
  • 恢复用户程序,所有新创建的对象标记为白色。
  • 后台并发清理所有内存管理单元 span,当 Goroutine 申请新的内存管理单元时就会触发清除。

具体而言,各个阶段的触发函数分别为:

在这里插入图片描述

在 GC 过程中会有两种后台任务(G),包括标记任务和清除任务。可以同时执行的标记任务约是 P 数量的四分之一,即 Go 所说的 25% CPU 用于 GC 的依据。清理除任务会在程序启动后运行,清除阶段时被唤醒。

4.GC 触发时机

触发 GC 的方式有两种:手动触发和自动触发。

手动触发调用runtime.GC()函数可以强制触发 GC,该方法在调用时会阻塞调用方直到 GC 完成。在 GC 期间也可能会通过 STW 暂停整个程序。

自动触发有两种:

  • 条件触发:当新分配的内存达到上次 GC 结束时存活对象占用内存的某个比例时触发 GC,该比例可以通过环境变量GOGC调整,默认值为 100,即新增 100% 的堆内存会触发 GC。
  • 定时触发。使用系统监控协程 sysmon,当超过一段时间(由runtime.forcegcperiod变量控制,默认两分钟)没有产生任何 GC 时,强制触发 GC。

运行时会通过如下所示的runtime.gcTrigger.test方法决定是否需要触发 GC。

// test reports whether the trigger condition is satisfied, meaning
// that the exit condition for the _GCoff phase has been met. The exit
// condition should be tested when allocating.
func (t gcTrigger) test() bool {if !memstats.enablegc || panicking.Load() != 0 || gcphase != _GCoff {return false}switch t.kind {case gcTriggerHeap:// Non-atomic access to gcController.heapLive for performance. If// we are going to trigger on this, this thread just// atomically wrote gcController.heapLive anyway and we'll see our// own write.trigger, _ := gcController.trigger()return gcController.heapLive.Load() >= triggercase gcTriggerTime:if gcController.gcPercent.Load() < 0 {return false}lastgc := int64(atomic.Load64(&memstats.last_gc_nanotime))return lastgc != 0 && t.now-lastgc > forcegcperiodcase gcTriggerCycle:// t.n > work.cycles, but accounting for wraparound.return int32(t.n-work.cycles.Load()) > 0}return true
}

满足触发 GC 的基本条件:允许垃圾收集、程序没有崩溃并且 GC 处于清除阶段,即 GC 状态能为_GCoff

// 允许垃圾回收
memstats.enablegc
// 程序没有 panic
panicking == 0
// 处于 _Gcoff 阶段
gcphase == _GCoff

对应的触发时机包括:

// 堆内存达到一定阈值
gcTriggerHeap
// 距离上一次垃圾回收超过一定时间,间隔由 runtime.forcegcperiod 变量控制,默认为 2 分钟
gcTriggerTime
// 如果当前没有启动 GC 则开始新一轮的 GC。手动调用 runtime.GC() 函数会走到该分支
gcTriggerCycle

5.哪里记录了对象的三色状态?

并没有真正的三个集合来分别装三色对象。

Go 的对象分配在 span 中,span 里有一个字段是 gcmarkBits。标记阶段里面每个 bit 代表一个 slot 已被标记。

白色对象 bit 为 0,灰色或黑色为 1。
在这里插入图片描述
每个 P(Processor) 都有 wbBuf 和 gcWork 以及全局的 workbuf 标记队列。队列中的指针为灰色对象,表示已标记待扫描。

从队列中出来并把其引用对象入队的为黑色对象,表示已标记已扫描。

在这里插入图片描述

6.如何观察 GC?

以下面的程序为例,使用四种不同的方式来介绍如何观察 GC。

package mainfunc allocate() {_ = make([]byte, 1<<20)
}func main() {for n := 1; n < 1000; n++ {allocate()}
}

方式1:GODEBUG=gctrace=1

设置环境变量 GODEBUG=gctrace=1,执行程序时可以看到 GC 日志。

go build -o main
GODEBUG=gctrace=1 ./maingc 1 @0.000s 2%: 0.009+0.23+0.004 ms clock, 0.11+0.083/0.019/0.14+0.049 ms cpu, 4->6->2 MB, 5 MB goal, 12 P
scvg: 8 KB released
scvg: inuse: 3, idle: 60, sys: 63, released: 57, consumed: 6 (MB)
gc 2 @0.001s 2%: 0.018+1.1+0.029 ms clock, 0.22+0.047/0.074/0.048+0.34 ms cpu, 4->7->3 MB, 5 MB goal, 12 P
scvg: inuse: 3, idle: 60, sys: 63, released: 56, consumed: 7 (MB)
gc 3 @0.003s 2%: 0.018+0.59+0.011 ms clock, 0.22+0.073/0.008/0.042+0.13 ms cpu, 5->6->1 MB, 6 MB goal, 12 P
scvg: 8 KB released
scvg: inuse: 2, idle: 61, sys: 63, released: 56, consumed: 7 (MB)
gc 4 @0.003s 4%: 0.019+0.70+0.054 ms clock, 0.23+0.051/0.047/0.085+0.65 ms cpu, 4->6->2 MB, 5 MB goal, 12 P
scvg: 8 KB released
scvg: inuse: 3, idle: 60, sys: 63, released: 56, consumed: 7 (MB)
scvg: 8 KB released
scvg: inuse: 4, idle: 59, sys: 63, released: 56, consumed: 7 (MB)
gc 5 @0.004s 12%: 0.021+0.26+0.49 ms clock, 0.26+0.046/0.037/0.11+5.8 ms cpu, 4->7->3 MB, 5 MB goal, 12 P
scvg: inuse: 5, idle: 58, sys: 63, released: 56, consumed: 7 (MB)
gc 6 @0.005s 12%: 0.020+0.17+0.004 ms clock, 0.25+0.080/0.070/0.053+0.051 ms cpu, 5->6->1 MB, 6 MB goal, 12 P
scvg: 8 KB released
scvg: inuse: 1, idle: 62, sys: 63, released: 56, consumed: 7 (MB)

在这个日志中可以观察到两类不同的信息:

gc 1 @0.000s 2%: 0.009+0.23+0.004 ms clock, 0.11+0.083/0.019/0.14+0.049 ms cpu, 4->6->2 MB, 5 MB goal, 12 P
gc 2 @0.001s 2%: 0.018+1.1+0.029 ms clock, 0.22+0.047/0.074/0.048+0.34 ms cpu, 4->7->3 MB, 5 MB goal, 12 P
...

以及:

scvg: 8 KB released
scvg: inuse: 3, idle: 60, sys: 63, released: 57, consumed: 6 (MB)
scvg: inuse: 3, idle: 60, sys: 63, released: 56, consumed: 7 (MB)
...

含义如下表所示:

字段含义
gc 2第二个 GC 周期
0.001程序开始后的 0.001 秒
2%该 GC 周期中 CPU 的使用率
0.018标记开始时, STW 所花费的时间(wall clock)
1.1标记过程中,并发标记所花费的时间(wall clock)
0.029标记终止时, STW 所花费的时间(wall clock)
0.22标记开始时, STW 所花费的时间(cpu time)
0.047标记过程中,标记辅助所花费的时间(cpu time)
0.074标记过程中,并发标记所花费的时间(cpu time)
0.048标记过程中,GC 空闲的时间(cpu time)
0.34标记终止时, STW 所花费的时间(cpu time)
4标记开始时,堆的大小的实际值
7标记结束时,堆的大小的实际值
3标记结束时,标记为存活的对象大小
5标记结束时,堆的大小的预测值
12P 的数量

wall clock 是指开始执行到完成所经历的实际时间,包括其他程序和本程序所消耗的时间; cpu time 是指特定程序使用 CPU 的时间。

wall clock < cpu time: 充分利用多核
wall clock ≈ cpu time: 未并行执行
wall clock > cpu time: 多核优势不明显

对于运行时向操作系统申请内存产生的垃圾回收(向操作系统归还多余的内存):

scvg: 8 KB released
scvg: inuse: 3, idle: 60, sys: 63, released: 57, consumed: 6 (MB)

含义由下表所示:

字段含义
8 KB released向操作系统归还了 8 KB 内存
3已经分配给用户代码、正在使用的总内存大小 (MB)
60空闲以及等待归还给操作系统的总内存大小(MB)
63通知操作系统中保留的内存大小(MB)
57已经归还给操作系统的(或者说还未正式申请)的内存大小(MB)
6已经从操作系统中申请的内存大小(MB)

方式2: go tool trace

go tool trace 的主要功能是将统计而来的信息以一种可视化的方式展示给用户。要使用此工具,可以通过调用 trace API:

package mainfunc main() {f, _ := os.Create("trace.out")defer f.Close()trace.Start(f)defer trace.Stop()(...)
}

并通过如下命令启动可视化界面。

go tool trace trace.out

方式3:debug.ReadGCStats

此方式可以通过代码的方式来直接实现对感兴趣指标的监控,例如我们希望每隔一秒钟监控一次 GC 的状态:

func printGCStats() {t := time.NewTicker(time.Second)s := debug.GCStats{}for {select {case <-t.C:debug.ReadGCStats(&s)fmt.Printf("gc %d last@%v, PauseTotal %v\n", s.NumGC, s.LastGC, s.PauseTotal)}}
}
func main() {go printGCStats()(...)
}

我们能够看到如下输出:

go run main.gogc 4954 last@2019-12-30 15:19:37.505575 +0100 CET, PauseTotal 29.901171ms
gc 9195 last@2019-12-30 15:19:38.50565 +0100 CET, PauseTotal 77.579622ms
gc 13502 last@2019-12-30 15:19:39.505714 +0100 CET, PauseTotal 128.022307ms
gc 17555 last@2019-12-30 15:19:40.505579 +0100 CET, PauseTotal 182.816528ms
gc 21838 last@2019-12-30 15:19:41.505595 +0100 CET, PauseTotal 246.618502ms

方式4:runtime.ReadMemStats

除了使用 debug 包提供的方法外,还可以直接通过运行时内存相关的 API 进行监控:

func printMemStats() {t := time.NewTicker(time.Second)s := runtime.MemStats{}for {select {case <-t.C:runtime.ReadMemStats(&s)fmt.Printf("gc %d last@%v, next_heap_size@%vMB\n", s.NumGC, time.Unix(int64(time.Duration(s.LastGC).Seconds()), 0), s.NextGC/(1<<20))}}
}
func main() {go printMemStats()(...)
}
go run main.gogc 4887 last@2019-12-30 15:44:56 +0100 CET, next_heap_size@4MB
gc 10049 last@2019-12-30 15:44:57 +0100 CET, next_heap_size@4MB
gc 15231 last@2019-12-30 15:44:58 +0100 CET, next_heap_size@4MB
gc 20378 last@2019-12-30 15:44:59 +0100 CET, next_heap_size@6MB

当然,后两种方式能够监控的指标很多,读者可以自行查看 debug.GCStats 和 runtime.MemStats 的字段,这里不再赘述。


参考文献

图示Golang垃圾回收机制 - 知乎
Golang垃圾回收(GC)介绍
Golang 的 goroutine 是如何实现的? - 知乎
Go 垃圾回收器指南 - 鸟窝
垃圾回收的认识 | Go 程序员面试笔试宝典
垃圾回收的基本想法 | Go 语言原本
垃圾收集器 | Go 语言设计与实现

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

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

相关文章

HiveSQL——设计一张最近180天的注册、活跃留存表

0 问题描述 现有一个用户活跃表user_active(user_id,active_date)、 用户注册表user_regist(user_id,regist_date)&#xff0c;表中分区字段都为dt(yyyy-MM-dd)&#xff0c;用户字段均为user_id; 设计一张 1-180天的注册活跃留存表&#xff1b;表结构如下&#xff1a; 1 数据分…

Agile Initiative, Epic, and Story/Task

Stories, also called “user stories,” are short requirements or requests written from the perspective of an end user. stories are something the team can commit to finish within a one- or two-week sprint.Epics are large bodies of work that can be broken do…

Java基础知识总结(持续更新中)

Java基础知识&#xff08;持续更新&#xff09; 类型转化&#xff1a;数字、字符串、字符之间相互转化 数字 <-> 字符串 // 数字转字符串 // method1int number 5;String str String.valueOf(number);// method2int number 5;Integer itr number; //int装箱为对…

C++:STL - vector

C&#xff1a;STL - vector 构造函数修改操作push_backpop_backerase 访问操作emptyoperatror[]backfront 容量操作sizecapacityresizereserve C的vector是一种可变长度的动态数组&#xff0c;被广泛用于C编程中。它是标准模板库&#xff08;STL&#xff09;中的容器之一&#…

【stomp实战】Springboot+Stomp协议实现聊天功能

本示例实现一个功能&#xff0c;前端通过websocket发送消息给后端服务&#xff0c;后端服务接收到该消息时&#xff0c;原样将消息返回给前端。前端技术栈htmlstomp.js&#xff0c;后端SpringBoot 前端代码 关于stomp客户端的开发&#xff0c;如果不清楚的&#xff0c;可以看…

2.6日学习打卡----初学RabbitMQ(一)

2.6日学习打卡 初识RabbitMQ、 一. MQ 消息队列 MQ全称Message Queue&#xff08;消息队列&#xff09;&#xff0c;是在消息的传输过程中保 存消息的容器。多用于系统之间的异步通信。 同步通信相当于两个人当面对话&#xff0c;你一言我一语。必须及时回复 异步通信相当于通…

Office 2010下载安装教程,保姆级教程,附安装包和工具

前言 Microsoft Office是由Microsoft(微软)公司开发的一套基于 Windows 操作系统的办公软件套装。常用组件有 Word、Excel、PowerPoint、Access、Outlook等。 准备工作 1、Win7 及以上系统 2、提前准备好 Office 2010 安装包 安装步骤 1.鼠标右击【Office2010(64bit)】压缩…

数据结构第十二天(队列)

目录 前言 概述 源码&#xff1a; 主函数&#xff1a; 运行结果&#xff1a; 前言 今天和大家共享一句箴言&#xff1a;我本可以忍受黑暗&#xff0c;如果我不曾见过太阳。 概述 队列&#xff08;Queue&#xff09;是一种常见的数据结构&#xff0c;遵循先进先出&#…

华为机考入门python3--(10)牛客10-字符个数统计

分类&#xff1a;字符 知识点&#xff1a; 字符的ASCII码 ord(char) 题目来自【牛客】 def count_unique_chars(s): # 创建一个空集合来保存不同的字符 unique_chars set() # 遍历字符串中的每个字符 for char in s: # 将字符转换为 ASCII 码并检查是否在范围内 #…

C#上位机与三菱PLC的通信05--MC协议之QnA-3E报文解析

1、MC协议回顾 MC是公开协议 &#xff0c;所有报文格式都是有标准 &#xff0c;MC协议可以在串口通信&#xff0c;也可以在以太网通信 串口&#xff1a;1C、2C、3C、4C 网口&#xff1a;4E、3E、1E A-1E是三菱PLC通信协议中最早的一种&#xff0c;它是一种基于二进制通信协…

教师如何找答案? #知识分享#职场发展

当今社会&#xff0c;随着信息技术的迅猛发展&#xff0c;大学生们在学习过程中面临着各种各样的困难和挑战。而在这些挑战中&#xff0c;面对繁重的作业和复杂的题目&#xff0c;大学生搜题软件应运而生 1.快解题 这是一个网站 是一款服务于职业考证的考试搜题软件,拥有几千…

从基建发力,CESS 如何推动 RWA 发展?

2023 年 11 月 30 日&#xff0c;Web3 基金会&#xff08;Web3 Foundation&#xff09;宣布通过 Centrifuge 将部分资金投资于 RWA&#xff08;Real World Assets&#xff0c;真实世界资产&#xff09;&#xff0c;试点投资为 100 万美元。Web3 基金会旨在通过支持专注于隐私、…

倒计时59天

(来源&#xff1a;b站左程云up 099&#xff09; 一&#xff1a;求逆元&#xff1a; 1&#xff09;要保证a可以整除b 2)要保证mod的是一个质数 3&#xff09;b和mod互质 题目2&#xff09;3&#xff09;一般都满足&#xff0c;主要是1) 方法&#xff1a;如求1.…

骨科器械行业分析:市场规模为360亿元

骨科器械一般指专门用于骨科手术用的专业医疗器械。按国家食品药品监督局的分类划分常分为&#xff1a;一类;二类和三类。按照使用用途和性能主要分为骨科用刀、骨科用剪、骨科用钳、骨科用钩、骨科用针、骨科用刮、骨科用锥、骨科用钻、骨科用锯、骨科用凿、骨科用锉/铲、骨科…

【C语言】一道相当有难度的指针某大厂笔试真题(超详解)

这是比较复杂的题目&#xff0c;但是如果我们能够理解清楚各个指针代表的含义&#xff0c;画出各级指针的关系图&#xff0c;这道题就迎刃而解了。 学会这道笔试题&#xff0c;相信你对指针的理解&#xff0c;对数组&#xff0c;字符串的理解都会上一个档次。 字符串存储使用的…

UDP是什么,UDP协议及优缺点

UDP&#xff0c;全称 User Datagram Protocol&#xff0c;中文名称为用户数据报协议&#xff0c;主要用来支持那些需要在计算机之间传输数据的网络连接。 UDP 协议从问世至今已经被使用了很多年&#xff0c;虽然目前 UDP 协议的应用不如 TCP 协议广泛&#xff0c;但 UDP 依然是…

JAVA设计模式之代理模式详解

代理模式 1 代理模式介绍 在软件开发中,由于一些原因,客户端不想或不能直接访问一个对象,此时可以通过一个称为"代理"的第三者来实现间接访问.该方案对应的设计模式被称为代理模式. 代理模式(Proxy Design Pattern ) 原始定义是&#xff1a;让你能够提供对象的替代…

OpenEuler20.03LTS SP2 上安装 OpenGauss3.0.0 单机部署过程(二)

开始安装 OpenGauss 数据库 3.1.7 安装依赖包 (说明:如果可以联网,可以通过网络 yum 安装所需依赖包,既可以跳过本步骤。如果网络无法连通,请把本文档所在目录下的依赖包上传到服务器上,手工安装后,即无需通过网络进行 Yum 安装了): 上传:libaio-0.3.111-5.oe1.x8…

【机器学习】合成少数过采样技术 (SMOTE)处理不平衡数据(附代码)

1、简介 不平衡数据集是机器学习和人工智能中普遍存在的挑战。当一个类别中的样本数量明显超过另一类别时&#xff0c;机器学习模型往往会偏向大多数类别&#xff0c;从而导致性能不佳。 合成少数过采样技术 (SMOTE) 已成为解决数据不平衡问题的强大且广泛采用的解决方案。 …

Webshell一句话木马

一、webshell介绍&#xff08;网页木马&#xff09; 分类&#xff1a; 大马&#xff1a;体积大、隐蔽性差、功能多 小马&#xff1a;体积小&#xff0c;隐蔽强&#xff0c;功能少 一句话木马&#xff1a;代码简短&#xff0c;灵活多样 二、一句话木马&#xff1a; &#xff1a;…