第3章 PHP程序调优

第3章 PHP程序调优

某书提出一个观点「代码质量与其整洁度成正比」。确实,干净的代码既在质量上较为可靠,也方便阅读,为后期维护、重构奠定了良好基础。

3.1 代码提炼封装

当代码太多的时候,我们常常会忽略掉以前我们所做的工作,使得我们不得不再做一次同样的工作。提炼封装能帮我们比较好的管理这些代码。

3.1.1 提取变量

1. 变量封装

将仅限于本类使用的变量使用 private 修饰,并提供对应的访问方法,可以将与外部调用者无关的变量隐藏起来,减少代码的耦合性与出错的概率。

例如 :

class Singleton
{
    public $instance = ...;
}

私有化 $instance 属性,并添加 getInstance/setInstance 方法管理 :

class Singleton
{
    private $instance = ...;

    public function getInstance()
    {
return $this->instance;
    }

    public function setInstance($instance)
    {
$this->instance = $instance;
    }
}

注意:并不是需要把所有的成员变量都加以 private 修饰,这样只会适得其反,需要根据变量的需求来进行封装。如果你的成员变量不需要被外部直接使用,就放心的使用 private 吧。

2. 变量提炼

给函数、类一个好的命名可以减少我们一些理解时间,并且代码也会更好看,变量也是同样。例 :

if (($user->toUpperCase()->indexOf("AGE") > 18) && ($user->toUpperCase()->indexOf("SEX") == 'male')) {
    # 代码块
}

这种方式很难知道需要判断的是什么,在字段无法精准表达的时候就会更难说明。建议使用下面方式 :

$isEnoughAge = $user->toUpperCase()->indexOf("AGE") > 18;
$isMale = $user->toUpperCase()->indexOf("SEX") == 'male';

if ($isEnoughAge && $isMale) {
    # 代码块
}

将小括号中的表达式提取出来赋予适合的变量,通过变量的名称即可了解表达含义。

提炼变量的主要原因是通过将复杂表达式给予适合的名称,从而使其更易于理解。当然并不是所有的表达式都需要提出,因为过多的提出会导致变量增多。当你觉得表达式很难表达你的意思,那就该提炼变量出场了。

3. 参数替代

当调用一个函数时,需要传递一长~~ 串的参数,这样是不合理的。我们应该把这一长串参数用更少的参数替代 - 如果可以一个对象最好。

例如 :

function enter($name, $clothes, $age, $sex...)
{
	# 代码块
}

enter('Jack', '西服', 16, 'male');

我们应该提取其中的参数作为一个对象传递 :

Class Person
{
    public $name = 'Jack';
    public $clothes = '西服';
    public $age = 16;
    public $sex = 'male';
}

function enter(Person $person)
{
	# 代码块
}

enter(new Person());

将需要使用的变量放到一个对象中,通过传递该对象来使用这些参数。

3.1.2 提炼函数

提炼函数 - 将大段代码中的一部分提取后或将多个方法中公用的部分提供出来,构成一个新方法。这样可以使整段程序的结构变得更清晰、增加可读性、扩展性和可维护性。

当有两个很类似的方法 :

function choose1() {
    return phone('某米');
}

function choose2() {
    return phone('某为');
}

我们可以提取其中共同的部分作为一个独立的函数 :

function choose($phone) {
    return phone($phone);
}

function choose1() {
    return choose('某米');
}

function choose2() {
    return choose('某为');
}

这里看起来是变得更麻烦了,但是实际开发我们不可能只会有这么一些,另外我们在修改功能时只需改变其核心部分而不需要每个都改变。

注意:每个创建的函数都需要被调用至少 1 次。

3.1.3 提炼类

1. 方法上移

将多个类所公用的部分提取出来,形成一个新的类,这个类被称为它们的基类或者父类。子类可以根据基类的成员结合自己的需求去完成工作。基类一般不会直接实例化使用,所以基类一般是抽象类。

当两个类的成员类似时 :

class Boy
{
    protected $name = 'Jack';

    public function getName()
    {
return $this->name;
    }

