聊聊两个Go即将过时的GC优化策略
转载请声明出处哦~,本篇文章发布于luozhiyun的博客:https://www.luozhiyun.com
这篇文章本来是要讲 Go Memory Ballast 以及 Go GC Tuner 来调整 GC 的策略,实现原理怎么样,效果如何。但是在写的过程中,发现 Go 1.19版本出了,有个新特性让这两个优化终究成为历史。
概述
首先我们来简单的看一下 Go GC中做了什么事,以及它里面比较耗时的地方是什么,我们才能对它进行优化。
首先对于 GC 来说有这么几个阶段:
- sweep termination(清理终止):会触发 STW ,所有的 P(处理器) 都会进入 safe-point(安全点);
- the mark phase(标记阶段):恢复程序执行,GC 执行根节点的标记,这包括扫描所有的栈、全局对象以及不在堆中的运行时数据结构;
- mark termination(标记终止):触发 STW,扭转 GC 状态,关闭 GC 工作线程等;
- the sweep phase(清理阶段):恢复程序执行,后台并发清理所有的内存管理单元;
在这几个阶段中,由于标记阶段是要从根节点对堆进行遍历,对存活的对象进行着色标记,因此标记的时间和目前存活的对象有关,而不是与堆的大小有关,也就是堆上的垃圾对象并不会增加 GC 的标记时间。
并且对于现代操作系统来说释放内存是一个非常快的操作,所以 Go 的 GC 时间很大程度上是由标记阶段决定的,而不是清理阶段。
在什么时候会触发 GC ?
我在这篇文章 https://www.luozhiyun.com/archives/475 做源码分析的时候有详细的讲到过,我这里就简单的说下。
在 Go 中主要会在三个地方触发 GC:
1、监控线程 runtime.sysmon 定时调用;
2、手动调用 runtime.GC 函数进行垃圾收集;
3、申请内存时 runtime.mallocgc 会根据堆大小判断是否调用;
runtime.sysmon
Go 程序在启动的时候会后台运行一个线程定时执行 runtime.sysmon 函数,这个函数主要用来检查死锁、运行计时器、调度抢占、以及 GC 等。
它会执行 runtime.gcTrigger
中的 test 函数来判断是否应该进行 GC。由于 GC 可能需要执行时间比较长,所以运行时会在应用程序启动时在后台开启一个用于强制触发垃圾收集的 Goroutine 执行 forcegchelper 函数。
不过 forcegchelper 函数在一般情况下会一直被 goparkunlock 函数一直挂起,直到 sysmon 触发GC 校验通过,才会将该被挂起的 Goroutine 放转身到全局调度队列中等待被调度执行 GC。
runtime.GC
这个比较简单,会获取当前的 GC 循环次数,然后设值为 gcTriggerCycle 模式调用 gcStart 进行循环。
runtime.mallocgc
我在内存分配 https://www.luozhiyun.com/archives/434 这一节讲过,对象在进行内存分配的时候会按大小分成微对象、小对象和大对象三类分别执行 tiny malloc、small alloc、large alloc。
Go 的内存分配采用了池化的技术,类似 CPU 这样的设计,分为了三级缓存,分别是:每个线程单独的缓存池mcache、中心缓存 mcentral 、堆页 mheap 。
tiny malloc、small alloc 都会先去 mcache 中找空闲内存块进行内存分配,如果 mcache 中分配不到内存,就要到 mcentral 或 mheap 中去申请内存,这个时候就会尝试触发 GC;而对于 large alloc 一定会尝试触发 GC 因为它直接在堆页上分配内存。
如何控制 GC 是否应该被执行?
上面这三个触发 GC 的地方最终都会调用 gcStart 执行 GC,但是在执行 GC 之前一定会先判断这次调用是否应该被执行,并不是每次调用都一定会执行 GC, 这个时候就要说一下 runtime.gcTrigger
中的 test 函数,这个函数负责校验本次 GC 是否应该被执行。
runtime.gcTrigger
中的 test 函数最终会根据自己的三个策略,判断是否应该执行GC:
- gcTriggerHeap:按堆大小触发,堆大小和上次 GC 时相比达到一定阈值则触发;
- gcTriggerTime:按时间触发,如果超过 forcegcperiod(默认2分钟) 时间没有被 GC,那么会执行GC;
- gcTriggerCycle:没有开启垃圾收集,则触发新的循环;
如果是 gcTriggerHeap 策略,那么会根据 runtime.gcSetTriggerRatio
函数中计算的值来判断是否要进行 GC,主要是由环境变量 GOGC(默认值为100 ) 决定阈值是多少。
我们可以大致认为,触发 GC 的时机是由上次 GC 时的堆内存大小,和当前堆内存大小值对比的增长率来决定的,这个增长率就是环境变量 GOGC,默认是 100 ,计算公式可以大体理解为:
hard_target = live_dataset + live_dataset * (GOGC / 100).
假设目前是 100M 内存占用,那么根据上面公式,会到 200M 的时候才会触发 GC。
触发 GC 的时机其实并不只是 GOGC 单一变量决定的,在代码 runtime.gcSetTriggerRatio
里面我们可以看到它控制的是一个范围:
func gcSetTriggerRatio(triggerRatio float64) {
// gcpercent 由环境变量 GOGC 决定
if gcpercent >= 0 {
// 默认是 1
scalingFactor := float64(gcpercent) / 100
// 最大的 maxTriggerRatio 是 0.95
maxTriggerRatio := 0.95 * scalingFactor
if triggerRatio > maxTriggerRatio {
triggerRatio = maxTriggerRatio
}
// 最大的 minTriggerRatio 是 0.6
minTriggerRatio := 0.6 * scalingFactor
if triggerRatio < minTriggerRatio {
triggerRatio = minTriggerRatio
}
} else if triggerRatio < 0 {
triggerRatio = 0
}
memstats.triggerRatio = triggerRatio
trigger := ^uint64(0)
if gcpercent >= 0 {
// 当前标记存活的大小乘以1+系数triggerRatio
trigger = uint64(float64(memstats.heap_marked) * (1 + triggerRatio))
...
}
memstats.gc_trigger = trigger
...
}
具体阈值计算是比较复杂的,从 gcControllerState.endCycle
函数中可以看到执行 GC 的时机还要看以下几个因素:
- 当前 CPU 占用率,GC 标记阶段最高不能超过整个应用的 25%;
- 辅助 GC 标记对象 CPU 占用率;
- 目标增长率(预估),该值等于:(下次 GC 完后堆大小 - 堆存活大小)/ 堆存活大小;
- 堆实际增长率:堆总大小/上次标记完后存活大小-1;
- 上次GC时触发的堆增长率大小;
这些综合因素计算之后得到的一个值就是本次的触发 GC 堆增长率大小。这些都可以通过 GODEBUG=gctrace=1,gcpacertrace=1
打印出来。
下面我们看看一个具体的例子:
package main
import (
"fmt"
)
func allocate() {
_ = make([]byte, 1<<20)
}
func main() {
fmt.Println("start.")
fmt.Println("> loop.")
for {
allocate()
}
fmt.Println("< loop.")
}
使用 gctrace 跟踪 GC 情况:
[root@localhost gotest]# go build main.go
[root@localhost gotest]# GODEBUG=gctrace=1 ./main
start.
> loop.
...
gc 1409 @0.706s 14%: 0.009+0.22+0.076 ms clock, 0.15+0.060/0.053/0.033+1.2 ms cpu, 4->6->2 MB, 5 MB goal, 16 P
gc 1410 @0.706s 14%: 0.007+0.26+0.092 ms clock, 0.12+0.050/0.070/0.030+1.4 ms cpu, 4->7->3 MB, 5 MB goal, 16 P
gc 1411 @0.707s 14%: 0.007+0.36+0.059 ms clock, 0.12+0.047/0.092/0.017+0.94 ms cpu, 5->7->2 MB, 6 MB goal, 16 P
...
< loop.
上面展示了 3 次 GC 的情况,下面我们看看:
gc 1410 @0.706s 14%: 0.007+0.26+0.092 ms clock, 0.12+0.050/0.070/0.030+1.4 ms cpu, 4->7->3 MB, 5 MB goal, 16 P
内存
4 MB:标记开始前堆占用大小 (in-use before the Marking started)
7 MB:标记结束后堆占用大小 (in-use after the Marking finished)
3 MB:标记完成后存活堆的大小 (marked as live after the Marking finished)
5 MB goal:标记完成后正在使用的堆内存的目标大小 (Collection goal)
可以看到这里标记结束后堆占用大小是7 MB,但是给出的目标预估值是 5 MB,你可以看到回收器超过了它设定的目标2 MB,所以它这个目标值也是不准确的。
在 1410 次 GC 中,最后标记完之后堆大小是 3 MB,所以我们可以大致根据 GOGC 推测下次 GC 时堆大小应该不超过 6MB,所以我们可以看看 1411 次GC:
gc 1411 @0.707s 14%: 0.007+0.36+0.059 ms clock, 0.12+0.047/0.092/0.017+0.94 ms cpu, 5->7->2 MB, 6 MB goal, 16 P
内存
5 MB:标记开始前堆占用大小 (in-use before the Marking started)
7 MB:标记结束后堆占用大小 (in-use after the Marking finished)
2 MB:标记完成后存活堆的大小 (marked as live after the Marking finished)
6 MB goal:标记完成后正在使用的堆内存的目标大小 (Collection goal)
可以看到在 1411 次GC启动时堆大小是 5 MB 是在控制范围之内。
说了这么多 GC 的机制,那么有没有可能 GC 的速度赶不上制造垃圾的速度呢?这就引出了 GC 中的另一种机制:Mark assist。
如果收集器确定它需要减慢分配速度,它将招募应用程序 Goroutines 来协助标记工作。这称为 Mark assist 标记辅助。这也就是为什么在分配内存的时候还需要判断要不要执行 mallocgc 进行 GC。
在进行 Mark assist 的时候 Goroutines 会暂停当前的工作,进行辅助标记工作,这会导致当前 Goroutines 工作的任务有一些延迟。
而我们的 GC 也会尽可能的消除 Mark assist ,所以会让下次的 GC 时间更早一些,也就会让 GC 更加频繁的触发。
我们可以通过 go tool trace 来观察到 Mark assist 的情况:
Go Memory Ballast
上面我们熟悉了 Go GC 的策略之后,我们来看看 Go Memory Ballast 是怎么优化 GC 的。下面先看一个例子:
func allocate() {
_ = make([]byte, 1<<20)
}
func main() {
ballast := make([]byte, 200*1024*1024) // 200M
for i := 0; i < 10; i++ {
go func() {
fmt.Println("start.")
fmt.Println("> loop.")
for {
allocate()
}
fmt.Println("< loop.")
}()
}
runtime.KeepAlive(ballast)
我们运行上面的代码片段,然后我们对资源利用的情况进行简单的统计:
从上面的结果我们可以直到,GC 的 CPU 利用率大约在 5.5 % 左右。
下面我们把 ballast 内存占用去掉,看看会是多少:
可以看到在没有 ballast 的时候 GC 的 CPU占用在 28% 左右。对 GC 的其他信息感兴趣的朋友可以使用 runtime.Memstats
定期抓取 GC 的信息进行打印。
那么为什么在申请了一个空的数组之后 CPU 占用会低这么多?首先我们在概述也讲到了,GC 会根据环境变量 GOGC 来决定下次 GC 的执行时机,所以如果我们申请了200M的数组,那么下次 GC 的时候大约会在 400M。由于我们上面的例子中,allocate 函数申请的对象都是临时对象,在 GC 之后会被再次减少到 200M 左右,所以下次执行 GC 的时机会被再次设置到 400M 。
但是如果没有 ballast 数组,感兴趣的可以自行去测试一下,大约会在 4M 左右的时候会触发 GC,这无疑对于临时变量比较多的系统来说会造成相当频繁的 GC。
总之,通过设置 ballast 数组我们达到了延迟 GC 的效果,但是这种效果只会在临时变量比较多的系统中有用,对于全局变量多的系统,用处不大。
那么还有一个问题,在系统中无故申请 200M 这么大的内存会不会对内存造成浪费?毕竟内存这么贵。其实不用担心,只要我们没有对 ballast 数组进行读写,是不会真正用到物理内存占用的,我们可以用下面的例子看一下:
func main() {
_ = make([]byte, 100<<20)
<-time.After(time.Duration(math.MaxInt64))
}
$ ps -eo pmem,comm,pid,maj_flt,min_flt,rss,vsz --sort -rss | numfmt --header --to=iec --field 4-5 | numfmt --header --from-unit=1024 --to=iec --field 6-7 | column -t | egrep "[t]est|[P]ID"
%MEM COMMAND PID MAJFL MINFL RSS VSZ
0.0 test_alloc 31248 0 1.1K 7.4M 821M
可以看到虚拟内存VSZ占用很大,但是RSS 进程分配的内存大小很小。
func main() {
ballast := make([]byte, 100<<20)
for i := 0; i < len(ballast)/2; i++ {
ballast[i] = byte('A')
}
<-time.After(time.Duration(math.MaxInt64))
}
$ ps -eo pmem,comm,pid,maj_flt,min_flt,rss,vsz --sort -rss | numfmt --header --to=iec --field 4-5 | numfmt --header --from-unit=1024 --to=iec --field 6-7 | column -t | egrep "[t]est|[P]ID"
%MEM COMMAND PID MAJFL MINFL RSS VSZ
0.4 test_alloc 31692 0 774 60M 821M
但是如果我们要对它进行写入操作,RSS 进程分配的内存大小就会变大,剩下的可以自己去验证。
对于 Go Ballast 的讨论其实很早就有人提过 issue ,其实官方只需要加一个最小堆大小的参数即可,但是一直没有得到实现。相比之下 Java 就好很多GC 的调优参数,InitialHeapSize 就可以设置堆的初始值。
这也导致了很多对性能要求比较高的项目如: tidb,cortex 都在代码里加了一个这样的空数组实现。
Go GC Tuner
这个方法其实是来自 uber 的这篇文章里面介绍的。根本问题还是因为 Go 的 GC 太频繁了,导致标记占用了很高的 CPU,但是 Go 也提供了 GOGC 来调整 GC 的时机,那么有没有一种办法可以动态的根据当前的内存调整 GOGC 的值,由此来控制 GC 的频率呢?
在 Go 中其实提供了 runtime.SetFinalizer
函数,它会在对象被 GC 的时候最后回调一下。在 Go 中 它是这么定义的:
type any = interface{}
func SetFinalizer(obj any, finalizer any)
obj 一般来说是一个对象的指针;finalizer 是一个函数,它接受单个可以直接用 obj 类型值赋值的参数。也就是说 SetFinalizer 的作用就是将 obj 对象的析构函数设置为 finalizer,当垃圾收集器发现 obj 不能再直接或间接访问时,它会清理 obj 并调用 finalizer。
所以我们可以通过它来设置一个钩子,每次 GC 完之后检查一下内存情况,然后设置 GOGC 值:
type finalizer struct {
ref *finalizerRef
}
type finalizerRef struct {
parent *finalizer
}
func finalizerHandler(f *finalizerRef) {
// 为 GOGC 动态设值
getCurrentPercentAndChangeGOGC()
// 重新设置回去,否则会被真的清理
runtime.SetFinalizer(f, finalizerHandler)
}
func NewTuner(options ...OptFunc) *finalizer {
// 处理传入的参数
...
f := &finalizer{}
f.ref = &finalizerRef{parent: f}
runtime.SetFinalizer(f.ref, finalizerHandler)
// 设置为 nil,让 GC 认为原 f.ref 函数是垃圾,以便触发 finalizerHandler 调用
f.ref = nil
return f
}
上面的这段代码就利用了 finalizer 特性,在 GC 的时候会调用 getCurrentPercentAndChangeGOGC 重新设置 GOGC 值,由于 finalizer 会延长一次对象的生命周期,所以我们可以在 finalizerHandler 中设置完 GOGC 之后再次调用 SetFinalizer 将对象重新绑定在 Finalizer 上。
这样构成一个循环,每次 GC 都会有一个 finalizerRef 对象在动态的根据当前内存情况改变 GOGC 值,从而达到调整 GC 次数,节约资源的目的。
上面我们也提到过,GC 基本上根据本次 GC 之后的堆大小来计算下次 GC 的时机:
hard_target = live_dataset + live_dataset * (GOGC / 100).
比如本次 GC 完之后堆大小 live_dataset 是 100 M,对于 GOGC 默认值 100 来说会在堆大小 200M 的时候触发 GC。
为了达到最大化利用内存,减少 GC 次数的目的,那么我们可以将 GOGC 设置为:
(可使用内存最大百分比 - 当前占内存百分比)/当前占内存百分比 * 100
也就是说如果有一台机器,全部内存都给我们应用使用,应用当前占用 10%,也就是 100M,那么:
GOGC = (100%-10%)/10% * 100 = 900
然后根据上面 hard_target 计算公式可以得知,应用将在堆占用达到 1G 的时候开始 GC。当然我们生产当中不可能那么极限,具体的最大可使用内存最大百分比还需要根据当前情况进行调整。
那么换算成代码,我们的 getCurrentPercentAndChangeGOGC 就可以这么写:
var memoryLimitInPercent float64 = 100
func getCurrentPercentAndChangeGOGC() {
p, _ := process.NewProcess(int32(os.Getpid()))
// 获取当前应用占用百分比
memPercent, _ := p.MemoryPercent()
// 计算 GOGC 值
newgogc := (memoryLimitInPercent - float64(memPercent)) / memPercent * 100.0
// 设置 GOGC 值
debug.SetGCPercent(int(newgogc))
}
上面这段代码我省去了很多异常处理,默认处理,以及 memoryLimitInPercent 写成了一个固定值,在真正使用的时候,代码还需要再完善一下。
写到这里,上面 Go Memory Ballast 和 Go GC Tuner 已经达到了我们的优化目的,但是在我即将提稿的时候,曹春晖大佬发了一篇文章中,说到最新的 Go 版本中 1.19 beta1版本中新加了一个 debug.SetMemoryLimit 函数。
Soft Memory Limit
这一个优化来自 issue#48409,在 Go 1.19 版本中被加入,优化原理实际上和上面差不多,通过内置的 debug.SetMemoryLimit
函数我们可以调整触发 GC 的堆内存目标值,从而减少 GC 次数,降低GC 时 CPU 占用的目的。
在上面我们也讲了,Go 实现了三种策略触发 GC ,其中一种是 gcTriggerHeap,它会根据堆的大小设定下次执行 GC 的堆目标值。 1.19 版的代码正是对 gcTriggerHeap 策略做了修改。
通过代码调用我们可以知道在 gcControllerState。heapGoalInternal
计算 HeapGoal 的时候使用了两种方式,一种是通过 GOGC 值计算,另一种是通过 memoryLimit 值计算,然后取它们两个中小的值作为 HeapGoal。
func (c *gcControllerState) heapGoalInternal() (goal, minTrigger uint64) {
// Start with the goal calculated for gcPercent.
goal = c.gcPercentHeapGoal.Load() //通过 GOGC 计算 heapGoal
// 通过 memoryLimit 计算 heapGoal,并和 goal 比较大小,取小的
if newGoal := c.memoryLimitHeapGoal(); go119MemoryLimitSupport && newGoal < goal {
goal = newGoal
} else {
...
}
return
}
gcPercentHeapGoal 的计算方式如下:
func (c *gcControllerState) commit(isSweepDone bool) {
...
gcPercentHeapGoal := ^uint64(0)
if gcPercent := c.gcPercent.Load(); gcPercent >= 0 {
// HeapGoal = 存活堆大小 + (存活堆大小+栈大小+全局变量大小)* GOGC/100
gcPercentHeapGoal = c.heapMarked + (c.heapMarked+atomic.Load64(&c.lastStackScan)+atomic.Load64(&c.globalsScan))*uint64(gcPercent)/100
}
c.gcPercentHeapGoal.Store(gcPercentHeapGoal)
...
}
和我们上面提到的 hard_target 计算差别不大,可以理解为:
HeapGoal = live_dataset + (live_dataset+栈大小+全局变量大小)* GOGC/100
我们再看看memoryLimitHeapGoal计算:
func (c *gcControllerState) memoryLimitHeapGoal() uint64 {
var heapFree, heapAlloc, mappedReady uint64
heapFree = c.heapFree.load()
heapAlloc = c.totalAlloc.Load() - c.totalFree.Load()
mappedReady = c.mappedReady.Load()
memoryLimit := uint64(c.memoryLimit.Load())
nonHeapMemory := mappedReady - heapFree - heapAlloc
...
goal := memoryLimit - nonHeapMemory
...
return goal
}
上面这段代码基本上可以理解为:
goal = memoryLimit - 非堆内存
所以正因为 Go GC 的触发是取上面两者计算结果较小的值,那么原本我们使用 GOGC 填的太大怕导致 OOM,现在我们可以加上 memoryLimit 参数限制一下;或者直接 GOGC = off ,然后设置 memoryLimit 参数,通过它来调配我们的 GC。
总结
我们这篇主要通过讲解 Go GC 的触发机制,然后引出利用这个机制可以比较 hack 的方式减少 GC 次数,从而达到减少 GC 消耗。
Go Memory Ballast 主要是通过预设一个大数组,让 Go 在启动的时候提升 Go 下次触发 GC 的堆内存阈值,从而避免在内存够用,但是应用内临时变量较多时不断 GC 所产生的不必要的消耗。
Go GC Tuner 主要时通过 Go 提供的 GC 钩子,设置 Finalizer 在 GC 完之后通过当前的内存使用情况动态设置 GOGC,从而达到减少 GC 的目的。
Soft Memory Limit 是1.19版本的新特性,通过内置的方式实现了 GC 的控制,通过设置 memoryLimit 控制 GC 内存触发阈值达到减少 GC 的目的,原理其实和上面两种方式没有本质区别,但是由于内置在 GC 环节,可以更精细化的检查当前的非堆内存占用情况,从而实现更精准控制。
Reference
https://github.com/golang/go/issues/23044
https://www.cnblogs.com/457220157-FTD/p/15567442.html
https://github.com/golang/go/issues/42430
https://eng.uber.com/how-we-saved-70k-cores-across-30-mission-critical-services/
https://xargin.com/dynamic-gogc/
https://github.com/cch123/gogctuner
https://golang.design/under-the-hood/zh-cn/part2runtime/ch08gc/pacing/
https://medium.com/a-journey-with-go/go-finalizers-786df8e17687
https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-garbage-collector
https://xargin.com/the-new-api-for-heap-limit/
https://pkg.go.dev/runtime/debug@master#SetMemoryLimit
https://tip.golang.org/doc/go1.19
https://github.com/golang/go/issues/48409
聊聊两个Go即将过时的GC优化策略的更多相关文章
- JVM:从实际案例聊聊Java应用的GC优化
原文转载自美团从实际案例聊聊Java应用的GC优化,感谢原作者的贡献 当Java程序性能达不到既定目标,且其他优化手段都已经穷尽时,通常需要调整垃圾回收器来进一步提高性能,称为GC优化.但GC算法复杂 ...
- 从实际案例聊聊Java应用的GC优化
转自美团点评技术博客:https://tech.meituan.com/jvm_optimize.html 当Java程序性能达不到既定目标,且其他优化手段都已经穷尽时,通常需要调整垃圾回收器来进一步 ...
- 从实际案例聊聊Java应用的GC优化--转
https://tech.meituan.com/jvm_optimize.html 当Java程序性能达不到既定目标,且其他优化手段都已经穷尽时,通常需要调整垃圾回收器来进一步提高性能,称为GC优化 ...
- Unity3D游戏GC优化总结---protobuf-net无GC版本优化实践
protobuf-net优化效果图 protobuf-net是Unity3D游戏开发中被广泛使用的Google Protocol Buffer库的c#版本,之所以c#版本被广泛使用,是因为c++版本的 ...
- 一步步优化JVM三:GC优化基础
本节主要描述关于垃圾回收器性能的三个指标,三个关于垃圾回收器优化的基本原则,以及优化HotSpot VM的垃圾回收器的信息收集,在这些指标中权衡以及信息的收集是非常重要的. 性能指标 吞吐量:衡 ...
- 如何降低90%Java垃圾回收时间?以阿里HBase的GC优化实践为例
过去的一年里,我们准备在Ali-HBase上突破这个被普遍认知的痛点,为此进行了深度分析及全面创新的工作,获得了一些比较好的效果.以蚂蚁风控场景为例,HBase的线上young GC时间从120ms减 ...
- JAVA GC优化入门
为什么需要优化GC? JAVA的GC是面试必考的题目,可是在实际项目中什么时候使用GC哪?或者应该什么时候优化GC哪?有句名言:“GC优化永远是最后一项任务”. 在使用GC之前,应该考虑一下进行GC的 ...
- golang gc 优化思路以及实例分析
一个即将上线的go 写的高频服务,压测的时候发现 gc 特别高,高到10%-15% 左右了,本文记录下优化 gc 的过程和和思路.线上环境1.10. 首先,查看gc 是否有异常,我们可以使用 gctr ...
- Unity下XLua方案的各值类型GC优化深度剖析
转自:http://gad.qq.com/article/detail/25645 前言 Unity下的C#GC Alloc(下面简称gc)是个大问题,而嵌入一个动态类型的Lua后,它们之间的交互很容 ...
随机推荐
- Nginx防御CC攻击
CC攻击可以归为DDoS攻击的一种.他们之间都原理都是一样的,即发送大量的请求数据来导致服务器拒绝服务,是一种连接攻击.CC攻击又可分为代理CC攻击,和肉鸡CC攻击.代理CC攻击是黑客借助代理服务器生 ...
- ShardingSphere-proxy-5.0.0建立mysql读写分离的连接(六)
一.修改配置文件config-sharding.yaml,并重启服务 # # Licensed to the Apache Software Foundation (ASF) under one or ...
- 毕设着急了吧?Python股票数据分析,制作动态柱状图
写在前面的一些屁话: 雪球成立于 2010 年,是北京雪球信息科技有限公司旗下推出的投资者社区.雪球一直致力于为中国投资者提供跨市场(沪深.香港.美国),跨品种(股票.基金.债券等)的数据查询.资讯获 ...
- SpringBoot项目启动org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException解决方法
将Pom文件中的SpringBoot版本调低即可. 我的是调成了2.5.6
- LeetCode. 812. 最大三角形面积
812. 最大三角形面积 鞋带公式 鞋带公式,用于计算任意多边形的面积,可用于计算三角形的面积 已知 ΔABC 三个顶点的坐标 A:(x1,y1). B:(x2,y2). C:(x3,y3) 对应的矩 ...
- Fleet 使用感受
1. 前言 笔者主要使用的编程语言是 Java.平时使用的 IDE 是 JetBrains 公司的 IntelliJ IDEA.有时候也会打开该公司旗下的 PyCharm.DataGrip.WebSt ...
- 常用类-jdk8之前的日期和API
一.System静态方法 点击查看代码 package com.Tang.StringDay01; import org.junit.Test; public class DateTimeTest { ...
- 牛客SQL刷题第三趴——SQL必知必会
01检索数据 SQL60 从 Customers 表中检索所有的 ID 编写 SQL 语句,从 Customers 表中检索所有的cust_id select * from Customers; SQ ...
- 沁恒CH32V103C8T6(二): Linux RISC-V编译和烧录环境配置
目录 沁恒CH32V103C8T6(一): 核心板焊接和Windows开发环境配置 沁恒CH32V103C8T6(二): Linux RISC-V编译和烧录环境配置 硬件准备 CH32V103 开发板 ...
- Solution -「HNOI2013」消毒
弱化一下,先考虑在二维上解决问题. 题目就转化为:有 \(n\) 个点 \((i, j)\) 需要被覆盖,而我们每次可以选一行或一列去覆盖,求覆盖所有点的最少选择次数. 如果我们对于每一个 \((i, ...