learning_notes

学习笔记

View project on GitHub

内存管理

常见的GC模式

  • 引用计数(销毁引用,计数器-1,为0,回收)
  • 标记清除(mark and sweep) 缺点:每次GC需要暂停所有的运行代码(go1.5之前使用),(go1.5之后使用)变种”三色标记”
  • 分代搜集

内存泄漏

  • 堆内存泄漏(使用内存后未释放)
  • 系统资源泄漏(使用后未关闭句柄)

GC调优

  • 减少对象的分配(对象重用)

go程序内存占用大的问题

  • 是go的垃圾回收有个触发阈值,这次是4M,gc百分比参数是100,下次触发gc是8M
  • 操作系统”拖延症”策略
  • 每次申请1M的内存,然后分割成不同大小的span,给对象使用,不使用的5分钟后交还系统

对齐规则

内存对齐

  • 结构体的成员变量: 编译器默认对齐长度 和 当前成员变量类型的长度(unsafe.Sizeof) 这2者间取最小值作为当前类型的对齐值。其偏移量必须为对齐值的整数倍

  • 结构体本身:编译器默认对齐长度 和 结构体的所有成员变量类型中的最大长度 这2者间 取最大数的最小整数倍作为对齐值

  • 结合以上两点,可得知若编译器默认对齐长度(#pragma pack(n))超过结构体内成员变量的类型最大长度时,默认对齐长度是没有任何意义的

如何观察GC

  • GODEBUG=gctrace=1
  • go tool trace(有图)
  • runtime.ReadMemStats

有GC,为何还会内存泄漏

  • 预期能被快速释放的内存因被根对象引用而没有得到迅速释放(局部使用变量存到全局对象)
  • goroutine 泄漏(协程上下文保存)

Go并发标记清除的,存在什么难点

  • 用户态代码可能并发地更新对象图

什么是根对象

  • 全局变量
  • 执行栈:每个 goroutine 都包含自己的执行栈,这些执行栈上包含栈上的变量及指向分配的堆内存区块的指针。
  • 寄存器

GC

  • Go1.3采用标记清除法, Go1.5采用三色标记法,Go1.8采用三色标记法+混合写屏障。

  • 一次完整的GC分为四个阶段:
    • 准备标记(需要STW),开启写屏障。
    • 开始标记
      • 在进入 GC 的三色标记阶段的一开始,所有对象都是白色的。
      • 遍历根节点集合里的所有根对象,把根对象引用的对象标记为灰色,从白色集合放入灰色集合。
      • 遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合
      • 重复第三步, 直到灰色集合中无任何对象。
      • 回收白色集合里的所有对象,本次垃圾回收结束。
    • 标记结束(STW),关闭写屏障
    • 清理(并发)
  • 基于插入写屏障和删除写屏障在结束时需要STW来重新扫描栈,带来性能瓶颈。混合写屏障分为以下四步:
    • GC开始时,将栈上的全部对象标记为黑色(不需要二次扫描,无需STW);
    • GC期间,任何栈上创建的新对象均为黑色
    • 被删除引用的对象标记为灰色
    • 被添加引用的对象标记为灰色
    • 总而言之就是确保黑色对象不能引用白色对象,这个改进直接使得GC时间从 2s降低到2us。

有了 GC,为什么还会发生内存泄露

  • 预期能被快速释放的内存因被根对象引用而没有得到迅速释放
  • goroutine 泄漏
  • 如果一个 goroutine 尝试向一个没有接收方的无缓冲 channel 发送消息,则该 goroutine 会被永久的休眠,整个 goroutine 及其执行栈都得不到释放

触发 GC 的时机是什么?

  • 主动触发,通过调用 runtime.GC 来触发 GC,此调用阻塞式地等待当前 GC 运行完毕。
  • 被动触发,分为两种方式:
    • 使用系统监控,当超过两分钟没有产生任何 GC 时,强制触发 GC。
    • 使用步调(Pacing)算法,其核心思想是控制内存增长的比例。

GC优化

  • 控制内存分配的速度,限制 goroutine 的数量,从而提高赋值器对 CPU 的利用率。
  • 减少并复用内存,例如使用 sync.Pool 来复用需要频繁创建临时对象,例如提前分配足够的内存来降低多余的拷贝。
  • 需要时,增大 GOGC 的值,降低 GC 的运行频率。