    public function setName($name)
    {
$this->name = $name;
    }
}

class Girl
{
    protected $name = 'Alice';

    public function getName()
    {
return $this->name;
    }

    public function setName($name)
    {
$this->name = $name;
    }
}

我们可以提取其中共同的部分作为基类 :

abstract Class Base
{
    protected $name = '';

    public function getName()
    {
return $this->name;
    }

    public function setName($name)
    {
$this->name = $name;
    }
}

class Boy extends Base
{
    protected $name = 'Jack';
}

class Girl extends Base
{
    protected $name = 'Alice';
}

除了提取为基类外,还可以把类的共同部分提取出来作为“供应商”,当有需要时直接通过该类的对象获取对应数据即可,而不需要继承该类。当然,凡事有利有弊,需要根据实际情况判断是否使用。

2. 方法下移

方法下移和方法上移就刚好相反了,上移是提取出共同的部分上移给基类,而下移则是把基类中只为个别子类 “开小灶” 的方法移到对应子类。

例 - 女孩不太喜欢玩耍,只想好好学习 :

abstract Class Base
{
    public function teach()
    {
# 教你做人
    }

    public function play()
    {
# 和你玩耍
    }
}

class Boy extends Base
{
    # 两种方法都会用
}

class Girl extends Base
{
    # 用不到 play 方法
}

这时 play 方法默认会被 Girl 类继承。需要做如下改动 :

abstract Class Base
{
    public function teach()
    {
# 教你做人
    }
}

class Boy extends Base
{
    public function play()
    {
# 和你玩耍
    }
}

class Girl extends Base
{
}

这就完成了方法下移了。

3. 移除委托

当需要另一个对象帮你完成某个功能时,你却找了一个中间人来横插一脚。我们需要判断中间人是否有存在的必要,如果没有必要则需要移除该委托。

例 - Jack 需要看时间,通过 Alice 查看 :

Class Jack
{
    public function checkTime()
    {
(new Alice())->checkTime();
    }
}

class Alice
{
    public function checkTime()
    {
(new Clock())->getTime();
    }
}

class Clock
{
    public function getTime()
    {
# 当前事件
    }
}

此时 Alice 则没有存在的必要,可以删除 :

Class Jack
{
    public function checkTime()
    {
(new Clock())->getTime();
    }
}

class Clock
{
    public function getTime()
    {
# 当前事件
    }
}

3.2 识别坏代码

3.2.1 Bloaters

Bloaters 是代码、方法和类已经增加到很多行代码,这时候它们已经很难使用了。这些代码并不会立马凸显问题,但是随着代码不断改变,问题就会越来越明显。识别坏代码,有助于发现代码的潜在问题,从而可以有的放矢的修改现有代码,使之不断完善。

1. Long Method(过长函数)

特征:一个方法含有太多行代码。一般来说,任何方法超过10行时,你就可以考虑是不是过长了。

原因:在一个原有功能上面添加功能更方便,多次在同一个方法中添加,该方法就会变大并且不易修改。

策略 :

  • 通过「提炼方法」将过长的函数按照功能的不同进行适当拆解为小函数。例如:注释、循环等就是提炼很好的地方。
  • 给函数取一个好的名字,通过名字来了解函数提供的功能,提高代码的理解性。

2. Large Class(过大类)

特征:一个类包含过多的字段、方法、代码行。

原因:类似过长函数。

策略 :

  • 如果过大类中的部分行为可以提炼到一个独立的组件中,可以使用「提炼类」。

    如果有必要为客户端提供一组操作和行为,可以使用「提炼接口」。

3. Primitive Obsession(基本类型偏执)

特征:总是使用基本类型而不使用对象。

原因:开始写程序时可能涉及的字段不多,在日益增加的功能中,字段越来越多,基本类型也越来越多。

策略:使用「提炼类」或 使用已存在的对象取代这些参数,实现使用对象代替基本型数据项目。

4. Long Parameter List(过长参数列)

特征:一个方法有超过三四个的参数。

