当前位置: 首页 > news >正文

Golang 内存模型小结

Go 的内存模型

Go 的内存模型描述了如何分配内存、访问内存以及内存共享等细节。Go 程序的内存管理主要依赖如下方面:

  • 堆内存(Heap Memory)。用于存放程序运行时创建的对象,由 Go 的垃圾回收器自动管理。堆内存的生命周期不由函数作用域决定,而是由对象引用来决定。
  • 栈内存(Stack Memory)。用于存放局部变量、函数参数等数据。生命周期与函数调用栈相关。栈的管理非常高效,因为栈空间是先进后出(LIFO)的结构,而且通常是由操作系统直接管理。
  • 堆栈分配(Heap-Stack Allocation)。Go 语言在运行时会决定对象应该分配堆上还是栈上,具体是由基于逃逸分析的结果来确定。

Go 的内存分配过程

Go 的内存分配由内存分配器负责,内存分配器的核心任务是从操作系统请求内存、将其分配给相应的 Go 程序使用、管理内存的回收。

内存分配过程概述

  1. 栈分配

    如果局部变量和参数不会逃逸,即在函数返回时不再使用,则它们会分配在栈上。栈的分配非常高效,由操作系统直接管理内存并自动回收。

    栈上的对象生命周期与函数调用相同,当函数返回时,栈上的所有局部变量都将被销毁。

  2. 堆分配

    当一个变量逃逸,即变量的生命周期超出了函数作用域,将被分配在堆上。堆上的对象生命周期不由栈帧的生命周期决定,而是由 GC 来管理。

    堆上的对象生命周期由 GC 决定,对象是否被回收取决于对象是否还有引用指向它。

  3. 逃逸分析

    Go 语言的编译器会对代码进行逃逸分析,以决定哪些变量应该分配在堆上,哪些变量应该分配在栈上。逃逸分析的目的是为了尽可能地避免不必要的堆分配,提高性能。

    如果一个局部变量的地址被返回或者传递给了全局变量,它就会逃逸到堆上;如果变量只是局部使用,并且没有返回其地址,它就不会逃逸。

// x 是局部变量,原本应该分配在栈上,但由于 foo 函数返回 x 的地址,x 的生命周期被延长,Go 编译器随即将它分配到堆上。
func foo() *int {// 栈分配x := 40// x 逃逸到堆return &x
}func main() {y := foo()fmt.Println(*y)
}

Go 的垃圾回收机制

Go 语言采用垃圾回收机制来管理堆内存的回收,GC 的主要任务是自动检测不再使用的对象并将其回收,从而避免内存泄漏。

GC 的工作原理

Go 的垃圾回收器使用标记-清扫算法

  1. 标记阶段

    GC 从根对象(比如全局变量、栈变量)开始,递归标记所有仍然在使用的对象。

  2. 清扫阶段

    一旦标记完成,GC 会清理掉没有被标记的对象,并释放它们占用的内存。

Go 的垃圾回收器是并行和分代的,在分配内存时,它会尽可能地避免长时间的停顿,这对于高性能应用程序而言至关重要。

  • 增量式 GC。Go 的垃圾回收是增量式的,即它会将垃圾回收的工作分成多个小步骤,避免一次性停顿。
  • 并行 GC。Go 的垃圾回收是并行的,它会利用多核处理器的优势,同时进行多个 GC 任务。
  • 分代 GC。Go 的垃圾回收采用分代的策略,即将年轻代和老年代分开管理,年轻代对象更容易回收,而老年代对象的回收频率较低。

GC 的停顿时间

尽管 Go 的垃圾回收器非常高效,但在 GC 过程中,仍然会产生停顿。Go 1.5+ 在GC 算法上进行了改进,减少了停顿时间。

  • GC 时间。GC 的停顿时间通常是短暂的,但是对于实时性要求高的系统来说,可能仍然需要进行调优。
  • 调优 GC。可以通过环境变量 GOGC 来调整 GC 的触发阈值。GOGC 值决定了垃圾回收器的触发时机,默认值是 100,表示当堆内存增长到原来的一倍时,便触发 GC。

此外,Go 语言提供了 pprof 工具,可以用于分析 Go 程序性能,通过 pprof,可以查看 GC 的运行情况,分析垃圾回收的时间消耗和内存使用情况;runtime/pprof 包提供了获取程序运行时的性能信息的功能,可以用于分析内存分配的情况。

