JVM和Python解释器的硬盘夜话
这个电脑的主人是个程序员,他相继学习了C, Java ,Python, Go, 但是似乎停留在Hello World的水平。
随着hello.c, HelloWorld.java , Hello.py等文件被删除,曾经热闹非凡的硬盘夜话也冷清了起来.....
(码农翻身友情提示:参见文末的硬盘夜话系列文章)
JVM先生发觉有点不太对劲,原来那些围着自己献殷勤的Java文件都不见了。
茫然四顾,也找不到一个可以执行的class文件, JVM先生觉得非常孤独。
到隔壁目录逛逛吧,说不定还有点新发现。
果然,隔壁目录是正在发呆的Python解释器,JVM先生曾经见主人用它执行过一次Hello.py。
当Python明白JVM先生的处境,不由得幸灾乐祸起来: “看来你活不久了,传说中可怕的卸载很快就会来找你了。”
“怎么可能?你才活不久! 可能你还不知道吧,Hello.py也去回收站享清福了,你现在和我一样,都是孤家寡人!” JVM先生马上反驳, “再说了,主人怎么可能卸载我? Java可是世界上使用者最多的语言。”
“你没看到主人穿的T恤上写的字吗? 人生苦短,我用Python,这已经充分说明一切了。” Python解释器补了一刀。
“得意什么? 你不就是个小小的解释器吗? 怎么能和我这性能卓越的虚拟机相比?”
“解释器? 你居然当我是解释器? 我明明是虚拟机好不好?别以为只有你有字节码,我也有。” Python解释器急忙澄清自己的身份。
“那你还不是解释执行的?” JVM先生有点底气不足。
“你是只知其一,不知其二,我看起来是直接解释执行的,实际上我在背后把Python文件做了编译,也形成了字节码。”
说着,Python给出了一段自己的字节码
LOAD_FAST 0 (x)
LOAD_FAST 1 (y)
BINARY_ADD
LOAD_CONST 1 (10)
BINARY_MULTIPLY
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可以被回收了。
1a1 = ClassA() # a1指向对象(简称对象A)的引用计数为 1
2a2 = a1 # a1,a2 指向同一个对象,对象A引用计数为 2
3a1 = ClassB() # a1 指向新的对象, 对象A的引用计数变为1
(可左右滑动)
“看起来简单,实际上一点都不简单,每次遇到变量的赋值操作的时候,你都得把增加新对象的引用计数,还得减少老对象的引用计数,更要命的是循环引用问题, 你怎么解决?” JVM先生问道。
1a = ClassA() # 对象A的引用计数为1
2b = ClassB() # 对象B的引用计数为1
3a.t = b # 对象B的引用计数为2
4b.t = a # 对象A的引用计数为2
5del a # 对象A还在被b所引用,引用计数还是为1,无法删除
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,很有道理!”
“既然你用引用计数,怎么处理多个线程同时修改一个对象的引用计数问题? 如果引用计数被错误地修改, 很可能会导致一个对象一直不被回收,或者回收了一个不能被回收对象。 难道你在每个对象上都加了一把锁? 只让一个线程进入修改?” 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做点大项目了!
相关阅读:
参考资料:
https://www.youtube.com/watch?v=ph374fJqFPE
https://realpython.com/python-gil/
(完)