原因:可能是将多个算法并到一个函数中时发生的。函数中的入参可以用来控制最终选用哪个算法去执行。

策略:

  • 如果向已有的对象发出一条请求就可以取代一个参数,则可通过「以函数取代参数」移除参数列,通过在函数内部向上述已存在的对象查询来获取参数。
  • 如果某些数据缺乏合理的对象归属,可使用「 引入参数对象」为它们制造出一个参数对象。

5. Data Clumps(数据泥团)

特征:代码的不同部分包含相同的变量组 - 签名、数据库连接信息等 。

原因:复制粘贴或设计不当导致。

策略:通过使用「创建新的参数对象取代这些参数」或「使用已存在的对象取代这些参数」,实现使用对象代替类成员属性和方法中参数列表,清除数据泥团,使代码简洁,也提高维护性和易读性。

一个好的评断办法是:删掉众多数据中的一笔,其它数据有没有因此失去意义。如果不在有意义则应该为其生成一个新的对象了。

3.2.2 Object-Orientation Abusers

Object-Orientation Abusers 是因为面向对象编程原则不完整或不正确的应用。

1. Switch Statements(switch 语句)

特征:同样的switch语句出现在不同的方法或不同的类中,这样当需要增加新的 case 分支或者修改时,就必须找到所有的地方,然后进行修改。

原因:对 switch 语句的依赖。

策略:大多数时候,一看到 switch 语句,就应该考虑以多态来替换它。

  • switch 语句常常根据类型码进行选择,你要的是 “与该类型码相关的函数或类”,所以应该运用「提炼函数」将 switch 语句提炼到一个独立函数中,再以「搬移函数」将它搬移到需要多态性的那个类里。
  • 如果你的 switch 是基于类型码来识别分支,这时可以运用「以子类取代类型码」或「以策略模式取代类型码」。
  • 一旦完成这样的继承结构后,就可以运用「以多态取代条件表达式」 了。
  • 如果条件分支并不多并且它们使用不同参数调用相同的函数,多态就没必要了。在这种情况下,你可以运用「以明确函数取代参数」。
  • 如果你的选择条件之一是 null,可以运用「引入 Null 对象」。

2. Temporary Field(临时字段)

特征:某些只在特定环境下有意义的字段。

原因:当存储一个数据却没有比较好的容器。

策略:通过「提炼类」将这些临时字段及其相关的函数移植的一些新的类中。

3. Refused Bequest(被拒绝的遗赠)

特征:子类应该继承父类的函数和数据。但如果它们不想或不需要继承,这就意味继承体系设计错误。

原因:有人只是为了重用超类中的代码而创建子类。但是超类和子类是完全不同的。

策略:

  • 如果继承有意义,可以为子类创建一个兄弟类,将所有超类中对于子类无用的字段和函数提取到兄弟类中;
  • 如果继承没有意义并且子类和父类之间确实没有共同点,可以运用「以委托取代继承」消除继承。

4. Alternative Classes with Different Interfaces(异曲同工的类)

特征:两个类中有着不同的函数,却在做着同一件事。

原因:创建这个类的程序员并不知道已经有实现这个功能的类存在。

策略 :

  • 使用「重命名方法」,根据他们的用途来重命名。
  • 可以适当运用「搬移函数」,使类与接口保持一致。
  • 如果仅有部分一致,可以使用「提炼超类」,可以让已存在的类作为超类。
3.2.3 Change Preventers

Change Preventers 意味着如果需要在代码的一个地方更改某些内容,就必须在其他地方也进行许多更改。因此,程序开发变得更加复杂和代价更大。

1. Divergent Change(发散式变化)

特征:当你对类进行更改时,发现不得不修改更多不相关的方法。

原因:复制粘贴或编程结构不合理导致。

策略:将每次因同一条件变化,而需要同时修改的若干方法通过「提炼类」将它们提炼到一个新类中。

2. Shotgun Surgery(霰弹式修改)

特征:任何修改都需要在许多不同类上做小幅度修改。

原因:单一的职责的拆分。

