英文:
Setting pointers to nil to prevent memory leak in Golang
问题
我正在学习Go语言,作为练习,我想要实现一个链表。为了参考,我查看了官方的Go代码(https://golang.org/src/container/list/list.go)。其中有一段代码让我印象深刻:
108 // remove removes e from its list, decrements l.len, and returns e.
109 func (l *List) remove(e *Element) *Element {
110 e.prev.next = e.next
111 e.next.prev = e.prev
112 e.next = nil // 避免内存泄漏
113 e.prev = nil // 避免内存泄漏
114 e.list = nil
115 l.len--
116 return e
117 }
我很好奇在这种情况下将指针设置为nil如何防止内存泄漏?如果可能的话,我想构建一个具有这个缺陷的程序,并在使用pprof进行分析时观察它(我将使用一个修改过的list.go版本,不包含这个将指针设置为nil的部分)。
为了更清楚地回答:如果其中一个节点有外部指针指向它,那么所有相邻的被移除的节点将通过该指针保持活动引用,不会被移除。
- 我们创建一个指向Node2的外部指针
- 我们从链表中移除节点2-4
- 在这一点上,你期望只有Node 1、2和5仍然存在,其余的节点应该被垃圾回收。然而,由于Node2仍然指向Node3等,整个链表仍然没有被回收。
英文:
I'm learning Go, and as an exercise I wanted to implement a linked list. For reference I looked at the official Go code (https://golang.org/src/container/list/list.go) . One thing that stuck with me are these lines:
108 // remove removes e from its list, decrements l.len, and returns e.
109 func (l *List) remove(e *Element) *Element {
110 e.prev.next = e.next
111 e.next.prev = e.prev
112 e.next = nil // avoid memory leaks
113 e.prev = nil // avoid memory leaks
114 e.list = nil
115 l.len--
116 return e
117 }
I am curious as to how does setting pointers to nil in this case prevent memory leaks? If possible I would like to construct a program which has this flaw and see it while profiling with pprof (I would use a modified verion of the list.go without this nil pointer setting).
For clarity of answer: If one of the nodes has an external pointer to it, then all of the adjacent removed nodes will have an active reference through that pointer and won't be removed.
- We create an external pointer pointing to Node2
- We remove nodes 2-4 from the list
- You would expect at this point only for the Node 1,2 &
5 to be alive and the rest to be GC-ed. However, due to Node2 still
pointing to Node3 & etc., the entire chain remains uncollected.
答案1
得分: 13
你的假设是正确的。如果有一组指针彼此相互指向,但没有任何指向该组成员的引用/指针,垃圾回收器将检测到该组为不可达,并将正确释放该组。
但是,关于内存泄漏的解释很简单。我们可以从列表中获取包含未导出的Element.next
和Element.prev
指针的list.Element
包装器,这些指针指向列表中的下一个和上一个元素。
当从列表中移除一个元素时,如果这些指针不被设置为nil
,它们将保留对下一个和上一个元素包装器的引用,包括与这些元素相关联的值。
看下面的例子:
var e2 *list.Element
func main() {
listTest()
fmt.Println(e2.Value)
// 在这一点上,我们期望列表中的所有内容随时都可以被垃圾回收,我们只有对e2的引用。
// 如果e2.prev和e2.next不被设置为nil,
// e1和e3将无法被释放!
}
func listTest() {
l := list.New()
e1 := l.PushBack(1)
e2 = l.PushBack(2)
e3 := l.PushBack(3)
// 现在列表为[1, 2, 3]
fmt.Println(e1.Value, e2.Value, e3.Value)
l.Remove(e2)
// 现在列表为[1, 3],不包含e2
}
在listTest()
函数中,我们构建了一个包含3个元素的列表,并将第二个元素存储在全局变量e2
中。然后我们移除了这个元素。现在我们期望除了e2
(以及其中包装的值)之外的所有内容在listTest()
返回时被垃圾回收,因为列表在listTest()
函数外部是不可访问的。是的,我们在e2
中有一个指向元素的指针,但是e2
(应该)与列表不再有任何关联,因为我们已经将其移除。
如果e2
中的prev
和next
指针不被设置为nil
,那么它们指向的元素中包装的值将永远无法被释放,递归地。但是由于List.Remove()
正确地将它们设置为nil
,在上面的例子中,e1
和e3
以及它们包装的值将在下一次垃圾回收运行时被释放。
英文:
Your assumptions are correct. If there is a group of pointers pointing to each other, but there is no reference / pointer to any member of this group, the group will be detected as unreachable by the garbage collector and will be freed properly.
But the explanation to memory leak is simple. We can get the list.Element
wrappers from the list which contain the unexported Element.next
and Element.prev
pointers to the next and previous elements in the list.
When removing an element from the list if these pointers would not be set to nil
, they would held references to the next and previous element wrappers, including the values associated with those elements.
See this example:
var e2 *list.Element
func main() {
listTest()
fmt.Println(e2.Value)
// At this point we expect everything from the list to be
// garbage collected at any time, we only have reference to e2.
// If e2.prev and e2.next would not be set to nil,
// e1 and e3 could not be freed!
}
func listTest() {
l := list.New()
e1 := l.PushBack(1)
e2 = l.PushBack(2)
e3 := l.PushBack(3)
// List is now [1, 2, 3]
fmt.Println(e1.Value, e2.Value, e3.Value)
l.Remove(e2)
// Now list is [1, 3], it does not contain e2
}
In listTest()
we build a list with 3 elements, and we store the 2nd element in a global variable e2
. Then we remove this element. Now we would expect that except e2
(and the value wrapped in it) everything else gets garbage collected when listTest()
returns, because the list is not accessible outside the listTest()
function. Yes, we have a pointer in e2
to an element, but e2
has (should have) nothing to do with the list anymore as we removed it.
If the prev
and next
pointers in e2
would not be set to nil
, values wrapped in elements pointed by them could never be freed, recursively. But since List.Remove()
properly sets those to nil
, in the above example e1
and e3
–along with the values wrapped in them– will be freed (on next garbage collection run).
答案2
得分: 0
Golang的垃圾回收器基于三色标记-清除算法。
简而言之,你的程序使用的每个内存都与一个颜色相关联。颜色决定了内存是否应该被回收。
如果某个内存没有被直接或间接引用,该算法将标记该内存以便释放。但是,如果我们看一下代码:
e.prev.next = e.next
e.next.prev = e.prev
这将把e.next中的指针复制到e.prev.next中。现在,假设你想通过一个新创建的完整元素来更新e.prev.next。
之前移除的元素不会被回收,因为它仍然被e.next引用。
这就是为什么存在这些代码行的原因:
e.next = nil // 避免内存泄漏
e.prev = nil // 避免内存泄漏
这样可以防止旧引用的存在,从而防止内存泄漏。
英文:
Golang garbage collector is based on the tri-color mark-and-sweep algorithm.
In short, every memory you're program use is associated to a color. The color determine if the memory shall be garbaged or not.
This algorithm will flag a memory to be freed if this memory is not referenced somewhere (directly and indirectly). But if we take a look at the code:
e.prev.next = e.next
e.next.prev = e.prev
This copy the pointer in e.next into e.prev.next. Now, let's say you want to update e.prev.next by a new fully created element.
The previously removed element won't be garbaged because it is still referenced by e.next.
This is why those lines exist:
e.next = nil // avoid memory leaks
e.prev = nil // avoid memory leaks
This prevent from leaving old references, and thus prevent from memory leaks.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论