Go memory leak in background goroutine

People coming from C or C++ may think that there is no memory leak in languages with garbage collection. But the truth is:

Memory leak does exist in languages with automatic garbage collection.

In perspective of human beings, garbage means something will never being used after sometime. Unfortunately, computer can’t understand this. Computer uses a concept unreachable to detect whether an object is garbage or not. There is a gap, as you may already known, that is “reachable but no longer used”.

Here, I show you a memory leak case using Go. Also, it can happen in other garbage collection language, like Java. Goroutine is so lightweight and convenient in Go. Sometimes, you may use it to do some background jobs. Suppose, I write following code for a database library:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package main

import (
"fmt"
"runtime"
"sync"
"time"
)

type Request struct {
}

type DB struct {
closed bool
reqC chan Request
mutex sync.Mutex
}

func (db *DB) Close() {
db.mutex.Lock()
if db.closed {
db.mutex.Unlock()
return
}
db.closed = true
db.mutex.Unlock()

fmt.Println("db closing")
close(db.reqC)
runtime.SetFinalizer(db, nil)
}

func backgroundWork(db *DB) {
for range db.reqC {
}
fmt.Println("db channel closed")
}

func Open(address string, name string) (*DB, error) {
db := &DB{reqC: make(chan Request, 100)}
runtime.SetFinalizer(db, (*DB).Close)
go backgroundWork(db)
return db, nil
}

func test() {
Open("tcp://127.0.0.1:4444", "test")
}

func main() {
test()
for {
time.Sleep(5)
}
}

You may find that, database object created in function test is a garbage, but it never and will never got collected. The object is reachable from function backgroundWork called in goroutine fired by go backgroundWork(db) in Open.

If we want garbage collector to collect object for us, we should not reference the object in any case. In above code, we should not reference database object in backgroundWork. Let us make some changes:

1
2
3
4
5
6
7
8
9
10
11
12
func backgroundWork(reqC chan Request) {
for range reqC {
}
fmt.Println("db channel closed")
}

func Open(address string, name string) (*DB, error) {
db := &DB{reqC: make(chan Request, 100)}
runtime.SetFinalizer(db, (*DB).Close)
go backgroundWork(db.reqC)
return db, nil
}

Now, we run the programm and find that the database object got collected.

I recommend three principles when you use thread or goroutine as background workhorse for API objects:

  • Don’t reference, directly or indirectly, API objects in background threads or goroutines
  • Shutdown background threads or goroutines in API objects’ finalizer/closer function
  • Care about “double free” issue, including manual-manual, manual-automatic gc

If you do want reference API object in background workhorse, use its weak reference.