一文读透php到底是不是最好的语言

2020-06-15 15:56:36 浏览数 (1)

导读: 说到php,一句顺口溜流传已广:php是世界上最好的语言;还有一个特别的谐音名字:拍xx片。至于php是不是世界上最好的语言,这个有意思的口号我们也探究一下他的来源。

程序猿的世界,自有自的丰富多彩,无论有人理解还是不理解。

说到面向对象,php 也是有的

代码语言:javascript复制
$mercedes = new Car ();
$bmw = new Car ();
$audi = new Car ();

好,立马拥有三个对象;有了三个对象,每个对象的具体参数有哪些呢,重量,排气量,承载人数,能干嘛呢,能开,能停,能播放音乐,能按喇叭,就是这个对象能做的事。简单的理解就是描述这个物体(对象)的特征和动作。

代码语言:javascript复制
<?php
class Car {
  /* 成员变量 */
  var $name;
  var $weight;
  var $emissions;
  
  /* 成员函数 */
 public  function setName($par){
     $this->name = $par;
  }
 public function getName(){
     return $this->name;
  }  
  
 public function run(){
     echo "run";
  }  
  
  public function parking(){
     echo "parking";
  }
  
}
?>

这样一个鲜活的对象出炉了,哈哈哈

面向对象还有很多内容,这里就不一一展开了,我们先来看看php的发展历程。

01 php发展历程

问世

1994年,拉斯姆斯·勒多夫(Rasmus Lerdorf)创造了 PHP,事实上这个时候的 PHP 只是用 Perl 语言编写的一系列 CGI 脚本,用于跟踪他在线简历的访问情况,统计他自己网站的访问者。勒多夫把这些 CGI 脚本命名为“Personal Home Page Tools”。这里我们姑且称之为 PHP 1 吧!但这个早期的 PHP Tools 并不是一门语言,只是一些工具,提供基本的变量,并使用嵌入式HTML句法自动处理表单变量。

PHP 2

由于勒多夫写的这个小程序轻巧且简便,吸引了很多人的关注,在1995年,勒多夫发布了 PHP/FI 2.0。这个FI是一个可以做 SQL 查询的东西,2.0是其更新版本。这次发布了一个基本完善的工具包,它不仅可以访问数据库,还能嵌入 HTML 中动态处理数据。新的工具包,吸引了很多的程序开发者,其中包括 Zeev Suraski(泽埃夫·苏拉斯基)和 Andi Gutmans(安迪·古曼兹),他们后来加入到了PHP3的开发当中。

PHP 3

在1997年中,开始了第三版的开发计划,开发小组加入了 Zeev Suraski 及 Andi Gutmans。1998年末,PHP 3 第一个官方正式版发行,其特点是具有更好的执行效果和更清晰的结构。此外,该版本最强大的地方在于它的可扩展性,这点吸引的大量的开发人员加入并提交新的 PHP 扩展模块。

这个全新的语言伴随着一个新的名称的发布,它标志着 PHP 不在仅仅是个人网页的小工具。尽管它还叫 PHP,但其全称叫“PHP: Hypertext Preprocessor”。这种递归的写法,并非 PHP 独创,众所周知,GUN 便是“GUN’s Not UNIX”的缩写。

PHP 4

在 PHP 3 发布不久,Zeev Suraski 及 Andi Gutmans 就开始投入到 PHP 4 的开发当中,主要目标是增强程序运行性能和 PHP 自身的模块性。新的 PHP 核心被称为“Zend”引擎(两名开发者的缩写),由 C 语言编写,相同的 PHP 脚本在新版本中运行,性能提高了近十倍。在千禧年(2000年),PHP 4 正式发布。

主要增加了以下特征:

各种web服务器的支持

丰富的数组操作函数

完整的会话机制

对输出缓存的支持

增加了对类和对象的支持,是 PHP 面向对象的雏形

新时代 PHP 5

尽管,PHP 发展势头之猛,但相比较其他语言如 Java 来说,还有很多问题。面向对象的支持不够完善,无法捕获异常(Exception)等,这导致多年以来 PHP 一直被认为是一门面向过程的语言(即使在 PHP 5 发布后)。2004年7月,PHP 5 正式发布,这标志着 PHP 一个新时代的到来。并且往后多年,PHP 一直在 5 这个版本上迭代,是 PHP 历时最长的一个大版本。

它的核心采用的是第二代 Zend 引擎,并引入了对 PECL 模块的支持。PHP 5 最大的特点是引入了面向对象的全部机制。

由于 PHP 5 小版本众多,在此列出各版本的年份与新特性:

PHP版本 发行年份 新特性

5.0 2004-07-13 -

5.1 2005-11-24 -

5.2 2006-11-02 -

5.3 2009-06-30 引入了命名空间

闭包和匿名函数

5.4 2012-03-01 引入性状(trait)

内置web服务器

5.5 2013-06-20 引入PHP生成器(generator)

MySQL扩展被废弃,可使用MySQLi或者PDO

5.6 2014-08-28 -

飞跃 PHP 7

2015.12.3 PHP 7 问世了,这是 PHP 的一次飞跃。PHP7 修复了大量 BUG ,新增了功能和语法糖。这些改动涉及到了核心包、 GD 库、 PDO 、 ZIP 、 ZLIB 等熟悉和不熟悉的核心功能与扩展包。PHP 7 移除了已经被废弃的函数,如 mysql_ 系列函数在 PHP 5.5 被废弃,在 PHP 7 被删除。PHP 7 的性能高于 HHVM 。并且是 PHP 5.6 的两倍。