Go 的内存泄漏

分析:

  • 未关闭的资源。比如未关闭的文件、数据库连接等。
  • 循环引用。多个对象相互引用,导致 GC 无法回收。
  • 持久化引用。通过全局变量或者长生命周期的对象保持对不再使用的对象的引用。

处理:

  • 使用 defer 关闭资源。确保在使用完资源后及时关闭它们。
  • 避免循环引用。确保对象间的引用不会形成循环。
  • 合理管理全局变量。避免全局变量持有对不再使用的对象的引用。

Go 的可见性和重排序

在 Go 中,多个 goroutine 并发访问共享变量时,如果不通过同步原语(如 Channel、锁、atomic 操作)进行同步,则变量的读写操作是没有顺序性和一致性保证的。Go 编译器和底层 CPU 可能会对指令进行重排序,以提高执行效率,但这会导致实际运行时的指令顺序与源码不一致,从而引发并发错误。如果希望一个 goroutine 中写入的值能被另一个 goroutine 正确读取,就必须使用同步原语来建立同步关系。

// 在没有同步的前提下,thread2 可能会输出:b = 1 而 a = 0
var a, b intfunc thread1() {a = 1b = 1
}func thread2() {fmt.Println(b)fmt.Println(a)
}
  • 编译器或 CPU 重排序。thread1 中的 a = 1 和 b = 1 两行代码在源码中是有先后顺序的,但为了优化性能,编译器或 CPU 可能会将它们重排序为先执行 b = 1,再执行 a = 1,因为它们之间没有依赖关系。
  • 内存可见性问题。即使在 thread1 的执行顺序是 a = 1; b = 1,由于没有同步,thread2 可能在写入 a 之前就观察到了写入 b,因为缓存同步机制、CPU 内存模型等原因,导致一个 goroutine 对变量的修改对另一个 goroutine 不可见。

相关文章:

  • Docker实战
  • Linux下的Socket编程
  • 小白的进阶之路系列之三----人工智能从初步到精通pytorch计算机视觉详解上
  • React+Taro 微信小程序做一个页面,背景图需贴手机屏幕最上边覆盖展示
  • 桥接智能制造:PROFINET与Devicenet混合架构赋能汽车擦净机器人升级
  • java每日精进 5.22【多数据源(读写分离)、事务】
  • 觉醒三境:在敦煌的风沙中寻找生命的纹路
  • 火山引擎火山云带宽价格
  • 【大模型面试每日一题】Day 26:从伦理角度,大模型可能存在哪些潜在风险?技术上如何实现内容安全控制(如RLHF、红队测试)?
  • Ubuntu-多显示器黑屏问题及nvidia显卡驱动安装
  • 当物联网“芯”闯入纳米世界:ESP32-S3驱动的原子力显微镜能走多远?
  • 自制操作系统day7(获取按键编码、FIFO缓冲区、鼠标、键盘控制器(Keyboard Controller, KBC)、PS/2协议)
  • 鸿蒙Flutter实战:23-混合开发详解-3-源码模式引入
  • FreeBSD14.2因为爆内存而导致Xfce4视窗被卡,桌面变黑色,只能看到鼠标在窗体中心,鼠标无反应,键盘无反应
  • 自制操作系统day8 (鼠标数据取得、通往32位模式之路、A20GATE、切换到保护模式、控制寄存器cr0-cr4以及cr8、ALIGNB)
  • 创建信任所有证书的HttpClient:Java 实现 HTTPS 接口调用,等效于curl -k
  • 【Java面试】从Spring Boot到Kafka:技术栈与业务场景全面剖析
  • 养生新策:五维开启健康生活
  • 青少年编程与数学 02-020 C#程序设计基础 01课题、C#编程概要
  • 现代生活健康养生新策略
  • 信誉好的龙岗网站建设/seo服务深圳
  • 网站建设犭金手指B排名14/南昌seo实用技巧
  • 广南网站制作/怎样在百度上打广告
  • 石家庄高端网站建设/seo专业培训班
  • 网站建设要什么知识/seo点击软件手机
  • 有人用dw做网站吗/西安seo排名