php垃圾回收机制

写时复制

首先,php的变量复制用的是写时复制方式,举个例子.

1
2
3
4
5
6
7
8
9
10
$a='蛋蛋'.time();
$b=$a;
$c=$a;
//这个时候内存占用相同,$b,$c都将指向$a的内存,无需额外占用

$b='蛋蛋1号';
//这个时候$b的数据已经改变了,无法再引用$a的内存,所以需要额外给$b开拓内存空间

$a='蛋蛋2号';
//$a的数据发生了变化,同样的,$c也无法引用$a了,需要给$a额外开拓内存空间

引用计数

既然变量会引用内存,那么删除变量的时候,就会出现一个问题了:

1
2
3
4
5
6
7
8
9
10
$a='蛋蛋';
$b=$a;
$c=$a;
//这个时候内存占用相同,$b,$c都将指向$a的内存,无需额外占用

$b='蛋蛋1号';
//这个时候$b的数据已经改变了,无法再引用$a的内存,所以需要额外给$b开拓内存空间

unset($c);
//这个时候,删除$c,由于$c的数据是引用$a的数据,那么直接删除$a?

很明显,当$c引用$a的时候,删除$c,不能把$a的数据直接给删除,那么该怎么做呢?
这个时候,php底层就使用到了引用计数这个概念
引用计数,给变量引用的次数进行计算,当计数不等于0时,说明这个变量已经被引用,不能直接被回收,否则可以直接回收,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$a = '蛋蛋'.time();
$b = $a;
$c = $a;

xdebug_debug_zval('a');
xdebug_debug_zval('b');
xdebug_debug_zval('c');

$b='蛋蛋2号';
xdebug_debug_zval('a');
xdebug_debug_zval('b');

echo "脚本结束\n";


//输出

a: (refcount=3, is_ref=0)='蛋蛋1578154814'
b: (refcount=3, is_ref=0)='蛋蛋1578154814'
c: (refcount=3, is_ref=0)='蛋蛋1578154814'
a: (refcount=2, is_ref=0)='蛋蛋1578154814'
b: (refcount=1, is_ref=0)='蛋蛋2号'
脚本结束

注意,xdebug_debug_zval函数是xdebug扩展的,使用前必须安装xdebug扩展

引用计数特殊情况

当变量值为整型,浮点型时,在赋值变量时,php7底层将会直接把值存储(php7的结构体将会直接存储简单数据类型),refcount将为0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$a = 1111;
$b = $a;
$c = 22.222;
$d = $c;

xdebug_debug_zval('a');
xdebug_debug_zval('b');
xdebug_debug_zval('c');
xdebug_debug_zval('d');
echo "脚本结束\n";


//输出

a: (refcount=0, is_ref=0)=1111
b: (refcount=0, is_ref=0)=1111
c: (refcount=0, is_ref=0)=22.222
d: (refcount=0, is_ref=0)=22.222
脚本结束

当变量值为interned string字符串型(变量名,函数名,静态字符串,类名等)时,变量值存储在静态区,内存回收被系统全局接管,引用计数将一直为1(php7.3)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$str = '蛋蛋';    // 静态字符串
$str = '蛋蛋' . time();//普通字符串


$a = 'aa';
$b = $a;
$c = $b;

$d = 'aa'.time();
$e = $d;
$f = $d;

xdebug_debug_zval('a');
xdebug_debug_zval('d');
echo "脚本结束\n";

//输出

a: (refcount=1, is_ref=0)='aa'
d: (refcount=3, is_ref=0)='aa1578156506'
脚本结束

当变量值为以上几种时,复制变量将会直接拷贝变量值,所以将不存在多次引用的情况

###引用时引用计数变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$a = 'aa';
$b = &$a;
$c = $b;

xdebug_debug_zval('a');
xdebug_debug_zval('b');
xdebug_debug_zval('c');
echo "脚本结束\n";

//输出

a: (refcount=2, is_ref=1)='aa'
b: (refcount=2, is_ref=1)='aa'
c: (refcount=1, is_ref=0)='aa'
脚本结束

当引用时,被引用变量的value以及类型将会更改为引用类型,并将引用值指向原来的值内存地址中.

之后引用变量的类型也会更改为引用类型,并将值指向原来的值内存地址,这个时候,值内存地址被引用了2次,所以refcount=2.

而$c并非是引用变量,所以将值复制给了$c,$c引用还是为1