php从以前到现在一直都是单继承的语言,无法同时从两个基类中继承属性和方法,为了解决这个问题,php出了Trait这个特性

PHP 在过去的几年里走过了漫长的道路。成长为处理 web 的最卓越的语言并非易事。

php在国内的发展:

最初php在新浪,百度内部大范围使用,有大量的php4.0代码,在当时新浪,百度作为大厂,对整个php的需求量很大;以百度惠新宸,金山张宴推荐nginx,lnmp环境为代表的个人使得php在国内快速成体系普及,一时之间惠新宸和张宴等人都成为广大phper的膜拜对象。惠新宸贡献的yaf框架在百度内部一度成为标准php框架,yaf框架名字上叫php框架,实际上是php c语言的扩展, 后面百度使用hhvm,它会将PHP代码转换成高级别的字节码(一种中间语言),在运行时即时(JIT)编译器会将这些字节码翻译成机器码如今鸟哥已是链家的vp,张宴后面好像是自己创业了(电商,待确定)。

( 来自百度百科)

在开源界的php更是精彩纷呈,phpcms,dedecms,帝国cms,discuz论坛(被腾讯收购),phpwind(被阿里收购),几乎大小网站都是用这些开源的cmd搭建,并且配有很好的模板自由更换,网店商城系统更有ecshop和ncshop等等,让建站变得十分简单,个人站长一时间风光无限,至今这些cms仍然是搭建基础网站最简单有效的方式,功能强大,灵活使用。

然而,随着互联网竞争的越来越同质化和激烈,百度关键字排名算法的人工干预更多,个人站长逐渐淡出互联网,影响力不再那么广,同时pc互联网像移动互联网转变,导致原来的bbs和个人网站逐渐退出,php的应用范围再一次缩减,随着大数据和云计算的到来,java应用面越来越多。

在java开始大面积向前发展的时候,php并没有停止脚步,大家都知道 PHP 7 发布的时候号称比 PHP 5.6 快两倍,那 PHP 7.3 的性能如何呢?Phoronix 在 PHP 7.3 Alpha1 发布时,曾进行过一项基准测试。结果发现在常用的 PHPBench 基准测试中,PHP 7.3.0 Alpha 1 比当前的 PHP 7.2 稳定版本快约 7% ,比 PHP 7.0 快 22% :

由于惠新宸 yaf框架的启发,国内其他c扩展框架也逐步开花,其中swoole就是根据互联网最新的应用场景发展一种新的解决方案,来自官方的说法:

使 PHP 开发人员可以编写高性能的异步并发 TCP、UDP、Unix Socket、HTTP, WebSocket 服务。Swoole 可以广泛应用于互联网、移动通信、企业软件、云计算、 网络游戏、物联网(IOT)、车联网、智能家居等领域。使用 PHP Swoole 作为 网络通信框架,可以使企业 IT 研发团队的效率大大提升,更加专注于开发创新产品

swoole更大程度以c扩展的形式解决了php在短链接上的短板,使得php也可以具有同java类似的相关生态;

在swoole基础上还有一个easyswoole,基于swoole扩展的框架封装,目前算是比较成熟,但是由于社区团队(个人)维护,毕竟精力有限,仔细研究后可以考虑。

php也出现了java生态一样的包管理工具composer,在开源界的框架ci,yii2,laravel,thinkphp独领风骚,在中国或者二线城市thinkphp已经成为标配,更符合中国人的使用习惯,文档非常全,larave显得更加骚气和编程之美,yii2提供的web组建widget也不错。

02 php zend vm 与java jvm

语言的本质都是编译解释为字节码(有的用解释,有的用编译,编译型语言常驻内存),最终由虚拟机编译成机器码,放在内存由cpu来执行;java有jvm,php也同样有zend vm,用来执行php的中间编译码opcode,php是一门解释性语言,也就是变编译成机器码边执行,如果代码没有改变,这个机器码是不会重新被编译的,也就是常称的php opcode cache,所以假设代码未改变的条件下php与编译型语言差别不会很大,更大的性能差异来自于执行各自中间码的效率,于是php有了编译成c 的hhvm来解决这个问题

字节码

  • 是一种包含执行程序、由一序列 op代码/数据对 组成的二进制文件。
  • 是一种中间码,它比机器码更抽象,需要直译器转译后才能成为机器码的中间代码。
  • 是编码后的数值常量、引用、指令等构成的序列。

机器码

  • 是电脑的CPU可直接解读的数据,可以直接执行,并且是执行速度最快的代码。

转换关系

  • 通常是有编译器将源码编译成字节码,然后虚拟机器将字节码转译为机器码

在讲其他语言之前,先来了解一下php虚拟机zend引擎,也相当于java的jvm,这样我们可以更直观的了解到相关语言的本质都是大同小异

PHP:一种解释型语言

PHP经常会被定义为“脚本语言”或者是“解释型语言”,什么是“解释型语言”呢?

所谓“解释型语言”就是指用这种语言写的程序不会被直接编译为本地机器语言(native machine language),而是会被编译为一种中间形式(代码),很显然这种中间形式不可能直接在CPU上执行(因为CPU只能执行本地机器指令),但是这种中间形式可以在使用本地机器指令(如今大多是使用C语言)编写的软件上执行。

在wiki中虚拟机的定义是:虚拟机(Virtual Machine),在计算机科学中的体系结构里,是指一种特殊的软件, 他可以在计算机平台和终端用户之间创建一种环境,而终端用户则是基于这个软件所创建的环境来操作软件。在计算机科学中,虚拟机是指可以像真实机器一样运行程序的计算机的软件实现。

