|
|
51CTO旗下网站
|
|
移动端
创建专栏

JVM和Python解释器的硬盘夜话

这个电脑的主人是个程序员,他相继学习了C, Java ,Python, Go, 但是似乎停留在Hello World的水平。随着hello.c, HelloWorld.java , Hello.py等文件被删除,曾经热闹非凡的硬盘夜话也冷清了起来.....

作者:刘欣|2018-09-18 15:58

这个电脑的主人是个程序员,他相继学习了C, Java ,Python, Go, 但是似乎停留在Hello World的水平。

随着hello.c, HelloWorld.java , Hello.py等文件被删除,曾经热闹非凡的硬盘夜话也冷清了起来.....

JVM先生

JVM先生发觉有点不太对劲,原来那些围着自己献殷勤的Java文件都不见了。

茫然四顾,也找不到一个可以执行的class文件, JVM先生觉得非常孤独。

到隔壁目录逛逛吧,说不定还有点新发现。

果然,隔壁目录是正在发呆的Python解释器,JVM先生曾经见主人用它执行过一次Hello.py。

当Python明白JVM先生的处境,不由得幸灾乐祸起来: “看来你活不久了,传说中可怕的卸载很快就会来找你了。”

“怎么可能?你才活不久! 可能你还不知道吧,Hello.py也去回收站享清福了,你现在和我一样,都是孤家寡人!” JVM先生马上反驳, “再说了,主人怎么可能卸载我? Java可是世界上使用者最多的语言。”

“你没看到主人穿的T恤上写的字吗? 人生苦短,我用Python,这已经充分说明一切了。” Python解释器补了一刀。

“得意什么? 你不就是个小小的解释器吗? 怎么能和我这性能卓越的虚拟机相比?”

“解释器? 你居然当我是解释器? 我明明是虚拟机好不好?别以为只有你有字节码,我也有。” Python解释器急忙澄清自己的身份。

“那你还不是解释执行的?” JVM先生有点底气不足。

“你是只知其一,不知其二,我看起来是直接解释执行的,实际上我在背后把Python文件做了编译,也形成了字节码。”

说着,Python给出了一段自己的字节码

  1. LOAD_FAST       0 (x) 
  2. LOAD_FAST       1 (y) 
  3. BINARY_ADD 
  4. LOAD_CONST      1 (10) 
  5. BINARY_MULTIPLY 
  6. RETURN_VALUE 

经验老道的JVM先生一眼就看出来,这是基于栈的虚拟机!

你看它先把x, y 两个变量从某个地方给取出来,压入栈中, 然后弹出,做加法运算,把结果也压入栈中。

接下来把常量10 压入栈中,把上个结果(x+y) 和10 进行相乘, 最后返回。

其实这段代码表达的就是 (x+y)*10 ! 和自己的JVM字节码真是非常像!

(码农翻身友情提示: 在《我是一个Java Class》中对基于栈的操作有漫画描述)

虽然胸有激雷, 但JVM压抑着努力做到面如平湖, 他淡淡地说:这不就是 (x+y)*10 嘛!

垃圾回收

“哈哈,我就知道老兄你一眼就能看透, 除此之外,我也有垃圾回收呢,主人只需要把对象创建起来,根本不用管什么时候把对象占据的空间和释放掉。” Python再次抛出炸弹。

“垃圾回收?你是怎么做垃圾回收的? ” JVM先生一下子兴奋起来,这可是他最厉害的领域之一,Python竟然敢班门弄斧!

“我主要使用简单明了的引用计数法。” Python很得意。

所谓引用计数法就是给每个对象都增加一个“引用计数”的字段,每次有新的变量指向了对象A,A的引用计数就会加一,变量指向了别的对象,A的引用计数就是减一,当引用计数为0 ,就意味着对象A可以被回收了。

  1. a1 = ClassA()   # a1指向对象(简称对象A)的引用计数为 1 
  2. a2 = a1         # a1,a2 指向同一个对象,对象A引用计数为 2 
  3. a1 = ClassB()   # a1 指向新的对象, 对象A的引用计数变为1 

“看起来简单,实际上一点都不简单,每次遇到变量的赋值操作的时候,你都得把增加新对象的引用计数,还得减少老对象的引用计数,更要命的是循环引用问题, 你怎么解决?” JVM先生问道。

  1. a = ClassA()   # 对象A的引用计数为1 
  2. 2b = ClassB()   # 对象B的引用计数为1 
  3. 3a.t = b        # 对象B的引用计数为2 
  4. 4b.t = a        # 对象A的引用计数为2 
  5. 5del a          # 对象A还在被b所引用,引用计数还是为1,无法删除 
  6. 6del b          # 对象B还在被a所引用,引用计数还是为1,无法删除 

Python嘿嘿一笑:“我不是说了吗,我主要是引用计数,我还有标记-清除,分代回收等算法作为辅助呢,从一个根集合开始,查找还被引用的,需要存活的对象...... 想来你是十分熟悉了。”

JVM先生当然很熟悉,想想自己的年轻代(里边还要划分成eden,survivor),年老代,Minor GC,Full GC,各种各样的垃圾收集器Serial、PraNew、Parallel Scavenge,Serial Old、Parallel Old、CMS,各种各样的参数调优,经常把新手搞得眼花缭乱,又兴奋又迷茫。

没想到这小子也有一套标记-清除,分代回收,看来在理论基础上就难于压倒他了。

“可是,网上讨论Java 垃圾回收的文章铺天盖地,为什么很少人讨论Python垃圾回收的参数,调优啊?是不是你做得不怎么样啊?” JVM先生很疑惑。