详细引用计数知识,底层原理可查看:https://www.cnblogs.com/sohuhome/p/9800977.html

php生命周期

php将每个运行域作为一次生命周期,每次执行完一个域,将回收域内所有相关变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php
/**
* Created by PhpStorm.
* User: Tioncico
* Date: 2020/1/6 0006
* Time: 14:22
*/

echo "php文件的全局开始\n";

class A{
protected $a;
function __construct($a)
{
$this->a = $a;
echo "类A{$this->a}生命周期开始\n";
}
function test(){
echo "类test方法域开始\n";
echo "类test方法域结束\n";
}
//通过类析构函数的特性,当类初始化或回收时,会调用相应的方法
function __destruct()
{
echo "类A{$this->a}生命周期结束\n";
// TODO: Implement __destruct() method.
}
}

function a1(){
echo "a1函数域开始\n";
$a = new A(1);
echo "a1函数域结束\n";
//函数结束,将回收所有在函数a1的变量$a
}
a1();

$a = new A(2);

echo "php文件的全局结束\n";
//全局结束后,会回收全局的变量$a

可看出,每个方法/函数都作为一个作用域,当运行完该作用域时,将会回收这里面的所有变量.

再看看这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
echo "php文件的全局开始\n";

class A
{
protected $a;

function __construct($a)
{
$this->a = $a;
echo "类{$this->a}生命周期开始\n";
}

function test()
{
echo "类test方法域开始\n";
echo "类test方法域结束\n";
}

//通过类析构函数的特性,当类初始化或回收时,会调用相应的方法
function __destruct()
{
echo "类{$this->a}生命周期结束\n";
// TODO: Implement __destruct() method.
}
}

$arr = [];
$i = 0;
while (1) {
$arr[] = new A('arr_' . $i);
$obj = new A('obj_' . $i);
$i++;
echo "数组大小:". count($arr).'\n';
sleep(1);
//$arr 会随着循环,慢慢的变大,直到内存溢出

}

echo "php文件的全局结束\n";
//全局结束后,会回收全局的变量$a

全局变量只有在脚本结束后才会回收,而在这份代码中,脚本永远不会被结束,也就说明变量永远不会回收,$arr还在不断的增加变量,直到内存溢出.

内存泄漏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function a(){
class A {
public $ref;
public $name;

public function __construct($name) {
$this->name = $name;
echo($this->name.'->__construct();'.PHP_EOL);
}

public function __destruct() {
echo($this->name.'->__destruct();'.PHP_EOL);
}
}

$a1 = new A('$a1');
$a2 = new A('$a2');
$a3 = new A('$3');

$a1->ref = $a2;
$a2->ref = $a1;

unset($a1);
unset($a2);

echo('exit(1);'.PHP_EOL);
}
a();
echo('exit(2);'.PHP_EOL);

当$a1和$a2的属性互相引用时,unset($a1,$a2) 只能删除变量的引用,却没有真正的删除类的变量,这是为什么呢?

首先,类的实例化变量分为2个步骤,1:开辟类存储空间,用于存储类数据,2:实例化一个变量,类型为class,值指向类存储空间.

当给变量赋值成功后,类的引用计数为1,同时,a1->ref指向了a2,导致a2类引用计数增加1,同时a1类被a2->ref引用,a1引用计数增加1

当unset时,只会删除类的变量引用,也就是-1,但是该类其实还存在了一次引用(类的互相引用),

这将造成这2个类内存永远无法释放,直到被gc机制循环查找回收,或脚本终止回收(域结束无法回收).

手动回收机制

在上面,我们知道了脚本回收,域结束回收2种php回收方式,那么可以手动回收吗?答案是可以的.

手动回收有以下几种方式:

unset,赋值为null,变量赋值覆盖,gc_collect_cycles函数回收

unset

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class A
{
public $ref;
public $name;

public function __construct($name)
{
$this->name = $name;
echo($this->name . '->__construct();' . PHP_EOL);
}

public function __destruct()
{
echo($this->name . '->__destruct();' . PHP_EOL);
}
}

$a = new A('$a');
$b = new A('$b');
unset($a);
//a将会先回收
echo('exit(1);' . PHP_EOL);
//b需要脚本结束才会回收

//输出
$a->__construct();
$b->__construct();
$a->__destruct();
exit(1);
$b->__destruct();

