我是研究过jvm 所以在读这本书的时候总是先关注 python 的虚拟机。

关注python 的虚拟机,首先你要先了解 .py文件编译之后,在python虚拟机中是什么的结构;换句话说,要知道pyc 二进制文件字节码的 格式,文件魔术、字符表、字符串,常量,模块信息、字节码、方法和变量等信息。

然后pyc在虚拟机中执行时候,创建的对象信息是什么样的,即pyCodeObject.如图:


图中的pyCodeObject包含子模块(方法和类)和子pyCodeObject信息,这样指针链接关系,构成环境链,环境链(模块链)对于python虚拟机而言非常重要,是执行的关键!!

虚拟机是依据环境链需要和创建对应的pyFrameObject对象和其对应的栈结构。

然后再去了解虚拟机的架构,内存管理,垃圾回收,以及多线程的实现和一些机制。

对于虚拟机编译py之后,在内存上创建pycodeObject,在运行的之后,同时会创建pyFrameObject对象。这个对象对应的是一个栈结构,当pycodeObject第一次使用的时候,就会被虚拟机创建运行栈数据对象,pycodeObject和pyFrameObject一一对应。pyFrameObject里面包含了pycodeObject,同时包含栈信息,执行当前环境链上的前一个frame信息,builtin、local、global名字空间,当前字节码指令信息(在源代码的行数),上一个字节码,需要内存空间信息等。如图:


虚拟机执行过程中,在一个环境域中(作用域)执行的结果要保存到对应的pyFrameObject中,那么才能保证在不同的作用域取值的正确性,有效性。对于pyFrameObject的调用关系(pycodeObject中import)也会在pyFrameObject中的struck_frame *last_frame  形成栈依赖(调用)链。我们写一个python文件,文件中直接声明一个方法直接调用,python虽然也是面对对象的编程语言,但是对于开发者是可以面对过程的开发。只是虚拟机底层对函数进行了封装,函数也是对象(python中万物皆对象)。函数被封装为pyFunctionObject,其中包含了参数,函数名,func_doc,func_code,func_globals,func_module等等。func_code是指向func_code对象的指针,func_code也是对象,是代码指令对象。func_module是函数可调用的modules;func_global是函数的上下文或者函数可调用的空间。函数的调用指针指向pyFunctionObject,再转向pyCodeObject,其中涉及参数的传递,上下文的使用。


对于python类与对象,类有一个统一的缔造者Object 类,j基类Object类有一个type类型的metaclass,或者可以说是类对象模版。这点和python和javascript虚拟机很相似,js引擎对于方法和类都是有封装的,都是一个对象,且在js引擎中,函数和类都分别有自己的顶层模版。对于python class 创建一个对象,首先是class对象的创建,然后根据这个class对象去创建instance 对象。class对象的创建是获取pyTypeClassA(假如是classA类)的对象封装,其中包含类继承信息,class 对象在初始化的时候,调用type_new。这个方法首先做的就是获取父类信息(python支持多继承),然后调用metaclass模版去创建class对象。对于 对象instance的创建,虚拟机调用object_new 方法,直接获取class对象,然后就是type_call 区执行init初始化。


对于jvm 和 python 虚拟机有很多类似的地方,虽然两者一个是编译型语言,一个是脚本。但是这两者都是有源码解析生成中间文件,中间文件在被加载后生成字节码文件对象,然后虚拟机后面的执行都是和这个字节码对象有关。区别在于,jvm中每一个字节码文件就是class对象,在加载的时候就会创建类对象。而且方法是直接存储在类对象中,不会单独的封装成方法对象。但是python的方法对象和类对象的管理和js引擎类似,万物皆对象,对类和方法以及作用域都进行了对象的封装。


对于java创建一个对象,首先到方法区的常量池中,寻找该对象的类类型,(全局常量,类的字符信息表都存在常量池中)然后检测该类是不是已经初始化了(有没有创建类对象),如果是就是快速创建;否则就是慢创建。