“嘿嘿,那是因为我就不给Python程序员提供那些烦人的调优选项,你只要用就行了,难道你写个Python脚本还要关注垃圾回收吗? 没必要! 人生苦短,我用Python,很有道理!”

GIL

“既然你用引用计数,怎么处理多个线程同时修改一个对象的引用计数问题? 如果引用计数被错误地修改, 很可能会导致一个对象一直不被回收,或者回收了一个不能被回收对象。 难道你在每个对象上都加了一把锁? 只让一个线程进入修改?” JVM 的思考颇有深度。

“嘿嘿,我没有在每个对象上都加锁,每次访问都加锁、解锁,开销太大! 并且还很容易引发死锁。相反, 我只设置了一把锁,Global Interpreter Lock ,简称GIL, 这把超级大锁只允许一个线程获得Python解释器的控制权, 简单来说,同一时刻,只有一个线程能运行!”

“同一时刻,只有一个线程能运行? ” JVM简直不敢相信,这绝对颠覆了自己的世界观和人生观。

用户写了多线程的程序,如果CPU有多核,只有一个线程执行,怎么利用多核? 是为了实现“一核有难,多核围观”吗?

线程切换的时候还得释放GIL,竞争GIL,多线程可能跑得比单线程都慢了! 要多线程有什么用?

“其实也没什么大不了的,老兄你也知道,这程序的瓶颈啊,它不在CPU, 而在于IO, 就是用户的输入,数据库的查许,网络的访问, 线程等到有IO操作的时候,放弃GIL这个超级大锁,让别的线程去执行就是了。”


“那要是有个CPU密集型的线程在执行,根本没有I/O, 一直霸占着GIL不放,那该怎么办? ” JVM先生问道。

“放心吧,我肯定不能让他霸占着CPU不放,我也得给别的线程一个机会运行。 具体的做法也很简单,每当线程执行了100 ticks, 就需要释放这个GIL。”

“tick ? 是时钟周期吗?”

“不是时钟周期,是和我的字节码相关的,一个tick映射到一条或多条字节码。”

“当线程A执行了100个ticks以后,你就让他放弃GIL,然后具体怎么处理?” JVM先生刨根问底。

“然后我就发个信号给操作系统老大喽,让他去调度那些因为没有获得GIL锁而挂起的线程,大家去竞争这把锁,当然线程A也会参与竞争,大家都站在同一个起跑线上,谁获得了GIL, 谁就可以执行了。 ”


JVM觉得Python的这种作法实在是古怪,操作系统老大本来有一套自己的线程调度的策略,现在你为了让线程释放GIL, 又来搞个什么ticks, 把简单的东西给变复杂了啊。

JVM先生很快想到另外一个问题: “线程A也会参与竞争?! 那要是在多核情况下,被分配到其他核的线程由于需要等待信号,唤醒以后才能竞争,线程A会不会经常抢先,‘打压’别的线程,让它们难以抬头,难以运行? ”


Python不由得佩服JVM,它在这方面知识储备真厉害,一下子就抓住了关键的小尾巴。他尴尬地笑了笑: “嗯,有这个可能。 ”

JVM从打心底鄙视这种GIL的全局锁,太不讲人性了。

“如果真想利用多核的特性,还想避开GIL, Python专家建议,还是用多进程吧! ” Python无奈地说道。

“多进程? 你要知道每个进程都是独立的,数据共享起来比线程要麻烦得多! 程序不经过大改动是不行的。 你们怎么不把这个不讲人性的GIL给去掉啊??”

“哎呀,不好改啊,历史遗留问题了, 我们Python诞生于上世纪90年代初,比你Java 还早。 Python的设计目标就是易于使用,易于扩展,很多用C语言写的扩展库被开发出来,由于有GIL, 这些扩展库都不必考虑线程安全问题,很容易被集成进来。”

看来存在就是合理的,C的扩展库极大地丰富了Python的功能,促进了Python的发展和使用。

但是随之多核的出现和流行,GIL慢慢地不合时宜了。关键是现在想去修改也很难了。

“那你们有没有计划,什么时候把GIL给干掉?”

“我觉得等到Python 3000也许有戏。” Python开玩笑,他还挺乐观。

JVM先生突然想到一件事情:“我听说你们Python语言在我的JVM上也有实现,叫做什么Jython,它有GIL的限制吗?”

“Jython啊,他在底层都被编译成你的Java字节码了,在你的虚拟中运行,是没有GIL的。”

“哼哼,还是我的平台厉害吧!” JVM先生很得意。

尾声

两人正聊得热火朝天, 突然看到主人回到电脑前,拿起鼠标,敲起键盘,不知道要做什么事情。

两人非常紧张,惴惴不安地迎接最终的审判: 卸载。

可怕的卸载并没有来临, 相反,电脑里入住了两个IDE, 一个是IntelliJ IDEA, 还有一个是PyCharm,两人不由得欢呼起来: 看来主人并不打算抛弃我们,而是要用IDE做点大项目了!

【本文为51CTO专栏作者“刘欣”的原创稿件,转载请通过作者微信公众号coderising获取授权】

戳这里,看该作者更多好文

【编辑推荐】

  1. 为什么Python这么慢?
  2. Python技巧 101:这17个骚操作你都Ok吗
  3. 这些Python代码技巧,你肯定还不知道
  4. 如何用Python做自动化特征工程
  5. TIOBE 9 月排行榜出炉,Python跻身前三,猜猜谁被挤掉
【责任编辑:武晓燕 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