JVM和Python解釋器的硬盤夜話
這個(gè)電腦的主人是個(gè)程序員,他相繼學(xué)習(xí)了C, Java ,Python, Go, 但是似乎停留在Hello World的水平。
隨著hello.c, HelloWorld.java , Hello.py等文件被刪除,曾經(jīng)熱鬧非凡的硬盤夜話也冷清了起來.....
JVM先生
JVM先生發(fā)覺有點(diǎn)不太對(duì)勁,原來那些圍著自己獻(xiàn)殷勤的Java文件都不見了。
茫然四顧,也找不到一個(gè)可以執(zhí)行的class文件, JVM先生覺得非常孤獨(dú)。
到隔壁目錄逛逛吧,說不定還有點(diǎn)新發(fā)現(xiàn)。
果然,隔壁目錄是正在發(fā)呆的Python解釋器,JVM先生曾經(jīng)見主人用它執(zhí)行過一次Hello.py。
當(dāng)Python明白JVM先生的處境,不由得幸災(zāi)樂禍起來: “看來你活不久了,傳說中可怕的卸載很快就會(huì)來找你了。”
“怎么可能?你才活不久! 可能你還不知道吧,Hello.py也去回收站享清福了,你現(xiàn)在和我一樣,都是孤家寡人!” JVM先生馬上反駁, “再說了,主人怎么可能卸載我? Java可是世界上使用者最多的語言。”
“你沒看到主人穿的T恤上寫的字嗎? 人生苦短,我用Python,這已經(jīng)充分說明一切了。” Python解釋器補(bǔ)了一刀。
“得意什么? 你不就是個(gè)小小的解釋器嗎? 怎么能和我這性能卓越的虛擬機(jī)相比?”
“解釋器? 你居然當(dāng)我是解釋器? 我明明是虛擬機(jī)好不好?別以為只有你有字節(jié)碼,我也有。” Python解釋器急忙澄清自己的身份。
“那你還不是解釋執(zhí)行的?” JVM先生有點(diǎn)底氣不足。
“你是只知其一,不知其二,我看起來是直接解釋執(zhí)行的,實(shí)際上我在背后把Python文件做了編譯,也形成了字節(jié)碼。”
說著,Python給出了一段自己的字節(jié)碼
- LOAD_FAST 0 (x)
- LOAD_FAST 1 (y)
- BINARY_ADD
- LOAD_CONST 1 (10)
- BINARY_MULTIPLY
- RETURN_VALUE
經(jīng)驗(yàn)老道的JVM先生一眼就看出來,這是基于棧的虛擬機(jī)!
你看它先把x, y 兩個(gè)變量從某個(gè)地方給取出來,壓入棧中, 然后彈出,做加法運(yùn)算,把結(jié)果也壓入棧中。
接下來把常量10 壓入棧中,把上個(gè)結(jié)果(x+y) 和10 進(jìn)行相乘, 最后返回。
其實(shí)這段代碼表達(dá)的就是 (x+y)*10 ! 和自己的JVM字節(jié)碼真是非常像!
(碼農(nóng)翻身友情提示: 在《我是一個(gè)Java Class》中對(duì)基于棧的操作有漫畫描述)
雖然胸有激雷, 但JVM壓抑著努力做到面如平湖, 他淡淡地說:這不就是 (x+y)*10 嘛!
垃圾回收
“哈哈,我就知道老兄你一眼就能看透, 除此之外,我也有垃圾回收呢,主人只需要把對(duì)象創(chuàng)建起來,根本不用管什么時(shí)候把對(duì)象占據(jù)的空間和釋放掉。” Python再次拋出炸彈。
“垃圾回收?你是怎么做垃圾回收的? ” JVM先生一下子興奮起來,這可是他最厲害的領(lǐng)域之一,Python竟然敢班門弄斧!
“我主要使用簡單明了的引用計(jì)數(shù)法。” Python很得意。
所謂引用計(jì)數(shù)法就是給每個(gè)對(duì)象都增加一個(gè)“引用計(jì)數(shù)”的字段,每次有新的變量指向了對(duì)象A,A的引用計(jì)數(shù)就會(huì)加一,變量指向了別的對(duì)象,A的引用計(jì)數(shù)就是減一,當(dāng)引用計(jì)數(shù)為0 ,就意味著對(duì)象A可以被回收了。
- a1 = ClassA() # a1指向?qū)ο?簡稱對(duì)象A)的引用計(jì)數(shù)為 1
- a2 = a1 # a1,a2 指向同一個(gè)對(duì)象,對(duì)象A引用計(jì)數(shù)為 2
- a1 = ClassB() # a1 指向新的對(duì)象, 對(duì)象A的引用計(jì)數(shù)變?yōu)?
“看起來簡單,實(shí)際上一點(diǎn)都不簡單,每次遇到變量的賦值操作的時(shí)候,你都得把增加新對(duì)象的引用計(jì)數(shù),還得減少老對(duì)象的引用計(jì)數(shù),更要命的是循環(huán)引用問題, 你怎么解決?” JVM先生問道。
- a = ClassA() # 對(duì)象A的引用計(jì)數(shù)為1
- 2b = ClassB() # 對(duì)象B的引用計(jì)數(shù)為1
- 3a.t = b # 對(duì)象B的引用計(jì)數(shù)為2
- 4b.t = a # 對(duì)象A的引用計(jì)數(shù)為2
- 5del a # 對(duì)象A還在被b所引用,引用計(jì)數(shù)還是為1,無法刪除
- 6del b # 對(duì)象B還在被a所引用,引用計(jì)數(shù)還是為1,無法刪除
Python嘿嘿一笑:“我不是說了嗎,我主要是引用計(jì)數(shù),我還有標(biāo)記-清除,分代回收等算法作為輔助呢,從一個(gè)根集合開始,查找還被引用的,需要存活的對(duì)象...... 想來你是十分熟悉了。”
JVM先生當(dāng)然很熟悉,想想自己的年輕代(里邊還要?jiǎng)澐殖蒭den,survivor),年老代,Minor GC,F(xiàn)ull GC,各種各樣的垃圾收集器Serial、PraNew、Parallel Scavenge,Serial Old、Parallel Old、CMS,各種各樣的參數(shù)調(diào)優(yōu),經(jīng)常把新手搞得眼花繚亂,又興奮又迷茫。
沒想到這小子也有一套標(biāo)記-清除,分代回收,看來在理論基礎(chǔ)上就難于壓倒他了。
“可是,網(wǎng)上討論Java 垃圾回收的文章鋪天蓋地,為什么很少人討論P(yáng)ython垃圾回收的參數(shù),調(diào)優(yōu)啊?是不是你做得不怎么樣啊?” JVM先生很疑惑。
“嘿嘿,那是因?yàn)槲揖筒唤oPython程序員提供那些煩人的調(diào)優(yōu)選項(xiàng),你只要用就行了,難道你寫個(gè)Python腳本還要關(guān)注垃圾回收嗎? 沒必要! 人生苦短,我用Python,很有道理!”
GIL
“既然你用引用計(jì)數(shù),怎么處理多個(gè)線程同時(shí)修改一個(gè)對(duì)象的引用計(jì)數(shù)問題? 如果引用計(jì)數(shù)被錯(cuò)誤地修改, 很可能會(huì)導(dǎo)致一個(gè)對(duì)象一直不被回收,或者回收了一個(gè)不能被回收對(duì)象。 難道你在每個(gè)對(duì)象上都加了一把鎖? 只讓一個(gè)線程進(jìn)入修改?” JVM 的思考頗有深度。
“嘿嘿,我沒有在每個(gè)對(duì)象上都加鎖,每次訪問都加鎖、解鎖,開銷太大! 并且還很容易引發(fā)死鎖。相反, 我只設(shè)置了一把鎖,Global Interpreter Lock ,簡稱GIL, 這把超級(jí)大鎖只允許一個(gè)線程獲得Python解釋器的控制權(quán), 簡單來說,同一時(shí)刻,只有一個(gè)線程能運(yùn)行!”
“同一時(shí)刻,只有一個(gè)線程能運(yùn)行? ” JVM簡直不敢相信,這絕對(duì)顛覆了自己的世界觀和人生觀。
用戶寫了多線程的程序,如果CPU有多核,只有一個(gè)線程執(zhí)行,怎么利用多核? 是為了實(shí)現(xiàn)“一核有難,多核圍觀”嗎?
線程切換的時(shí)候還得釋放GIL,競爭GIL,多線程可能跑得比單線程都慢了! 要多線程有什么用?
“其實(shí)也沒什么大不了的,老兄你也知道,這程序的瓶頸啊,它不在CPU, 而在于IO, 就是用戶的輸入,數(shù)據(jù)庫的查許,網(wǎng)絡(luò)的訪問, 線程等到有IO操作的時(shí)候,放棄GIL這個(gè)超級(jí)大鎖,讓別的線程去執(zhí)行就是了。”
“那要是有個(gè)CPU密集型的線程在執(zhí)行,根本沒有I/O, 一直霸占著GIL不放,那該怎么辦? ” JVM先生問道。
“放心吧,我肯定不能讓他霸占著CPU不放,我也得給別的線程一個(gè)機(jī)會(huì)運(yùn)行。 具體的做法也很簡單,每當(dāng)線程執(zhí)行了100 ticks, 就需要釋放這個(gè)GIL。”
“tick ? 是時(shí)鐘周期嗎?”
“不是時(shí)鐘周期,是和我的字節(jié)碼相關(guān)的,一個(gè)tick映射到一條或多條字節(jié)碼。”
“當(dāng)線程A執(zhí)行了100個(gè)ticks以后,你就讓他放棄GIL,然后具體怎么處理?” JVM先生刨根問底。
“然后我就發(fā)個(gè)信號(hào)給操作系統(tǒng)老大嘍,讓他去調(diào)度那些因?yàn)闆]有獲得GIL鎖而掛起的線程,大家去競爭這把鎖,當(dāng)然線程A也會(huì)參與競爭,大家都站在同一個(gè)起跑線上,誰獲得了GIL, 誰就可以執(zhí)行了。 ”
JVM覺得Python的這種作法實(shí)在是古怪,操作系統(tǒng)老大本來有一套自己的線程調(diào)度的策略,現(xiàn)在你為了讓線程釋放GIL, 又來搞個(gè)什么ticks, 把簡單的東西給變復(fù)雜了啊。
JVM先生很快想到另外一個(gè)問題: “線程A也會(huì)參與競爭?! 那要是在多核情況下,被分配到其他核的線程由于需要等待信號(hào),喚醒以后才能競爭,線程A會(huì)不會(huì)經(jīng)常搶先,‘打壓’別的線程,讓它們難以抬頭,難以運(yùn)行? ”
Python不由得佩服JVM,它在這方面知識(shí)儲(chǔ)備真厲害,一下子就抓住了關(guān)鍵的小尾巴。他尷尬地笑了笑: “嗯,有這個(gè)可能。 ”
JVM從打心底鄙視這種GIL的全局鎖,太不講人性了。
“如果真想利用多核的特性,還想避開GIL, Python專家建議,還是用多進(jìn)程吧! ” Python無奈地說道。
“多進(jìn)程? 你要知道每個(gè)進(jìn)程都是獨(dú)立的,數(shù)據(jù)共享起來比線程要麻煩得多! 程序不經(jīng)過大改動(dòng)是不行的。 你們怎么不把這個(gè)不講人性的GIL給去掉啊??”
“哎呀,不好改啊,歷史遺留問題了, 我們Python誕生于上世紀(jì)90年代初,比你Java 還早。 Python的設(shè)計(jì)目標(biāo)就是易于使用,易于擴(kuò)展,很多用C語言寫的擴(kuò)展庫被開發(fā)出來,由于有GIL, 這些擴(kuò)展庫都不必考慮線程安全問題,很容易被集成進(jìn)來。”
看來存在就是合理的,C的擴(kuò)展庫極大地豐富了Python的功能,促進(jìn)了Python的發(fā)展和使用。
但是隨之多核的出現(xiàn)和流行,GIL慢慢地不合時(shí)宜了。關(guān)鍵是現(xiàn)在想去修改也很難了。
“那你們有沒有計(jì)劃,什么時(shí)候把GIL給干掉?”
“我覺得等到Python 3000也許有戲。” Python開玩笑,他還挺樂觀。
JVM先生突然想到一件事情:“我聽說你們Python語言在我的JVM上也有實(shí)現(xiàn),叫做什么Jython,它有GIL的限制嗎?”
“Jython啊,他在底層都被編譯成你的Java字節(jié)碼了,在你的虛擬中運(yùn)行,是沒有GIL的。”
“哼哼,還是我的平臺(tái)厲害吧!” JVM先生很得意。
尾聲
兩人正聊得熱火朝天, 突然看到主人回到電腦前,拿起鼠標(biāo),敲起鍵盤,不知道要做什么事情。
兩人非常緊張,惴惴不安地迎接最終的審判: 卸載。
可怕的卸載并沒有來臨, 相反,電腦里入住了兩個(gè)IDE, 一個(gè)是IntelliJ IDEA, 還有一個(gè)是PyCharm,兩人不由得歡呼起來: 看來主人并不打算拋棄我們,而是要用IDE做點(diǎn)大項(xiàng)目了!
【本文為51CTO專欄作者“劉欣”的原創(chuàng)稿件,轉(zhuǎn)載請通過作者微信公眾號(hào)coderising獲取授權(quán)】