unset的回收原理其实就是引用计数-1,当引用计数-1之后为0时,将会直接回收该变量,否则不做操作(这就是上面内存泄漏的原因,引用计数-1并没有等于0)

=null回收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class A
{
public $ref;
public $name;

public function __construct($name)
{
$this->name = $name;
echo($this->name . '->__construct();' . PHP_EOL);
}

public function __destruct()
{
echo($this->name . '->__destruct();' . PHP_EOL);
}
}

$a = new A('$a');
$b = new A('$b');
$c = new A('$c');
unset($a);
$c=null;
xdebug_debug_zval('a');
xdebug_debug_zval('b');
xdebug_debug_zval('c');

echo('exit(1);' . PHP_EOL);

=null和unset($a),作用其实都为一致,null将变量值赋值为null,原先的变量值引用计数-1,而unset是将变量名从php底层变量表中清理,并将变量值引用计数-1,唯一的区别在于,=null,变量名还存在,而unset之后,该变量就没了:

1
2
3
4
5
6
7
8
9
10
$a->__construct();
$b->__construct();
$c->__construct();
$a->__destruct();
$c->__destruct();
a: no such symbol //$a已经不在符号表
b: (refcount=1, is_ref=0)=class A { public $ref = (refcount=0, is_ref=0)=NULL; public $name = (refcount=1, is_ref=0)='$b' }
c: (refcount=0, is_ref=0)=NULL //c还存在,只是值为null
exit(1);
$b->__destruct();

变量覆盖回收

通过给变量赋值其他值(例如null)进行回收:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class A
{
public $ref;
public $name;

public function __construct($name)
{
$this->name = $name;
echo($this->name . '->__construct();' . PHP_EOL);
}

public function __destruct()
{
echo($this->name . '->__destruct();' . PHP_EOL);
}
}

$a = new A('$a');
$b = new A('$b');
$c = new A('$c');
$a=null;
$c= '练习时长两年半的个人练习生';
xdebug_debug_zval('a');
xdebug_debug_zval('b');
xdebug_debug_zval('c');

echo('exit(1);' . PHP_EOL);

//输出

$a->__construct();
$b->__construct();
$c->__construct();
$a->__destruct();
$c->__destruct();
a: (refcount=0, is_ref=0)=NULL
b: (refcount=1, is_ref=0)=class A { public $ref = (refcount=0, is_ref=0)=NULL; public $name = (refcount=1, is_ref=0)='$b' }
c: (refcount=1, is_ref=0)='练习时长两年半的个人练习生'
exit(1);
$b->__destruct();

可以看出,c由于覆盖赋值,将原先A类实例的引用计数-1,导致了$c的回收,但是从程序的内存占用来说,覆盖变量并不是意义上的内存回收,只是将变量的内存修改为了其他值.内存不会直接清空.

gc_collect_cycles

回到之前的内存泄漏章节,当写程序不小心造成了内存泄漏,内存越来越大,可是php默认只能脚本结束后回收,那该怎么办呢?我们可以使用gc_collect_cycles 函数,进行手动回收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
function a(){
class A {
public $ref;
public $name;

public function __construct($name) {
$this->name = $name;
echo($this->name.'->__construct();'.PHP_EOL);
}

public function __destruct() {
echo($this->name.'->__destruct();'.PHP_EOL);
}
}

$a1 = new A('$a1');
$a2 = new A('$a2');

$a1->ref = $a2;
$a2->ref = $a1;

$b = new A('$b');
$b->ref = $a1;

echo('$a1 = $a2 = $b = NULL;'.PHP_EOL);
$a1 = $a2 = $b = NULL;
echo('gc_collect_cycles();'.PHP_EOL);
echo('// removed cycles: '.gc_collect_cycles().PHP_EOL);
//这个时候,a1,a2已经被gc_collect_cycles手动回收了
echo('exit(1);'.PHP_EOL);

}
a();
echo('exit(2);'.PHP_EOL);

//输出

$a1->__construct();
$a2->__construct();
$b->__construct();
$a1 = $a2 = $b = NULL;
$b->__destruct();
gc_collect_cycles();
$a1->__destruct();
$a2->__destruct();
// removed cycles: 4
exit(1);
exit(2);

注意,gc_colect_cycles 函数会从php的符号表,遍历所有变量,去实现引用计数的计算并清理内存,将消耗大量的cpu资源,不建议频繁使用