策略 :

  • 使用「移动函数」和「移动字段」将不同类中相同的行为到一个独立类中。 如果没有合适的类则创建一个新类。
  • 通常,可以运用「将类内联化」将一些列相关行为放进同一个类。

对比发散式变化与霰弹式修改:发散式变化是指一个类受多种变化的影响。霰弹式修改是指一个变化引发多个类相应的修改。

3. Parallel Inheritance Hierarchies(平行继承体系)

特征:为某个类添加一个子类,必须同时为另一个类相应添加一个子类。是霰弹式修改 的特殊情况(比较少见)。

原因:随着新类的加入,继承体系越来越大。

策略:在一个类继承体系的对象中引用另一个类继承体系的对象,然后运用「移动方法」和「移动字段」将被引用类中的一些方法和成员变量迁移宿主类中,消除被引用类的继承体系。

3.2.4 Dispensables

​ 可有可无的东西往往不能带给我们有效的帮助,反而会增加工作压力。

1. Comments(过多的注释)

特征:这个也是坏代码吗?注释本身不是坏事,但是如果代码能够表达其意则没有必要过多注释。

原因:代码含义不直观,想要通过注释说明。

策略:

  • 如果需要说明一段代码功能,可以试试「提取方法」,提取出一个独立的函数,让函数名称解释该函数的用途。
  • 如果觉得需要注释来说明系统的某些假设条件,也可尝试使用「引入断言」,来明确标明这些假设。
  • 当你感觉需要撰写注释时,请先尝试重构,试着让所有的注释都变得多余。

注意:以上之外还有注释请简洁针对性编写注释,拒绝长篇大论。

2. Duplicate Code(重复代码)

特征:如果你在一个以上的地方看到相同的代码,那么可以设法将它们合而为一。

原因:多个程序员同时在同一程序的不同部分上工作时,以及特定部分的代码看上去不同但实际在做同一件事。

策略:

  • 在同一个类的不同地方:通过采用「提取方法」提炼出重复的代码,然后在这些地方调用上述提炼出方法。
  • 在不同子类中:通过「提取方法」提炼出重复的代码,然后将该方法移动到上级的父类内。
  • 在没有关系的类中:通过对其中一个使用「提取类」 将重复的代码提炼到一个新类中,然后在另一个类中调用生成的新类,消除重复的代码。

3. Lazy Class(冗余类)

特征:一些类由于种种原因已经不再承担足够责任,当其价值低于维护价值则应该删除了。

原因:随着代码的变迁类被需要的越来越少。

策略:通过 「折叠继承体系」,将这些冗余的类合并到父类或子类中,或者通过「将类内联化」,将这些冗余类中的所有函数、字段移到其他相关的类中。

4. Data Class(纯稚的数据类)

特征:类拥有字段以及用来访问字段的 getter 和 setter 函数,但是没有其他的功能函数。

原因:只用对象存储个别字段。

策略:找出其他类中访问数据类中的 getter/setter 的函数,尝试以「移动方法」将这些函数移植到数据类中,实现将数据和操作行为装在一起,也让数据类承担一定的责任。

5. Dead Code(死码)

特征:变量、参数、字段、方法或类等不再使用(通常是因为它们过时了)。

原因:当对程序的需求发生变化或做出更正时,没有人有时间清理旧代码。

策略 :

  • 找到死代码的最快方法是使用一个好的 IDE。
  • 删除未使用的代码和不需要的文件。

6. Speculative Generality(夸夸其谈未来性)

特征:存在未被使用的类、函数、字段或参数。

原因:准备为未来提供更多可能,但是一直没有行动。

策略 :

  • 如果抽象类作用不大,使用「折叠继承体系」合并抽象类。
  • 委托类作用不大,使用「将类内联化」移除非必要的委托 。
  • 函数中有无用参数,使用「移除参数」删除多余的参数。
3.2.5 Couplers

Couplers 下的所有坏代码都会导致类之间的过度耦合以及过度委托。

1. Feature Envy(依恋情结)

特征:一个函数访问其它对象的数据比访问自己的数据更多。