虚拟机是一种抽象的计算机,它有自己的指令集,有自己的内存管理体系。在此类虚拟机上实现的语言比较低抽象层次的语言更加明了,更加简单易学。

PHP文件是如何被解析的,生成的中间代码表示什么,生成的中间代码与实际的PHP代码是如何对应的,生成的中间代码如何被执行的?在执行过程中会将会哪些中间的数据?整个虚拟机是否可以优化?如何优化?

Zend虚拟机体系结构

从概念层将Zend虚拟机的实现进行抽象,我们可以将Zend虚拟机的体系结构分为:解释层、执行引擎、中间数据层。

Zend虚拟机体系结构图

当一段PHP代码进入Zend虚拟机,它会被执行两步操作:编译和执行。对于一个解释性语言来说,这是一个创造性的举动,但是,现在的实现并不彻底。现在当PHP代码进入Zend虚拟机后,它虽然会被执行这两步操作,但是这两步操作对于一个常规的执行过程来说却是连续的, 也就是说它并没有转变成和Java这种编译型语言一样:生成一个中间文件存放编译后的结果。如果每次执行这样的操作,对于PHP脚本的性能来说是一个极大的损失。虽然有类似于APC,eAccelerator等缓存解决方案。但是其本质上是没有变化的,并且不能将两个步骤分离,各自发展壮大。

解释层

解释层是Zend虚拟机执行编译过程的位置。它包括词法解析、语法解析和编译生成中间代码三个部分。词法分析就是将我们要执行的PHP源文件,去掉空格,去掉注释,切分为一个个的标记(token), 并且处理程序的层级结构(hierarchical structure)。

语法分析就是将接受的标记(token)序列,根据定义的语法规则,来执行一些动作,Zend虚拟机现在使用的Bison使用巴科斯范式(BNF)来描述语法。编译生成中间代码是根据语法解析的结果对照Zend虚拟机制定的opcode生成中间代码, 在PHP5.3.1中,Zend虚拟机支持135条指令(见Zend/zend_vm_opcodes.h文件), 无论是简单的输出语句还是程序复杂的递归调用,Zend虚拟机最终都会将所有我们编写的PHP代码转化成这135条指令的序列, 之后在执行引擎中按顺序执行。

中间数据层

当Zend虚拟机执行一个PHP代码时,它需要内存来存储许多东西, 比如,中间代码,PHP自带的函数列表,用户定义的函数列表,PHP自带的类,用户自定义的类, 常量,程序创建的对象,传递给函数或方法的参数,返回值,局部变量以及一些运算的中间结果等。我们把这些所有的存放数据的地方称为中间数据层。

如果PHP以mod扩展的方式依附于Apache2服务器运行,中间数据层的部分数据可能会被多个线程共享,如果PHP自带的函数列表等。如果只考虑单个进程的方式,当一个进程被创建时它就会被加载PHP自带的各种函数列表,类列表,常量列表等。当解释层将PHP代码编译完成后,各种用户自定义的函数,类或常量会添加到之前的列表中, 只是这些函数在其自身的结构中某些字段的赋值是不一样的。

当执行引擎执行生成的中间代码时,会在Zend虚拟机的栈中添加一个新的执行中间数据结构(zend_execute_data), 它包括当前执行过程的活动符号列表的快照、一些局部变量等。

执行引擎

Zend虚拟机的执行引擎是一个非常简单的实现,它只是依据中间代码序列(EX(opline)),一步一步调用对应的方法执行。在执行引擎中没并有类似于PC寄存器一样的变量存放下一条指令,当Zend虚拟机执行到某条指令时,当它所有的任务都执行完了, 这条指令会自己调用下一条指令,即将序列的指针向前移动一个位置,从而执行下一条指令,并且在最后执行return语句,如此反复。这在本质上是一个函数嵌套调用。

回到开头的问题,PHP通过词法分析、语法分析和中间代码生成三个步骤后,PHP文件就会被解析成PHP的中间代码opcode。生成的中间代码与实际的PHP代码之间并没有完全的一一对应关系。只是针对用户所给的PHP代码和PHP的语法规则和一些内部约定生成中间代码, 并且这些中间代码还需要依靠一些全局变量中转数据和关联。至于生成的中间代码的执行过程是依据中间代码的顺利, 依赖于执行过程中的全局变量,一步步执行。当然,在遇到一些函数跳转也会发生偏移,但是最终还是会回到偏移点。

1.从物理机说起

虚拟机也是计算机,设计思想和物理机有很多相似之处;

1.1冯诺依曼体系结构

冯·诺依曼是当之无愧的数字计算机之父,当前计算机都采用的是冯诺依曼体系结构;设计思想主要包含以下几个方面:

  • 指令和数据不加区别混合存储在同一个存储器中,它们都是内存中的数据。现代CPU的保护模式,每个内存段都有段描述符,这个描述符记录着这个内存段的访问权限(可读,可写,可执行)。这就变相的指定了哪些内存中存储的是指令哪些是数据);
  • 存储器是按地址访问的线性编址的一维结构,每个单元的位数是固定的;
  • 数据以二进制表示;
  • 指令由操作码和操作数组成。操作码指明本指令的操作类型,操作数指明操作数本身或者操作数的地址。操作数本身并无数据类型,它的数据类型由操作码确定;任何架构的计算机都会对外提供指令集合;
  • 运算器通过执行指令直接发出控制信号控制计算机各项操作。由指令计数器指明待执行指令所在的内存地址。指令计数器只有一个,一般按顺序递增,但执行顺序可能因为运算结果或当时的外界条件而改变;