快速创建:就是直接根据类对象创建,首先在eden区,分配一块地址,指针指向一块空区间,设置空区间的大小(int* FirstAddress和size)。然后初始化对象头信息,分代年龄,偏向id,偏向时间戳等。然后一些方法指针指向类对象信息,接着就是调整线程栈对象的指向,pc的调整。

慢创建:就是类没有初始化,先将类进行加载或者初始化,然后再创建。


对于js和python对象虽然不一定和java一致,但是大致过程肯定是一样;首先应该是符号的匹配,空间分配,初始化信息,调整引用指向。


对于php zend 虚拟机其对每一个php文件编译生成对应的指令数组oop_array和数据数组;对于php中的类,编译之后生成对应的数据结构,但是内部还是只是存储的属性变量数组指针和方法列表指针。函数列表中每一个指针指向这个方法的指令数组的首地址。对于指令执行的时候,启动zend.execute() c代码函数,将指令和数据指针交给函数去执行。zend_execute 调用指令的方式有CALL, SWICH和GOTO方式,call是直接的函数调用,建立函数栈帧,指令一次出入栈。goto是直接调用指令,execute.mian将指令指针直接移交给EBP指令栈顶寄存器。swich方式 是指令在两者之间的判断选择。对于zend虚拟机,其内部没有建立栈帧,只是对于对象数据和代码指令都是存储在分配的内存堆中。数据的存储数据结构主要就是hashtable和数组,指令的执行是依据zend execute.main 建立的唯一的栈帧去执行。

**************************************************************************************************************

插入说一下JIT动态编译:

静态编译优化和动态编译优化最大的不同是他们在编译时所得到的信息量的不同。静态编译在运行程序之前就把所有的执行代码编译完,这时编译器所接受的编译信息量是不够多的。比如说:某个函数是否是大量地被调用了,函数的实参是不是一直是一个常数,等等。 动态编译之于静态编译,缺点是它需要即时编译代码,但是有一个优点——编译器可以获得静态编译期所没有的信息。比如:通过运行时的profiling可以知道哪些函数是被大量使用的。在哪些execution path上哪些函数的参数一直都没有变,等等。不要小看这些信息,当即时编译器了解这些信息之后可以在短时间内编译出比静态编译器更优质的二进制码。举例来说,一般程序也遵循90-10原则,即运行时的90%里计算机是在处理其中10%的代码,寻找到这些执行热点代码进行深度优化能得到比静态编译更好的性能(因为已知更多信息量)。  然而现实是:即时编译的开销非常大,暂时还不能超越静态编译的总体性能。不过,一个动态语言(如JAVA,Python)有着静态语言(如C++)所没有的各种优势,必然是将来程序语言发展的方向。伴随着强大的需求,即时编译器在将来也会更加强大。


对于解释器语言来说(不管直接解释 AST 还是字节码),指令分发是个开销很大的过程(即一个很大的 switch case ,根据得到的指令决定解释器下一步要做什么),这样会导致这部分的 CPU 指令缓存命中率大幅下降。


对于动态类型语言来说,JIT 时还可以做类型特化。比如一个函数 add(x, y),纯解释的话,每次执行时需要判断 x 与 y 的类型,根据类型再做具体的操作(整数加法 / 浮点数加法 / 字符串拼接)。假使实际代码运行过程中,x 和 y 一直都是整型类型,这些操作也不能省略,导致性能下降。但是 JIT 的时候,可以将此函数编译成 add_int_int(x: int, y: int) 的形式,这样性能就和编译型语言完全相等了。


函数内联,JIT 的时候可以根据函数调用情况,将常用的一些函数做内联编译。

**************************************************************************************************************

对于js虚拟机(例如 V8)它是基于JIT,没有所谓的中间字节码文件,在运行时的时候会针对字节码的解析和优化处理(这样可以获取运行时的处理信息,以达到性能提升)。

Chakra(Microsoft Internet Explorer)
Nitro/JavaScript Core (Safari)
Carakan (Opera)
SpiderMonkey (Firefox)
V8 (Chrome, Chromium)


Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