原因:在将字段移动到数据类后,可能会发生这种情况。

策略:使用「移动方法」将这些方法移动到对应的类中,以化解其 “相思之苦 ”,以 “少数服从多数” 的原则为主。

2. Inappropriate Intimacy(狎昵关系)

特征:两个类的联系过于紧密,使用了大量彼此的成员。

原因:类没有合理设计。

策略:

  • 采用「移动方法」和「移动字段」来减少关联。
  • 运用「将双向关联改为单向关联」,降低类之间过多的依存。
  • 过提取类两个类之间的共同点移植到一个新的类中,通过新的类来牵线搭桥。

3. Message Chains(过度耦合的消息链)

特征:在调用一个对象时,这个对象往往会调用另一个对象,被调用对象又会继续调用其他…。

原因:编写代码时怎么样写方便怎么来,导致需要的方法在不同对象。

策略:观察消息链最终得到的对象是用来干什么的,看看能否以提取方法的方式把使用该对象的代码提炼到一个独立函数中,再运用移动方法把这个函数推入消息链 。

4. Middle Man(中间人)

特征:我们经常会看到某个类有一半的函数都委托给其他类,这样就是过度委托。

原因:封装 - 对外部隐藏内部细节,通常封装会伴随着委托。委托过多久成了过度委托。

策略:当委托过多时,应该「移除中间委托」直接和负责对象交接。

3.2.6 Other Smells

其他的坏代码,暂无更多。

Incomplete Library Class(不完美的程序库类)

特征:当一个类库已经不能满足实际需要时,你就不得不改变这个库。

原因:类的作者没有未卜先知的能力,因此库往往构造的不够好。

策略 :

  • 如果可以修改直接修改最好。
  • 如果无法直接修改,并且只想修改其中的一两个函数,可以采用「引入外加函数」策略,以外加函数的方式来实现一项新功能。
  • 如果需要建立大量的额外函数,可应该采用「引入本地扩展」建立一个新类。

3.3 常见的代码优化细节

3.3.1 变量细节

1. 销毁变量

对于 global 变量应该用完就销毁掉,可以使用 unset 函数销毁。

2. 数组元素

数组的 key 添加引号,速度会比不加快。当不加时默认会先作为常量然后才当做字符串。

3. 谨慎声明全局变量

声明一个未被任何函数使用的全局变量,性能也会降低,所以谨慎声明。

4. 引号

单引号的性能优于双引号,双引号会优先解析所包裹的内容。

5. 变量复制

如非必要,不要为了方便或者整洁就把一个变量复制到另一个变量中。例如 $password = $_POST[‘password’]。

6. 多维数组

多维数组尽量不要循环嵌套赋值。

3.3.2 函数细节

1. echo 与 print

echo 效率高于 print,echo 是没有返回值的。

2. require_once/include_once

相对于 require_once/include_once 更推荐使用 require/include,因为 once 需要判断一次。

3. 引入路径

推荐使用绝对路径,因为相对路径会在 include_path 里遍历查找文件。

4. 时间获取

如果需要获取脚本执行时间,$_SERVER[‘REQUEST_TIME’] 优于 time( )。

5. 正则表达式

能不用正则表达式尽量不用,正则表达式很耗性能,匹配的越多越耗性能。

6. 替换

替换字符串之前,先使用 strpos 查找是否存在(非常快)。替换函数 str_replace 优于 preg_replace。

7. 参数

当参数不多,并且一个函数既能接受数组又能接受字符串,优先传递字符串。

8. 局部变量

在函数内创建局部变量调用最快,能作为局部变量优先局部。

9. 提前声明局部变量

如果使用一个变量时,使用一个已经定义过的局部变量比建立一个未声明的局部变量快,所以建议优先定义再使用。

10. echo

使用 echo 时,‘,’ 的性能优于 ‘.’。注意:echo 是语言结构而不是真正的函数。

11. isset 代替 strlen

在某些时候,可以使用 isset 代替 strlen(strlen 也足够快)。

12. file_get_contents

