深入浅出 Go-GC

&&& Golang GC流程 Go 如何启动 GC Go 触发 GC 有三种情况: 主动调用 runtime.gc() 可能会触发 Gc 当分配对象时可能会触发 Gc 守护协程定时Gc runtime/mgc.go const ( // 根据堆分配内存情况,判断是否触发 GC gcTriggerHeap gcTriggerKind = iota // 定时触发 GC gcTriggerTime // 手动触发 GC gcTriggerCycle } func (t gcTrigger) test() bool { // ... switch t.kind { case gcTriggerHeap: // ... // 如果是堆内存分配导致的 GC,会 Check 当前堆内存的使用情况 trigger, _ := gcController.trigger() return atomic.Load64(&gcController.heapLive) >= trigger case gcTriggerTime: // 如果是守护协程 GC,则会check当前距离上一次 GC 是否已经达到 2 min if gcController.gcPercent.Load() < 0 { return false } lastgc := int64(atomic.Load64(&memstats.last_gc_nanotime)) return lastgc != 0 && t.now-lastgc > forcegcperiod case gcTriggerCycle: // ... // 如果是手动触发 GC,check 前一轮 GC 有没有结束 return int32(t.n-work.cycles) > 0 } return true } Gc - Start Golang的标记清扫算法的关键点就是标记这一步,其采用三色标记法,从 Root 对象开始进行可达性分析,关键步骤如下: ...

September 19, 2025 · 小石堆

大模型调用的流式输出解析

从流式输出到服务端推送技术再到 Java 的 WebFlux 大模型流式输出 像 ChatGPT 这样的网页,我们不难发现问出问题后,大模型吐字是一段接一段的,但我们传统的 Http 请求,一般是每次获取一段数据就要再次发起请求一次。这是一种耗费资源的方式,简言之就是 Http 轮询(短轮询和长轮询 Comet),所以服务端主动推送数据的计数就应运而生。 服务端主动推送技术 这里抛开 Http 轮询计数,主要涉及到了 SSE 和 WebSocket,其实 SSE 和 WebSocket 都是服务于 “实时” 二字的。 SSE 协议 SSE(Server Send Events),顾名思义,服务端发送事件,是指服务端能够主动给客户端发送消息。其基于 Http 协议,需要按照 SSE 协议规范在消息响应体中填充数据,如果需要 SSE 协议,则需要 Http 长连接(默认),并且将请求中的 content-type 设置为 text/event-stream。 其原理实际上是在建立好的 Http 连接上,于客户端协商,返回的类型不为一次性的数据包,而是返回一个 Stream。而基于这个 Strem,服务器可以不断的往内部填入数据,客户端也可以依次接受数据。 其实 SSE 是比较常见的,因为很多时候,只需要服务器推送给客户端,而客户端不需要给服务器发送内容,比如说在一个常用开源容器监控系统 Dozzle 中,就能看到其身影。可以类比,如果一个系统,类似比赛的看板或者日志的看板,就比较适合用 SSE 协议。 因为 SSE 并非一个完全新的协议,而是使用了 Http 协议的功能,并定义一系列规范,所以 SSE 的优点就是: 轻量级(并非全新协议)(相较于 WebSocket) 基于 Http,基本上所有的浏览器都支持、 支持断开重连 缺点: ...

September 14, 2025 · 小石堆

Java ArrayList & Go Slice 扩容

Java 中的 ArrayList 类似 Golang 里的 Slice,都是动态数组 ArrayList 什么是 ArrayList ArrayList 是 Java 中最常见的动态数组之一,他实现了 List 接口,底层基于数组实现,但能自动扩容,可以理解为变长数组的一种。 List<String> list = new ArrayList<>(); list.add("zhangsan"); list.add("lisi"); System.out.println(list); ArrayList 的扩容原理 当创建一个 ArrayList 的时候,默认初始的容量为 10,其内部实现类似 Object[] elementData = new Object[10] 当元素超过容量时,arraylist 就会触发扩容机制,其触发条件为 if(size == elementData.length) 扩容策略如下 newCapacity = oldCapacity + (oldCapacity + 1),简言之就是扩容为原来的 1.5 倍。 这里涉及到扩容过程的主要函数是 grow() private void grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity + 1); if(newCapacity - minCapacity < 0) { newCapacity = minCapacity; } // 创建新数组,复制旧数据 elementData = Arrays.copyOf(elementData, newCapacity) } 这里的扩容规则实际上并没有 Go 的复杂,但是要理解 minCapacity ...

September 3, 2025 · 小石堆

浅谈 IO 多路复用

之前面试准备过这里的八股,但觉得理解不够深,故打算再探深些 五种 IO 模型 阻塞 IO 非阻塞 IO IO 多路复用 信号驱动 IO 异步 IO 阻塞 IO 最简单,最易懂 厨子做菜,做不好服务生也等着,等到菜做好再上菜。 不难看出,效率很低,但是实现简单,对应到计算机世界中,这里用 C 语言的 read() 函数去举例(虽然 C 语言烂的一坨:) 用户态调用 read(fd, buf, count) 软中断切入内核态,内核根据 fd 查文件描述符表,找到对应的内核文件描述符 检查页缓存,是不是存在数据(之前用过,缓存还在) 缓存没有,触发磁盘 IO,等待数据,进程阻塞挂起 DMA 数据 Copy 完成,触发硬件中断,进而内核把页缓存的数据 copy_to_user 到用户空间 buf 上面就是一次完整的阻塞 IO,我们可以看出,在用户态触发 read() 后,后面的逻辑基本上都是交由内核态进程去接管的,但是该用户态进程此时卡死在这里,如果 fd 对应的是网络请求,那一个 fd 就得拉一个进程接管,假设上万个请求,那系统中会有上万个阻塞进程,所以不适合并发规模较大的场景。 非阻塞IO 厨子做菜,服务生不断轮询反问厨子有没有做好。 这一点对应到 CPU 就是进程不断调用 read(),如果数据准备好了,read()出数据,如果数据没有准备好,read()出 fasle,得到 false 的进程就去继续调用 read()。 while true: data = read(fd) // 非阻塞调用 if data == EAGAIN or data == EWOULDBLOCK: // 没有数据,立刻返回错误码 do_other_work() // 去做其他事情 else: process(data) // 一旦有数据就处理 break 也就是说,用户线程需要不断询问内核数据是否准备好,虽然能及时获取到数据,但是在没有获取到数据的时候,不会主动交出 CPU 资源,而一直占用 CPU。 ...

August 11, 2025 · 小石堆