1.2汇编语言简介

任何架构的计算机都会提供一组指令集合;

指令由操作码和操作数组成;操作码即操作类型,操作数可以是一个立即数或者一个存储地址;每条指令可以有0、1或2个操作数;

指令就是一串二进制;汇编语言是二进制指令的文本形式;

代码语言:javascript复制
push   �x
mov    �x, [%esp 8]
mov    �x, [%esp 12]
add    �x, �x
pop    �x

push、mov、add、pop等就是操作码; �x寄存器;[%esp 12]内存地址; 操作数只是一块可存取数据的存储区;操作数本身并无数据类型,它的数据类型由操作码确定; 如movb传送字节,movw传送字,movl传送双字等

1.3 函数调用栈

过程(函数)是对代码的封装,对外暴露的只是一组指定的参数和一个可选的返回值;可以在程序中不同的地方调用这个函数;假设过程P调用过程Q,Q执行后返回过程P;为了实现这一功能,需要考虑三点:

  • 指令跳转:进入过程Q的时候,程序计数器必须被设置为Q的代码的起始地址;在返回时,程序计数器需要设置为P中调用Q后面那条指令的地址;
  • 数据传递:P能够向Q提供一个或多个参数,Q能够向P返回一个值;
  • 内存分配与释放:Q开始执行时,可能需要为局部变量分配内存空间,而在返回前,又需要释放这些内存空间;

大多数的语言过程调用都采用了栈数据结构提供的内存管理机制;如下图所示:

函数的调用与返回即对应的是一系列的入栈与出栈操作; 函数在执行时,会有自己私有的栈帧,局部变量就是分配在函数私有栈帧上的; 平时遇到的栈溢出就是因为调用函数层级过深,不断入栈导致的;

2.PHP虚拟机

虚拟机也是计算机,参考物理机的设计,设计虚拟机时,首先应该考虑三个要素:指令,数据存储,函数栈帧;

下面从这三点详细分析PHP虚拟机的设计思路;

2.1指

2.1.1 指令类型

任何架构的计算机都需要对外提供一组指令集,其代表计算机支持的一组操作类型;

PHP虚拟机对外提供186种指令,定义在zend_vm_opcodes.h文件中;

代码语言:javascript复制
//加、减、乘、除等
#define ZEND_ADD                               1
#define ZEND_SUB                               2
#define ZEND_MUL                               3
#define ZEND_p                               4
#define ZEND_MOD                               5
#define ZEND_SL                                6
#define ZEND_SR                                7
#define ZEND_CONCAT                            8
#define ZEND_BW_OR                             9
#define ZEND_BW_AND                           10

2.1.2 指令

2.1.2.1指令的表示

指令由操作码和操作数组成;操作码指明本指令的操作类型,操作数指明操作数本身或者操作数的地址;

PHP虚拟机定义指令格式为:操作码 操作数1 操作数2 返回值;其使用结构体_zend_op表示一条指令:

代码语言:javascript复制
struct _zend_op {
    const void *handler;    //指针,指向当前指令的执行函数
    znode_op op1;           //操作数1         
    znode_op op2;           //操作数2
    znode_op result;        //返回值
    uint32_t extended_value;//扩展
    uint32_t lineno;        //行号
    zend_uchar opcode;      //指令类型
    zend_uchar op1_type;    //操作数1的类型(此类型并不代表字符串、数组等数据类型;其表示此操作数是常量,临时变量,编译变量等)
    zend_uchar op2_type;    //操作数2的类型
    zend_uchar result_type; //返回值的类型
};

2.1.2.2 操作数的表示

从上面可以看到,操作数使用结构体znode_op表示,定义如下:

constant、var、num等都是uint32_t类型的,这怎么表示一个操作数呢?(既不是指针不能代表地址,也无法表示所有数据类型); 其实,操作数大多情况采用的相对地址表示方式,constant等表示的是相对于执行栈帧首地址的偏移量; 另外,_znode_op结构体中有个zval *zv字段,其也可以表示一个操作数,这个字段是一个指针,指向的是zval结构体,PHP虚拟机支持的所有数据类型都使用zval结构体表示;

代码语言:javascript复制
typedef union _znode_op {
        uint32_t      constant;
        uint32_t      var;
        uint32_t      num;
        uint32_t      opline_num;
    #if ZEND_USE_ABS_JMP_ADDR
        zend_op       *jmp_addr;
    #else
        uint32_t      jmp_offset;
    #endif
    #if ZEND_USE_ABS_CONST_ADDR
        zval          *zv;
    #endif
} znode_op;

2.2 数据存储

PHP虚拟机支持多种数据类型:整型、浮点型、字符串、数组,对象等;PHP虚拟机如何存储和表示多种数据类型?

2.1.2.2节指出结构体_znode_op代表一个操作数;操作数可以是一个偏移量(计算得到一个地址,即zval结构体的首地址),或者一个zval指针;PHP虚拟机使用zval结构体表示和存储多种数据;