可以用 file_get_contents( ) 替代 file( )、fopen( )、feof( )、fgets( ) 等方法的情况下,尽量用 file_get_contents( ),因为他的效率高得多。

13. 预定义函数

如果可以,尽量使用 PHP 内部提供的函数。

14. split 与 explode

split 比 explode 快,尽量优先运用 split。

3.3.3 类细节

1. 尽量静态化

如果一个方法可以被静态化,那就尽量声明为静态的。

静态方法和非静态方法的效率主要区别在内存:静态方法在程序开始时生成内存,非静态方法在程序运行中生成内存,静态速度会快但是也会占内存。

2. 魔术方法

避免使用类似 __get__set 等魔术方法。

3. 类与方法

类的性能和方法个数没有关系。

4. 方法所属

同样的方法,在子类中的性能优于在父类中。

5. 函数与方法

同样功能,调用函数性能优于调用类方法。

6. 数据库操作

php 操作数据库不建议使用 mysql,而建议使用 增强型的 mysqli 或者 pdo。

7. 面向对象

面向对象的开销相对较大,所以并不是所有的都要面向对象。

3.3.4 其他细节

1. 循环最大值

如果在循环中有最大值,需要在循环之前就设置好。

2. 循环变量

循环内部不要声明变量,尤其是大的变量,比如 对象

3. 最好不用 @

用 @ 掩盖错误会降低脚本运行速度,并且在后台有很多额外操作。

4. 循环中的函数

尽量不要在循环中使用函数,例如通过 count 统计数组的元素个数,可以放到循环外部,直接使用统计的值,避免每次都统计一次。

5. 三元运算符

如果可以,优先使用三元运算符。

6. 自增

使用自增时,++$i 会笔记 $i++ 快一些。

7. 分支语句

switch - case 好于使用多个 if - else if 语句,并且代码更加容易阅读和维护。

8. foreach

foreach 效率更高,所以尽量使用 foreach 代替 for 或者 while 循环。

并不是说遇到上述情况就一定要改变,需要结合实际情况从多方面考虑是否改变,否则一味改变反而会得不偿失。

3.4 理解垃圾回收机制

3.4.1 垃圾回收简介

垃圾回收(英语:Garbage Collection,缩写为 GC ) ,顾名思义,是一种自动的内存管理机制。当一个电脑上的动态内存不再需要时,就应该予以释放,以让出内存,这种内存资源管理的机制,称为垃圾回收。垃圾回收机制可以让程序员不必过分关心程序内存分配,从而将更多的精力投入到业务逻辑。

机制升级

在 PHP5.2 及更早版本的 PHP 中,引擎在判断一个变量空间是否能够被释放是依据这个变量的 zval 的 refcount 的是否为 0,如果为 0 变量的空间可以被释放,否则就不释,这是一种简单的 GC 实现方案。在这种简单的 GC 实现方案中,出现了意想不到的变量内存泄漏情况,引擎无法回收这些内存。于是在 PHP5.3 中出现了新的 GC,新的 GC 有专门的机制负责清理垃圾数据,防止内存泄漏。

3.4.2 变量简介

在继续了解垃圾回收机制之前,我们需要先知道一个前提知识点 - 变量。

PHP 中变量存在于一个 zval 的变量容器中,此结构包含了变量的类型和值信息,这两个是众所周知的。另外还有两个字段信息 - is_ref 与 refcount。

is_ref 字段主要作用是用来标识变量是否是一个引用,通过该字段 PHP 可以区分一般变量和引用变量。refcount 字段表示有多少个变量指向这个 zval 容器,当该字段为 0 则说明没有任何变量指向该字段,就可以被释放。

例 :

# 1.当创建一个变量 a
$a = 'hello world';
## 对应的信息为  a: (refcount=1, is_ref=0)

# 赋值
# 2.创建变量 b,其值为 a 变量
$b = $a;
## 对应的信息为  a: (refcount=2, is_ref=0)
!!! 这里需要注意的是此时 a 和 b 两个变量指向同一个 zval, 只有当原变量发生改变时, 才会为新变量分配内存空间, 同时原变量的 refcount 减 1。当然, 如果 unset 原变量, 新变量直接就使用原变量的 zval 而不是重新分配。

