2019-3-26更新:

这个bug已经在go1.12修复了

=======

之前从一个朋友手上拿到一份代码,运行起来会不停吃内存,但是从代码本身看,好像又不应该:

类似一个单链表,每次申请一个节点接在尾部,然后curr指向最后一个节点,按理说在for循环中,curr引用的只有最后一个节点,而最后一个节点的next是nil,不引用任何其他数据,所以前面的节点应该都会被gc释放才对,但测试结果是内存不停涨。虽然没仔细分析过源码,但还是基本了解go的gc做法的,不可能是由于不及时导致

之后打印了相关内存地址才反应过来,是第一个Node的new分配在栈上了,go编译器会对代码做逃逸分析,如果在函数中new分配一个小对象,而且这个小对象不会逃逸出去,则可以直接将其分配到栈上,所以上面代码在优化后相当于:

n把持着链表头结点,整个链表都不会被gc

不仅是new,&Node{}这种写法,或像n这样直接定义的局部变量都会被做分析,来选择一个“最合适”的分配方案,并没有绝对的在栈上或堆上申请的说法,go认为开发者不需要知道这些细节,都由编译器智能判断

但是回到上面例程,由于没有n,所以从程序员角度,头结点是不可达的,没有任何正常办法去访问到,只是因为优化策略而没有释放掉(被main的栈帧把持),这个还是会造成一定困扰

那么函数返回后是不是一定就能被gc呢,改成这样:

结果这种情况下还是木有释放,这就有点可怕了,应该也不是释放了但没归还os的原因,毕竟这个内存大小不是一个小数目,而且如果我们想办法调整f的代码,让第一个new不被优化到栈上申请(比如弄个pcurr := new(*Node),然后把curr用*pcurr代替,即使用二级指针),内存是会被正常回收的

猜测go的实现中,虽然f返回了,但是所在的栈由于和main还在一个栈帧对象,所以还是保留,但不知道要保留多久了,这种事情碰上了可能会很难查

我是补充的分割线====================================

经过评论区讨论,大致有一些结论:

1 这是一个bug,大体上说,就是一个函数内部的struct临时对象若被编译器决定申请在栈上,则可能不会被gc回收,即认为可达,若这个struct内部存在指针引用,则引用的对象也都可达,唐生提供的两个链接里描述了这个问题

2 若指定runtime.GC(),是起作用的,会回收垃圾,但内存是否释放和go的os memory管理机制有关

3 原文中对于第二个例子没有回收的说法是错误的,其实是因为观察方法不对,正确的做法是:

也就是说,函数返回后,栈上的数据还是会作为不可达来处理的,所以正常业务开发的时候,这个问题看起来也不算严重(从main到main_loop这一段的临时对象容易出事,注意一下即可)

4 修改这个代码,使得内存能正常回收的几个办法: