概述
GC类型的语言遇到了性能问题,经常会怪罪于GC,但GC到底会影响什么,是不是周期性现象一定和GC有关,其实是非常不确定的。例如,无论是Go还是Java,STW的时间其实已经控制在了很短,除非是ms级别的超时,否则不可能是STW的问题。
details
gc调优的2个目标
降低gc的cpu开销
大部分情况,调优是针对CPU开销的,一般而言,只要不出现OOM,gc的CPU占比越小越好,因为gc占比越大,服务的吞吐量越小。常用的手段就是空间换时间,容忍更大的内存占用,减少gc的触发时间。
降低gc的stw时间,避免影响请求延时
现代gc的一个重要原则就是一次stw不会太长,一般控制在1ms以内。Go是如何做到的?Go只在2个阶段会进入stw,首先是栈root集合扫描时,其次是扫描完成时,这2个阶段都能做到和堆内存大小无关,因此都在ms或微妙级别。但也有一些延迟敏感型应用,需要进一步优化stw的时间。
Go gc
根对象
- 全局变量
- 执行栈:每个协程有自己的执行栈,包含栈变量和指向堆内存的指针
- 寄存器
三色标记法
- 白色:可回收的,未被访问的
- 灰色:待扫描的,可能指向了白色对象
- 黑色:已扫描的
写屏障和读屏障
写屏障和读屏障类似于一个被装饰的读写方法,当调用时可能会走到特殊的逻辑。
- 写屏障是一个在并发垃圾回收器中才会出现的概念,它是为了打破gc的不正确的充要条件而设置的
- 读屏障在复制算法中可能被用到
Go1.14的gc流程
- gcStart
- gcBgMarkPrepare/gcMarkRootPrepare/gcBgMarkWorker (stw)
- gcBgMarkWorker/gcMarkDone
- gcMarkTermination(stw)
- gcSweep
Go的gc时机
- 被动触发,2min一次
- GOGC参数,内存占用大于之前的x倍,触发gc
Java gc
G1
分代算法
- 年轻代:复制算法,MinorGC
- 老年代:标记清除 标记整理,FullGC
- 永久代:即方法区,某些情况会被回收
ZGC的3个stw
ZGC的stw时间只和Roots有关系,而G1的转移和存活大小有关系
- 初始标记
- 再标记
- 初始转移
ZGC的着色指针
ZGC实际仅使用64位地址空间的第041位,而第4245位存储元数据,第47~63位固定为0。
practice
Go
CPU profile
profile可以剖析出CPU时间被花费在了哪些函数,一般runtime.gc消耗的占比不应该大于20%,否则可以考虑调优
pprof trace
相比profile,可以更细致地看到gc的执行路径
GODEBUG=gctrace=1
Java
参数
- -Xmx:最大堆
- Xms:最小堆
- -XX:ReservedCodeCacheSize:JIT缓存
- -XX:ConcGCThreads:并发回收垃圾的线程,默认是总核数的12.5%
- -XX:ParallelGCThreads:STW阶段使用线程数,默认是总核数的60%
- -XX:ZAllocationSpikeTolerance:ZGC触发自适应算法的修正系数,默认2,数值越大,越早的触发ZGC
- -Xlog:设置GC日志中的内容、格式、位置以及每个日志的大小