# 引用
# 3.创建变量 c, 其值为 a 的引用赋值
$c = &$a;
## 此时对应的信息为  a:(refcount=2, is_ref=1)

通过上述例子,我们对 refcount 与 is_ref 字段有了一些了解了。但是上述主要是以标量为例,复合类型会相对麻烦。

安装 xdebug 拓展后,可以利用 xdebug_debug_zval 打印出 zval 容器信息。

3.4.3 复合类型

废话不多说,我们直接创建一个数组来查看 :

$a = array( 'meaning' => 'life', 'number' => 42 );
## 对应信息为:
a: (refcount=1, is_ref=0)=array (
   'meaning' => (refcount=1, is_ref=0)='life',
   'number' => (refcount=1, is_ref=0)=42
)

图示为 :

在这里插入图片描述

这是正常情况下所创建的一个数组,接下来看一个不正常的 :

# 在创建一个数组后,我们直接使用该
$a = array( 'one' );
$a[] =& $a;
## 对应信息为:
a: (refcount=2, is_ref=1)=array (
   0 => (refcount=1, is_ref=0)='one',
   1 => (refcount=2, is_ref=1)=...  # 这里的 ... 代表发生了递归操作,而这里则是指向数组本身
)

图示为 :

在这里插入图片描述

此时我们删除一个变量将删除这个符号,且 refcount 也会减 1。结果为 :

# 使用 unset 删除 $a
(refcount=1, is_ref=1)=array (
   0 => (refcount=1, is_ref=0)='one',
   1 => (refcount=1, is_ref=1)=...
)

对应图示为 :

在这里插入图片描述

此时用户不能再使用 $a 了,但是 refcount 的值没有归零,所以 5.2 及更早版本的 PHP 不会自动释放这一部分,这一部分即为所谓的 ‘垃圾’。在请求结束的时候倒是会清除,但是在清除之前会消耗不少内存。出现的情况较少还好,当情况出现的过多时,就会容易导致内存不足而崩溃。

垃圾开关

由于垃圾回收算法做了额外的事情,所以这也是相应的会消耗一些时间,垃圾回收也是一种时间换空间的机制。但 PHP 比较人性化的给我们提供了开关,可以根据自己的需求设置 :

  1. 通过 ini 文件中的 zend.enable_gc 项来开启或则关闭 GC(PHP 默认开启)。
  2. 在程序中使用 gc_enable() 和 gc_disable() 开启和关闭。

在 PHP 手册「性能方面考虑的因素」一文中对性能方面做了测试。开启垃圾回收时,消耗的时间百分比是 7%,而节省的内存则是 98%(数据仅供参考)。以上可以根据自己的需求来决定自己的设置。

3.4.4 垃圾回收机制

在上一部分中,我们通过一个案例了解到了什么是 ‘垃圾’。PHP 5.3 及之后版本采用了比较复杂的算法来处理环状引用导致内存泄露的问题(此处不深究算法)。当一个 zval 可能为垃圾时,回收算法会把该 zval 放入一个内存缓冲区。当缓冲区达到最大临界值时,回收算法会循环遍历所有缓冲区中的 zval,判断其是否为垃圾,如果是则进行释放处理。

在 PHP5.3 的 GC 中,针对的垃圾做了如下准则 :

  1. 如果一个 zval 的 refcount 增加,那么此 zval 还在使用,判定不是垃圾,不会进入缓冲区。
  2. 如果一个 zval 的 refcount 减少到 0, 那么 zval 会被立即释放掉,不属于 GC 要处理的垃圾对象,不会进入缓冲区。
  3. 如果一个 zval 的 refcount 减少之后大于 0,该 zval 还不能被释放,那么该 zval 可能成为一个垃圾,将被放入缓冲区。

只有在准则 3 下,GC 才会把 zval 收集起来,然后通过新的算法来判断此 zval 是否为垃圾。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

纳西妲爱编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值