代码语言:javascript复制
struct _zval_struct {
    zend_value        value;            //存储实际的value值
    union {
        struct {                        //一些标志位
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    type,         //重要;表示变量类型
                zend_uchar    type_flags,
                zend_uchar    const_flags,
                zend_uchar    reserved)     /* call info for EX(This) */
        } v;
        uint32_t type_info;
    } u1;

    union {                                 //其他有用信息
        uint32_t     next;                 /* hash collision chain */
        uint32_t     cache_slot;           /* literal cache slot */
        uint32_t     lineno;               /* line number (for ast nodes) */
        uint32_t     num_args;             /* arguments number for EX(This) */
        uint32_t     fe_pos;               /* foreach position */
        uint32_t     fe_iter_idx;          /* foreach iterator index */
        uint32_t     access_flags;         /* class constant access flags */
        uint32_t     property_guard;       /* single property guard *
    } u2;
};

zval.u1.type表示数据类型, zend_types.h文件定义了以下类型:

代码语言:javascript复制
#define IS_UNDEF                    0
#define IS_NULL                     1
#define IS_FALSE                    2
#define IS_TRUE                     3
#define IS_LONG                     4
#define IS_DOUBLE                   5
#define IS_STRING                   6
#define IS_ARRAY                    7
#define IS_OBJECT                   8
#define IS_RESOURCE                 9
#define IS_REFERENCE                10
…………

zend_value存储具体的数据内容,结构体定义如下:

_zend_value占16字节内存;long、double类型会直接存储在结构体;引用、字符串、数组等类型使用指针存储;

代码中根据zval.u1.type字段,判断数据类型,以此决定操作_zend_value结构体哪个字段;

可以看出,字符串使用zend_string表示,数组使用zend_array表示…

代码语言:javascript复制
typedef union _zend_value {
    zend_long         lval;            
    double            dval;            
    zend_refcounted  *counted;
    zend_string      *str;
    zend_array       *arr;
    zend_object      *obj;
    zend_resource    *res;
    zend_reference   *ref;
    zend_ast_ref     *ast;
    zval             *zv;
    void             *ptr;
    zend_class_entry *ce;
    zend_function    *func;
    struct {
        uint32_t w1;
        uint32_t w2;
    } ww;
} zend_value;

如下图为PHP7中字符串结构图:

2.3 再谈指令

2.1.2.1指出,指令使用结构体_zend_op表示;其中最主要2个属性:操作函数,操作数(两个操作数和一个返回值);

操作数的类型(常量、临时变量等)不同,同一个指令对应的handler函数也会不同;操作数类型定义在 Zend/zend_compile.h文件:

代码语言:javascript复制
//常量
#define IS_CONST    (1<<0)
//临时变量,用于操作的中间结果;不能被其他指令对应的handler重复使用
#define IS_TMP_VAR  (1<<1)
//这个变量并不是PHP代码中声明的变量,常见的是返回的临时变量,比如$a=time(), 函数time返回值的类型就是IS_VAR,这种类型的变量是可以被其他指令对应的handler重复使用的
#define IS_VAR      (1<<2)
#define IS_UNUSED   (1<<3)  /* Unused variable */
//编译变量;即PHP中声明的变量;
#define IS_CV       (1<<4)  /* Compiled variable */

操作函数命名规则为:ZEND_[opcode]_SPEC_(操作数1类型)_(操作数2类型)_(返回值类型)_HANDLER

比如赋值语句就有以下多种操作函数:

代码语言:javascript复制
ZEND_ASSIGN_SPEC_VAR_TMP_RETVAL_UNUSED_HANDLER,
ZEND_ASSIGN_SPEC_VAR_VAR_RETVAL_UNUSED_HANDLER,
ZEND_ASSIGN_SPEC_VAR_CV_RETVAL_UNUSED_HANDLER,

对于$a=1,其操作函数为:

ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER;函数实现为:

代码语言:javascript复制
{
    USE_OPLINE
    zval *value;
    zval *variable_ptr;
    SAVE_OPLINE();
    //获取op2对应的值,也就是1
    value = EX_CONSTANT(opline->op2);
    //在execute_data中获取op1的位置,也就是$a(execute_data类似函数栈帧,后面详细分析)
    variable_ptr = _get_zval_ptr_cv_undef_BP_VAR_W(execute_data, opline->op1.var);
    //赋值
    value = zend_assign_to_variable(variable_ptr, value, IS_CONST);
    if (UNEXPECTED(0)) {
        ZVAL_COPY(EX_VAR(opline->result.var), value);
    }  
    ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();

2.4 函数栈帧

2.4.1指令集

上面分析了指令的结构与表示,PHP虚拟机使用_zend_op_array表示指令的集合:

代码语言:javascript复制
struct _zend_op_array {
    …………
    //last表示指令总数;opcodes为存储指令的数组;
    uint32_t last;
    zend_op *opcodes;
    //变量类型为IS_CV的个数
    int last_var;
    //变量类型为IS_VAR和IS_TEMP_VAR的个数
    uint32_t T;
    //存放IS_CV类型变量的数组
    zend_string **vars;
    …………
    //静态变量
    HashTable *static_variables;
    //常量个数;常量数组
    int last_literal;
    zval *literals;
    …
};

注意:last_var代表IS_CV类型变量的个数,这种类型变量存放在vars数组中;在整个编译过程中,每次遇到一个IS_CV类型的变量(类似于$something),就会去遍历vars数组,检查是否已经存在,如果不存在,则插入到vars中,并将last_var的值设置为该变量的操作数;如果存在,则使用之前分配的操作数

2.4.2 函数栈帧

PHP虚拟机实现了与1.3节物理机类似的函数栈帧结构;

使用 _zend_vm_stack表示栈结构;多个栈之间使用prev字段形成单向链表;top和end指向栈低和栈顶,分别为zval类型的指针;

代码语言:javascript复制
truct _zend_vm_stack {
    zval *top;
    zval *end;
    zend_vm_stack prev;
};

考虑如何设计函数执行时候的帧结构:当前函数执行时,需要存储函数编译后的指令,需要存储函数内部的局部变量等(2.1.2.2节指出,操作数使用结构体znode_op表示,其内部使用uint32_t表示操作数,此时表示的就是当前zval变量相对于当前函数栈帧首地址的偏移量);

PHP虚拟机使用结构体_zend_execute_data存储当前函数执行所需数据;

代码语言:javascript复制
struct _zend_execute_data {
    //当前指令指令
    const zend_op       *opline; 
    //当前函数执行栈帧
    zend_execute_data   *call; 
    //函数返回数据          
    zval                *return_value;
    zend_function       *func;            
    zval                 This;      /* this   call_info   num_args */
    //调用当前函数的栈帧       
    zend_execute_data   *prev_execute_data;
    //符号表
    zend_array          *symbol_table;
#if ZEND_EX_USE_RUN_TIME_CACHE
    void               **run_time_cache;  
#endif
#if ZEND_EX_USE_LITERALS
    //常量数组
    zval                *literals;        
#endif
};

函数开始执行时,需要为函数分配相应的函数栈帧并入栈,代码如下:

代码语言:javascript复制
static zend_always_inline zend_execute_data *zend_vm_stack_push_call_frame(uint32_t call_info, zend_function *func, uint32_t num_args, zend_class_entry *called_scope, zend_object *object)
{
    //计算当前函数栈帧需要内存空间大小
    uint32_t used_stack = zend_vm_calc_used_stack(num_args, func);
    //根据栈帧大小分配空间,入栈
    return zend_vm_stack_push_call_frame_ex(used_stack, call_info,
        func, num_args, called_scope, object);
}
//计算函数栈帧大小
static zend_always_inline uint32_t zend_vm_calc_used_stack(uint32_t num_args, zend_function *func)
{
    //_zend_execute_data大小(80字节/16字节=5) 参数数目
    uint32_t used_stack = ZEND_CALL_FRAME_SLOT   num_args;
    if (EXPECTED(ZEND_USER_CODE(func->type))) {
        //当前函数临时变量等数目
        used_stack  = func->op_array.last_var   func->op_array.T - MIN(func->op_array.num_args, num_args);

    }
    //乘以16字节
    return used_stack * sizeof(zval);
}
//入栈
static zend_always_inline zend_execute_data *zend_vm_stack_push_call_frame_ex(uint32_t used_stack, uint32_t call_info, zend_function *func, uint32_t num_args, zend_class_entry *called_scope, zend_object *object)
{
    //上一个函数栈帧地址
    zend_execute_data *call = (zend_execute_data*)EG(vm_stack_top);
    //移动函数调用栈top指针
    EG(vm_stack_top) = (zval*)((char*)call   used_stack);
    //初始化当前函数栈帧
    zend_vm_init_call_frame(call, call_info, func, num_args, called_scope, object);
    //返回当前函数栈帧首地址
    return call;
}

从上面分析可以得到函数栈帧结构图如下所示:

PHP虚拟机也是计算机,有三点是我们需要重点关注的:指令集(包含指令处理函数)、数据存储(zval)、函数栈帧;

此时虚拟机已可以接受指令并执行指令代码;

但是,PHP虚拟机是专用执行PHP代码的,PHP代码如何能转换为PHP虚拟机可以识别的指令呢——编译;

PHP虚拟机同时提供了编译器,可以将PHP代码转换为其可以识别的指令集合;

理论上你可以自定义任何语言,只要实现编译器,能够将你自己的语言转换为PHP可以识别的指令代码,就能被PHP虚拟机执行;

再来稍微看看java虚拟机:

一、java代码编译执行过程

 1.源码编译:通过Java源码编译器将Java代码编译成JVM字节码(.class文件)

  2.类加载:通过ClassLoader及其子类来完成JVM的类加载

  3.类执行:字节码被装入内存,进入JVM虚拟机,被解释器解释执行

注:Java平台由Java虚拟机和Java应用程序接口搭建,Java语言则是进入这个平台的通道,

     用Java语言编写并编译的程序可以运行在这个平台上

二、JVM简介

1.java程序经过一次编译之后,将java代码编译为字节码也就是class文件,然后在不同的操作系统上依靠不同的java虚拟机进行解释,最后再转换为不同平台的机器码,最终得到执行

2.Java虚拟机(JVM) 处在核心的位置,是程序与底层操作系统、硬件无关的关键。

JVM的下方是移植接口,移植接口由两部分组成:适配器和Java操作系统, 其中依赖于平台的部分称为适配器,JVM 通过移植接口在具体的平台和操作系统上实现

JVM 的上方是Java的基本类库和扩展类库以及它们的API, 利用Java API编写的应用程序(application) 和小程序(Java applet) 可以在任何Java平台上运行而无需考虑底层平台

Java虚拟机(JVM)实现了程序与操作系统的分离,从而实现了Java 的跨平台

3.JVM在它的生存周期中有一个明确的任务,那就是运行Java程序,因此当Java程序启动的时候,就产生JVM的一个实例;当程序运行结束的时候,该实例也跟着消失了

4.三种JVM:① Sun公司的HotSpot ② BEA公司的JRockit ③ IBM公司的J9 JVM

在JDK1.7及其以前我们所使用的都是Sun公司的HotSpot,但由于Sun公司和BEA公司都被oracle收购,jdk1.8将采用Sun公司的HotSpot和BEA公司的JRockit两个JVM中精华形成jdk1.8的JVM。

三、JVM体系结构

1.Class Loader类加载器

负责加载 .class文件,class文件在文件开头有特定的文件标示,并且ClassLoader负责class文件的加载等,至于它是否可以运行,则由Execution Engine决定。

  ① 定位和导入二进制class文件

  ② 验证导入类的正确性

  ③ 为类分配初始化内存

  ④ 帮助解析符号引用.

2.Native Interface本地接口

  本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C 程序,Java诞生的时候C/C 横行的时候,要想立足,必须有调用C/C 程序,于是就在内存中专门开辟了一块区域处理标记为

  native的代码,它的具体作法是Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies。

 目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机,或者Java系统管理生产设备,在企业级应用中已经比较少见。

  因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用Web Service等。

3.Execution Engine 执行引擎:执行包在装载类的方法中的指令,也就是方法。

4.Runtime data area 运行数据区(即:虚拟机内存或者JVM内存 下节介绍)

从整个计算机内存中开辟一块内存存储Jvm需要用到的对象,变量等,分为:方法区,堆,虚拟机栈,程序计数器,本地方法栈。

四、JVM内存结构

1.程序计数器 PC Register

  每个线程都有一个程序计算器,就是一个指针,指向方法区中的方法字节码(下一个将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。

  程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。由于Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。如果线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie 方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError 情况的区域。

2.本地方法栈 Native Method Stack

 Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies

  本地方法栈与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务

3.方法区 Method Area

  用于存储虚拟机加载的:静态变量 常量 类信息 运行时常量池 (类信息:类的版本、字段、方法、接口、构造函数等描述信息 )

  默认最小值为16MB,最大值为64MB,可以通过-XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小

对于习惯在HotSpot 虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot 虚拟机的设计团队选择把GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机(如BEA JRockit、IBM J9 等)来说是不存在永久代的概念的。即使是HotSpot 虚拟机本身,根据官方发布的路线图信息,现在也有放弃永久代并“搬家”至Native Memory 来实现方法区的规划了。Java 虚拟机规范对这个区域的限制非常宽松,除了和Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。在Sun 公司的BUG 列表中,曾出现过的若干个严重的BUG 就是由于低版本的HotSpot 虚拟机对此区域未完全回收而导致内存泄漏。根据Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常。

4.栈 JVM Stack

  编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(引用指针,并非对象本身)

  栈是java 方法执行的内存模型:

  每个方法被执行的时候 都会创建一个“栈帧”用于存储局部变量表(包括参数)、操作栈、方法出口等信息。

  每个方法被调用到执行完的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

  (局部变量表:存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(引用指针,并非对象本身),

  其中64位长度的long和double类型的数据会占用2个局部变量的空间,其余数据类型只占1个。

  局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量是完全确定的,在运行期间栈帧不会改变局部变量表的大小空间)

  栈的生命期是跟随线程的生命期,线程创建时创建,线程结束栈内存也就释放,是线程私有的。

5.堆 Java Heap

  所有的对象实例以及数组都要在堆上分配,此内存区域的唯一目的就是存放对象实例

  堆是Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建

  堆是理解Java GC机制最重要的区域,没有之一

  结构:新生代(Eden区 2个Survivor区) 老年代 永久代(HotSpot有)

  新生代:新创建的对象——>Eden区

  GC之后,存活的对象由Eden区 Survivor区0进入Survivor区1

  再次GC,存活的对象由Eden区 Survivor区1进入Survivor区0

  老年代:对象如果在新生代存活了足够长的时间而没有被清理掉(即在几次Young GC后存活了下来),则会被复制到老年代

  如果新创建对象比较大(比如长字符串或大数组),新生代空间不足,则大对象会直接分配到老年代上(大对象可能触发提前GC,应少用,更应避免使用短命的大对象)

  老年代的空间一般比新生代大,能存放更多的对象,在老年代上发生的GC次数也比年轻代少

  永久代:可以简单理解为方法区(本质上两者并不等价)

  如上文所说:对于习惯在HotSpot 虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”,本质上两者并不等价

  仅仅是因为HotSpot 虚拟机的设计团队选择把GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已

  对于其他虚拟机(如BEA JRockit、IBM J9 等)来说是不存在永久代的概念的

  即使是HotSpot 虚拟机本身,根据官方发布的路线图信息,现在也有放弃永久代并“搬家”至Native Memory 来实现方法区的规划了

  Jdk1.6及之前:常量池分配在永久代

  Jdk1.7:有,但已经逐步“去永久代”

  Jdk1.8及之后:没有永久代(java.lang.OutOfMemoryError: PermGen space,这种错误将不会出现在JDK1.8中)

6.直接内存 Direct Memor

  直接内存并不是JVM管理的内存,可以这样理解,直接内存,就是JVM以外的机器内存,比如,你有4G的内存,JVM占用了1G,则其余的3G就是直接内存

  JDK中有一种基于通道(Channel)和缓冲区(Buffer)的内存分配方式,将由C语言实现的native函数库分配在直接内存中,用存储在JVM堆中的DirectByteBuffer来引用

  由于直接内存收到本机器内存的限制,所以也可能出现OutOfMemoryError的异常。

总结:

我们可以发现:

1:无论是java还是php ,虚拟机干的活都差不多,编译成中间码,负责中间码的执行,执行过程中内存管理

2:虚拟机本身就是一个系统,专门负责这门语言管理的操作系统,并且留个接口给外面对接

3:至于java和php不同的数据结构,不同的中间码等等这些就稍微有些区别,但最终都落脚到cpu去执行机器码,哪个中间码离机器码越近,那么执行效率越高

03 php 与java 对比

1、php与Java的语法比较

1)、注释

java支持:双斜杠(//)、 /**/ ;PHP支持:双斜杠(//)、#符号、 /**/ 。

2)、大小写敏感

在java中,所有函数名,关键字,类,变量等都是大小写敏感的;在PHP中,变量是大小写敏感的,而用户自定义的函数、类和关键字对大小写不敏感。

php是解释执行的服务器脚本语言,首先php有简单容易上手的特点。语法和c语言比较象,所以学过c语言的程序员可以很快的熟悉php的开发。

java的学习需要先学好java的语法和熟悉一些核心的类库,java处处面向对象,学java懂得面向对象的程序设计方法,所以java不如php好学。

更加直观一些的表达:php的语法和使用更加容易,java更多用类库和包来支撑生态

2、php与Java系统架构设计的对比

如果非得说到系统架构,php和java本质上区别不大,主要架构都是从域名解析负载均衡到代码服务器到缓存最后到数据库,最后的主要瓶颈都在数据库这层,而数据库通过缓存,集群,异步的方式都能很好的解决架构问题,这里我们可以看到系统架构上和语言有一定关系,但是主要因素还不在语言这一个层面。

一些常用的数据php可以通过nginx module直达缓存(memached与redis),失效后调用代码刷新,有的时候都还不需要经过语言这一层,这就是架构的魅力,但是落到细节的java和php语言成面的东西还是有一些区别,比如并发,长链接的处理。

实在要追求运行效率,php的c 扩展足以解决,所以php大并发架构不如java的说法是不完全正确的,java能支持的大并发架构,php也是可以实现的;架构好不好,和语言有一定关系,更多的是在于使用这门语言的人。

3、php与Java访问数据库速度的比较

php对于不同的数据库采用不同的数据库访问接口,所以数据库访问代码的通用性不强。例如:用Java开发的Web应用从MySQL数据库转到Oracle数据库只需要做很少的修改。而php则需要做大量的修改工作。

Java通过JDBC来访问数据库,通过不同的数据库厂商提供的数据库驱动方便地访问数据库。访问数据库的接口比较统一。

4、php与Java源代码安全的对比

PHP开发的程序的源代码一般是公开的,也可以通过 zend加密,至于安全性,通过sql注入拦截,外界传入数据拦截再加上linux服务器安全,自然也是安全的。

Java开发的程序,最后用户拿到的是只是一些编译好的class类,安全性是有一定提高。

5、php与Java开发成本的对比

PHP最经典的组合就是:PHP MySQL nginx linux 。非常适合开发中小型的Web应用,开发的速度比较快。而且所有的软件都是开源免费的,可以减少投入。

Java的Web应用服务器有免费Tomcat、JBoss等,如果需要更好的商业化的服务有:Web Sphere和 Web logic。

6、php与Java的性能比较

php7的性能已经有很大提高,某些情况下与java性能相当,但是由于java是编译型语言,变量常驻内存以及多线程,java在性能上还是有不错的表现

7:我该学php还是java

php简单易学,容易上手,java生态丰富,在大数据上优势明显,php做到一定程度只在web范围内,不便于长远发展,所以做php的同学可以做到一定程度同时开始java和go的学习

8:我们公司技术选型用java还是go

看公司目前的业务状态,如果是web,用java和go都一样,而在于你的技术leader对java还是go更熟悉,也在于你所在城市哪个语言的市场供应比较多

9:为什么市场上的php越来越少

这是一个很值得思考的问题,至从微服务出来以后,spring boot,spring cloud把web开发占据了很大一部分江山,而整个互联网进入更加深层次的存量竞争,原来简单的web系统已经不满足业务需求;同时更在于php和java两个语言从业人员的本身基础问题,因为php相对上手容易,所以市场上有大量基础一般的人存在,导致业务系统常常有一些状况,而java要能跑起来对基础还是有一定要求,结果就造成了好像java系统越来越坚固的假象,实质上语言上是差别不大,而是两种语言的从业人员的素质有一定差别。

php与其php与其他语言还有一些对比和差别,这里小编就不一一展开了,以后再详细展开

结论思考

回到最开始的问题,php是不是世界上最好的语言,我们一起来揭开这个谜底,相信看完上面的文字心里应该有一定数了

1:从语言本身设计或者出发点来讲php要说是世界上最好的语言,可能并不会很夸张,只是其应用场景更多的局限与web

2:至于"php是世界上最好的语言"这个段子什么时候开始流传的,我也不能追溯到,只是想到一种可能场景:在一个团队内,有java也有php,突然某天java能解决的问题php没解决了,主管要把php转java,而php是不服的,大声表达:php是世界上最好的语言

或许,php是世界上最好的语言是基于 以上两个因素同时而产生的,至于php是不是世界上的语言并不重要,各自有各自的场景,而"php是世界上最好的语言"这个段子就让他继续流传吧,这是程序猿界少有的段子,是我们happy的源泉,也或许是很多人骄傲的源泉,开心很重要

写一篇不错的文章需要花很多的时间,更重要的是这些更加接近真相的东西我们需要让更多的人知道,是对社会,是更多人共多的贡献,只有大家都有钱了,我们自己也才能让他们身上赚到更多的钱

0 人点赞