標籤:

Go垃圾回收?

下面的程序,每次循環中,上次申請的silice就不可達了,為是么垃圾回收沒有回收,而最終導致內存不夠用?

func main() {

for i := 0; i &< 100; i++ {

c := make([]int64, 10000000000)

fmt.Println(len(c))

}

}


不關 GC 事,make([]int64, 10000000000) 是 74G,而你的機器沒有開 overcommit_memory,所以分配失敗了

echo 1 &> /proc/sys/vm/overcommit_memory

再執行就可能被內核殺,而不是 go 運行時殺了。因為這個時候 malloc 不會失敗,但 go 的數組是初始化填 0 的,初始化的時候會觸發內核 oom killer。


@唐生 大大的回答就是正解了,請給他的回答點贊。關鍵點就在於「zeroing」——GC堆上申請的空間要被清零,在Linux這種commit on-demand的環境中最遲在「往內存寫入0」的動作會觸發其對應的內存被實際commit,沒那麼多內存可用的話直接就要被系統OOM killer幹掉了。

我就來跑個題而已。把題主給的Go的例子換成Java,稍微改寫一下方便演示。

(註:當然,Go的slice跟Java的數組並不直接對等;硬要說的話它某些方面像Java數組(例如長度不是類型的一部分),而另一些方面像Java的ArrayList(例如帶有一層間接,可以支持擴容等)。正因為Go的slice可以擴容,slice這個類型上並沒有保證len是不可變的,所以不像Java的數組的length有保證不可變而可以做進一步優化。

不過就題主的例子而言,未來Go的編譯器優化能力提升之後,還是有機會達到下面演示的效果的。)

public class DemoEA {
public static final long LEN_IN_GO_DEMO = 10_000_000_000L;
public static final int LEN_IN_THIS_DEMO = (int) LEN_IN_GO_DEMO / 10;

static {
System.out.println();
}

public static void main(String[] args) {
// for (int i = 0; i &< 100; i++) { long[] c = new long[LEN_IN_THIS_DEMO]; // ~7.4GB System.out.println(c.length); // } } }

這裡把題主的例子里slice長度給縮小到1/10,是因為Java語言規範規定數組長度一定要是int類型的,而題主例子里的長度超範圍了。不過這不重要。循環也給注釋掉了因為也不重要。

用GraalVM 0.22版(使用Graal作為JIT編譯器的HotSpot VM)來跑這個測試,並且強制讓main()一開始就被JIT編譯好,會看到main()對應的機器碼是這樣的:

DemoEA.main (null) [0x000000010bc00da0, 0x000000010bc00e10] 112 bytes
[Entry Point]
[Verified Entry Point]
[Constants]
# {method} {0x000000011563fb58} main ([Ljava/lang/String;)V in DemoEA
# parm0: rsi:rsi = [Ljava/lang/String;
# [sp+0x20] (sp of caller)
0x000000010bc00da0: mov %eax,-0x14000(%rsp)
0x000000010bc00da7: sub $0x18,%rsp
0x000000010bc00dab: mov %rbp,0x10(%rsp)
0x000000010bc00db0: movabs $0x1184420a0,%rsi ;*getstatic out {reexecute=0 rethrow=0 return_oop=0}
; - DemoEA::main@5 (line 12)
; {oop(a java/lang/Class = java/lang/System)}
0x000000010bc00dba: mov 0xa8(%rsi),%rsi ; OopMap{rsi=Oop off=33}
;*ldc {reexecute=1 rethrow=0 return_oop=0}
; - DemoEA::main@0 (line 11)

0x000000010bc00dc1: test %eax,(%rsi)
0x000000010bc00dc3: mov $0x86796cc,%edx ;*invokevirtual println {reexecute=0 rethrow=0 return_oop=0}
; - DemoEA::main@10 (line 12)

0x000000010bc00dc8: nopl 0x0(%rax)
0x000000010bc00dcf: callq 0x000000010bba9020 ; OopMap{off=52}
;*invokevirtual println {reexecute=0 rethrow=0 return_oop=0}
; - DemoEA::main@10 (line 12)
; {optimized virtual_call}
0x000000010bc00dd4: nop ;*invokevirtual println {reexecute=0 rethrow=0 return_oop=0}
; - DemoEA::main@10 (line 12)

0x000000010bc00dd5: mov 0x10(%rsp),%rbp
0x000000010bc00dda: add $0x18,%rsp
0x000000010bc00dde: test %eax,-0x1cc0dde(%rip) # 0x0000000109f40006
; {poll_return}
0x000000010bc00de4: vzeroupper
0x000000010bc00de7: retq

用Java來表示就是:

public static void main(String[] args) {
System.out.println(1_000_000_000);
}

那個大數組的分配直接被消除了,所以它只要在語言規範允許的範圍內有多大並不會影響實驗結果——不會由於大數組分配而觸發GC,是否放在循環里也無所謂。我這裡實驗把循環注釋掉主要是為了讓輸出的機器碼短一點,不然Graal會默認循環展開很多次,讀著麻煩。

* 實驗的命令是:

$ graalvm-0.22/bin/java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:-TieredCompilation -Xcomp -XX:CompileCommand=compileonly,DemoEA,main -XX:CompileCommand=dontinline,java/*,* -XX:+PrintCompilation -XX:-UseCompressedOops DemoEA

好玩不? ^_^

註:完全相同的實驗在原裝HotSpot VM上用C2作為JIT編譯器則達不到這個效果。C2會通過常量傳播把 System.out.println(c.length) 變成 System.out.println(1_000_000_000),但那個無用的數組分配還是給留著了…所以這裡我換了個JIT編譯器來演示。嘻嘻,反正選擇多。


把make的數據規模降低到十分之一,在我的機器上是測試ok了,雖然有點慢,說明還是有釋放的,否則100個slice無論如何也超了

不過,go的確有gc方面的問題,之前有個朋友給go在git提了一個示常式序,證明在某些情況下並不會及時釋放(實際那個例子沒有釋放)


關於內存不夠用,大佬已經回答了。

關於不可達和回收,沒有哪個GC能在unreachable的第一現場就回收資源的。

如果想看object什麼時候被GC發現unreachable,可以這麼寫

package main

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

func main() {
for i := 0; i &< 10; i++ { c := make([]int64, 10) func(i int) { runtime.SetFinalizer(c, func(interface{}) { fmt.Printf("%d unreachable ", i) }) }(i) fmt.Println(len(c)) } runtime.GC() time.Sleep(10 * time.Second) }

https://golang.org/pkg/runtime/#SetFinalizer


推薦閱讀:

Go語言為什麼沒有流行起來?
如何評價三年前四大系統編程語言大牛的那場談論?
Go與Python ?
Golang 在國內是否過譽了?

TAG:Go語言 |