另外,除去这些方法,php内存到达一定临界值时,会自动调用内存清理(我猜的),每次调用都会消耗大量的资源,可通过gc_disable 函数,去关闭php的自动gc

用css3 media实现响应式布局

##前言
响应式布局,说直白点就是一个网站能够兼容多个终端,可以按不同的分辨率显示不同的状态。而实现这个就要用到css3的Media Queries(媒介查询)。这个功能非常的强大,但是有优点的同时,缺点也是会存在的。那就是兼容各种设备工作量大,效率低下,加载时间长等。但是学起来很容易,看完下面的代码你就会了。

###示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
.page{
width:960px;
height:1000px;
margin:0 auto;
background:#CCC;
}
/* 设备最大宽度960px */
@media screen and (max–width: 960px) {
.page{
width:100%;
background:#69F;
}
}
/* 宽度大于480px且小于768px */
@media screen and (min–width: 480px) and (max–width:768px) {
.page{
width:100%;
background:#F00;
}
}
/* 设备最大宽度480px */
@media screen and (max–width:480px){
.page{
width:100%;
background:#00FF00;
}
}

这样就可以在不同的分辨率下采取不同的样式了。

另外还有一点,如果是移动端开发,一定要在头部加上以下代码。

怎么样,很容易吧。当然这只是响应式布局的一部分,其他的可以网上搜索相关资料。以上,只是个人对于响应式布局的一些理解,技术更新的速度很快,所以我们也要与时俱进。

用一个数值保存多选的值

前言

在开发过程中,对于网页中的多选,我们有很多种存储方式,常见的如逗号分隔。下文介绍一种通用设计方式:用一个整数来存储复选框的值。

准备知识 —— 位与运算

位与运算:二进制运算,相同位的两个数字都为1,则为1;若有一个不为1,则为0,如:

1
2
3
4
5
 下方是运算
00101
& 11100
------------
00100

设计

将多项的选项值分别设置为 2 的 n 次方,n 从 0 开始,每多一项,n + 1。即 1,2,4,8…
多选的存储值为各项值之和,如选中了第 1、3 项,则值为:1 + 4 = 5

1
$ hexo server

回显

假设存储的值为 5 ,要使相应的项被勾选,则是循环多项的值,每项与存储值 5 进行 位与运算,如果值与选项本身的值相等,则选中该项;相反地,如果运算值为 0 ,则设置为不选中:

1
2
3
4
5
下方回显
1 & 5 = 1
2 & 5 = 0
4 & 5 = 4
8 & 5 = 0

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!DOCTYPE html> 
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Checkbox Test</title>
</head>
<body>
<form>
<input type="checkbox" name="test" value="1"> 1
<input type="checkbox" name="test" value="2"> 2
<input type="checkbox" name="test" value="4"> 4
<input type="checkbox" name="test" value="8"> 8
</form>
<input type="text" id="result" placeholder="设置要回显的值">
<button id="show">回显</button>
<script src="http://libs.baidu.com/jquery/1.11.1/jquery.min.js"></script>
<script>
$(function () {
$("[name='test']").on("change", function () {
var result = 0;
$("[name='test']:checkbox:checked").each(function(){
result += parseInt($(this).val());
});
$("#result").val(result); });
$("#show").on("click", function () {
var result = parseInt($("#result").val());
$("[name='test']:checkbox").each(function(){
var value = parseInt($(this).val());
if ((result & value) == value) {
$(this).prop("checked", true);
} else {
$(this).prop("checked", false);
}
});
});
});
</script>
</body>
</html>

通过PHP生成PHP

背景介绍

百度收录的页面比较慢,最近新的三方流以及方案页面发布出来,想着每天多一点抓取。现在整体的方案是:

  1. 页面有点击的时候, js 自动推送
  2. 通过更新 sitemap.xml 自动让蜘蛛来抓取
  3. 通过脚本主动推送新页面到引擎

今天主要是上述的第三点,主动推送新页面到百度引擎中去。尝试的方法是通过 php 代码生成 php 代码。

注意事项

  1. EOF 的结尾空格问题
  2. EOF 中的 $ 符号要转义
  3. 纯属手痒尝试,模仿请自行负责

代码留存备份

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?php
include('head.php');
$sql = "select * from flow where published='y'";

$f_values = '';
$f_values .=<<<EOF
<?php
\$urls = [
EOF;
$flows = get_table_data($sql,$mysqli_space);
if(false == empty($flows))
{
foreach($flows as $flow)
{
$f_values.=<<<EOF
'https://www.12306.com/flow-{$flow['d_id']}.html',
EOF;
$f_values.=PHP_EOL;
}
}
//案例详情
$sql_bcase = "select * from beautycase where published='y'";

$cases = get_table_data($sql_bcase,$mysqli_space);
if(false == empty($cases))
{
foreach($cases as $case)
{
$f_values.=<<<EOF
'https://www.12306.com/beautycase-{$case['d_id']}.html',
EOF;
$f_values.=PHP_EOL;
}
}


$f_values.=<<<EOF
];
\$api = 'http://data.zz.baidu.com/urls?site=www.12306.com&token=abcdefg';
\$ch = curl_init();
\$options = array(
CURLOPT_URL => \$api,
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => implode("\n", \$urls),
CURLOPT_HTTPHEADER => array('Content-Type: text/plain'),
);
curl_setopt_array(\$ch, \$options);
\$result = curl_exec(\$ch);
echo \$result;
EOF;
file_put_contents('/www/wwwroot/TransDataToPub/putUrls.php',$f_values);

order_by_rand引发的大报错

症状一 磁盘满了

40G 的磁盘突然满了,查看后发现是 mysql 的 ibtmp1 文件超过 32G 了,这个是临时文件,通过修改配置解决。

ibtmp1是非压缩的 innodb 临时表的独立表空间,通过 innodb_temp_data_file_path 参数指定文件的路径,文件名和大小,默认配置为 ibtmp1:12M:autoextend,也就是说在支持大文件的系统这个文件大小是可以无限增长的。

1
2
3
4
5
6
# 使用的命令
du -h --max-depth=1
ls -alh

# my.cnf 配置增加
innodb_temp_data_file_path = ibtmp1:12M:autoextend:max:5G

当时这里忽略了一个问题,为什么这个文件体积暴增?

症状二 程序执行很慢,定时任务重复执行

因为要全量发布文章,定时任务每 5 分钟执行发布 70 篇文章,发现多了好多进程,根据启动时间说明 5 分钟 70 篇文章没有发布完,导致下一个任务启动重复从数据库里取数据发布,这个时候想到的是定时任务加锁,防止重复执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1. 利用临时文件是否存在来实现
$lockFile = '/home/cronlock/test.lock';
if(file_exists($lockFile)) {
exit('not finish);
}
touch($lockFile);
doTheCron();
unlink($lockFile);

2. 利用排它锁来实现
$fp = fopen("/tmp/testlock.txt","w+");
if(flock($fp,LOCK_EX|LOCK_NB)){
doTheCron()
flock($fp,LOCK_UN)
}else{
echo('文件已锁定');
}
fclose($fp);

3. linux flock 锁机制 不需要改代码
*/5 * * * * flock -xn /tmp/test.lock -c 'sudo -u www /usr/bin/php /www/wwwroot/admin.abc.com/crontab.php' >> /www/abc/data/crontab.log

症状三 php 执行慢

观察文章发布日志,发现获取推荐文章比较慢,进而发现 order by rand() 函数,以为一共 2 万条数据不会慢到哪里去,没想到这个是巨坑。

1
2
3
4
5
6
7
8

#sql:select d_id ,{文章标题},{文章作者id},{作者名称},{文章摘要},{文章内容},{图片展示}, url, createdatetime, savedatetime from {文章详情页} where published='y' order by rand() limit 5

#sql:select d_id ,{文章标题},{文章作者id},{作者名称},{文章摘要},{文章内容},{图片展示}, url, createdatetime, savedatetime from {文章详情页} where published='y' and d_id>=(SELECT floor(RAND() * (SELECT MAX(d_id) FROM {文章详情页}))) ORDER BY d_id LIMIT 5;

#sql: select d_id ,{文章标题},{文章作者id},{作者名称},{文章摘要},{文章内容},{图片展示}, url, createdatetime, savedatetime {文章详情页} AS t1 JOIN (SELECT ROUND(RAND() * (SELECT MAX(id) FROM {文章详情页})) AS id) AS t2
WHERE t1.d_id >= t2.d_id
ORDER BY t1.d_id ASC LIMIT 5

修改完,2 分钟内 2000 条文章发布完毕。