美文网首页
PHP学习笔记-内存溢出解决方案

PHP学习笔记-内存溢出解决方案

作者: 赵客缦胡缨v吴钩霜雪明 | 来源:发表于2021-01-07 10:49 被阅读0次

一.内存溢出解决方案

在做数据统计分析时,经常会遇到大数组,可能会发生内存溢出,这里分享一下我的解决方案。

假定日志中存放的记录数为500000条,那么解决方案如下:

ini_set(‘memory_limit’,’64M’); //重置php可以使用的内存大小为64M

一般在远程主机上是不能修改php.ini文件的,只能通过程序设置。

注:在safe_mode(安全模式)下,ini_set失效

set_time_limit(600);//设置超时限制为6分钟
$farr = $Uarr = $Marr = $IParr = $data = $_sub = array();
$spt = ”$@#!$”;
$root = ”/Data/webapps/VisitLog”;
$path = $dpath = $fpath = NULL;
$path = $root.”/”.date(“Y-m”,$timestamp);
$dpath = $path.”/”.date(“m-d”,$timestamp);
for($j=0;$j<24;$j++){
$v = ($j < 10) ? ”0″.$j : $j;
$gpath = $dpath.”/”.$v.”.php”;
if(!file_exists($gpath)){
continue;
} else {
$arr = file($gpath);////将文件读入数组中
array_shift($arr);//移出第一个单元-》<?php exit;?>
$farr = array_merge($farr,$arr);
unset($arr);
}
}
if(empty($this->farr)){
echo ”<p><center>没有相关记录!</center></p>”;
exit;
}
while(!empty($farr)){
$_sub = array_splice($farr, 0, 10000); //每次取出$farr中1000个
for($i=0,$scount=count($_sub);$i<$scount;$i++){
$arr = explode($spt,$_sub[$i]);
$Uarr[] = $arr[1]; //vurl
$Marr[] = $arr[2]; //vmark
$IParr[] = $arr[3].” |$nbsp;”.$arr[1]; //IP
}
unset($_sub);//用完及时销毁
}
unset($farr);

这里,不难看出,一方面,我们要增加PHP可用内存大小,另一方面,只要我们想办法对数组进行分批处理,分而治之,将用过的变量及时销毁(unset),一般是不会出现溢出问题的。

另外,为了节省PHP程序内存损耗,我们应当尽可能减少静态变量的使用,在需要数据重用时,可以考虑使用引用(&)。

再一点就是:数据库操作完成后,要马上关闭连接;

一个对象使用完,要及时调用析构函数(__destruct())。

二.unset销毁变量并释放内存问题

PHP的unset()函数用来清除、销毁变量,不用的变量,我们可以用unset()将它销毁。

但是某些时候,用unset()却无法达到销毁变 量占用的内存!

<?php
$s=str_repeat('1',255); //产生由255个1组成的字符串
$m=memory_get_usage(); //获取当前占用内存
unset($s);
$mm=memory_get_usage(); //unset()后再查看当前占用内存
echo $m-$mm;
?>

最后输出unset()之前占用内存减去unset()之后占用内存,如果是正数,那么说明unset(s)已经将s从内存中销毁(或者 说,unset()之后内存占用减少了),可是我在PHP5和windows平台下,得到的结果是:0。

这是否可以说明,unset(s)并没有起 到销毁变量s所占用内存的作用呢?我们再作下面的例子:

<?php
$s=str_repeat('1',256); //产生由256个1组成的字符串
$m=memory_get_usage(); //获取当前占用内存
unset($s);
$mm=memory_get_usage(); //unset()后再查看当前占用内存
echo $m-$mm;
?>

这个例子,和上面的例子几乎相同,唯一的不同是,s由256个1组成,即比第一个例子多了一个1,得到结果是:272。这是否可以说 明,unset(s)已经将$s所占用的内存销毁了?
通过上面两个例子,我们可以得出以下结论:

结论一、unset()函数只能在变量值占用内存空间超过256字节时才会释放内存空间。

那么是不是只要变量值超过256,使用unset就可以释放内存空间呢?我们再通过一个例子来测试一下:

<?php
$s=str_repeat('1',256); //这和第二个例子完全相同
$p=&$s;
$m=memory_get_usage();
unset($s); //销毁$s
$mm=memory_get_usage();
echo $p.'<br />';
echo $m-$mm;
?>

刷新页面,我们看到第一行有256个1,第二行是0,按理说我们已经销毁了s,而p只是引用s的变量,应该是没有内容了,另 外,unset(s)前后内存占用没变化!现在我们再做以下的例子:

<?php
$s=str_repeat('1',256); //这和第二个例子完全相同
$p=&$s;
$m=memory_get_usage();
$s=null; //设置$s为null
$mm=memory_get_usage();
echo $p.'<br />';
echo $m-$mm;
?>

现在刷新页面,我们看到,输出$p已经是没有内容了,unset()前后内存占用量之差是272,即已经清除了变量占用的内存。

本例中的$s=null也 可以换成unset(),如下:

<?php
$s=str_repeat('1',256); //这和第二个例子完全相同
$p=&$s;
$m=memory_get_usage();
unset($s); //销毁$s
unset($p);
$mm=memory_get_usage();
echo $p.'<br />';
echo $m-$mm;
?>

我们将s和p都使用unset()销毁,这时再看内存占用量之差也是272,说明这样也可以释放内存。

那么,我们可以得到另外一条结论:

结论二、只有当指向该变量的所有变量(如引用变量)都被销毁后,才会释放内存。

三.PHP大量数据循环时内存耗尽问题的解决方案(适用于导出大量数据时内存耗尽情况)

最近在开发一个PHP程序时遇到了下面的错误:

PHP Fatal error: Allowed memory size of 268 435 456 bytes exhausted

错误信息显示允许的最大内存已经耗尽。

遇到这样的错误起初让我很诧异,但转眼一想,也不奇怪,因为我正在开发的这个程序是要用一个foreach循环语句在一个有4万条记录的表里全表搜索具有特定特征的数据,也就是说,一次要把4万条数据取出,然后逐条检查每天数据。

可想而知,4万条数据全部加载到内存中,内存不爆才怪。

毕竟编程这么多年,我隐约记得PHP里提供有非一次全部加载数据的API,是像处理流媒体那样,随用随取随丢、数据并不会积累在内存的查询方法。

经过简单的搜索,果然在官方网站上找到的正确的用法。

这个问题在PHP的官方网站上叫缓冲查询和非缓冲查询(Buffered and Unbuffered queries)

PHP的查询缺省模式是缓冲模式。

也就是说,查询数据结果会一次全部提取到内存里供PHP程序处理。

这样给了PHP程序额外的功能,比如说,计算行数,将 指针指向某一行等。

更重要的是程序可以对数据集反复进行二次查询和过滤等操作。

但这种缓冲查询模式的缺陷就是消耗内存,也就是用空间换速度。

相对的,另外一种PHP查询模式是非缓冲查询,数据库服务器会一条一条的返回数据,而不是一次全部返回,这样的结果就是PHP程序消耗较少的内存,但却增加了数据库服务器的压力,因为数据库会一直等待PHP来取数据,一直到数据全部取完。

很显然,缓冲查询模式适用于小数据量查询,而非缓冲查询适应于大数据量查询。

对于PHP的缓冲模式查询大家都知道,下面列举的例子是如何执行非缓冲查询API。

非缓冲查询方法一: mysqli

<?php 
$mysqli  = new mysqli("localhost", "my_user", "my_password", "world"); 
$uresult = $mysqli->query("SELECT Name FROM City", MYSQLI_USE_RESULT);

if ($uresult) { 
   while ($row = $uresult->fetch_assoc()) { 
       echo $row['Name'] . PHP_EOL; 
   } 
} 
$uresult->close(); 
?>

非缓冲查询方法二: pdo_mysql

<?php 
$pdo = new PDO("mysql:host=localhost;dbname=world", 'my_user', 'my_pass'); 
$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);

$uresult = $pdo->query("SELECT Name FROM City"); 
if ($uresult) { 
   while ($row = $uresult->fetch(PDO::FETCH_ASSOC)) { 
       echo $row['Name'] . PHP_EOL; 
   } 
} 
?>

非缓冲查询方法三: mysql

<?php 
$conn = mysql_connect("localhost", "my_user", "my_pass"); 
$db   = mysql_select_db("world");

$uresult = mysql_unbuffered_query("SELECT Name FROM City"); 
if ($uresult) { 
   while ($row = mysql_fetch_assoc($uresult)) { 
       echo $row['Name'] . PHP_EOL; 
   } 
} 
?>

四.大文件上传导内存溢出

报错情况:PHP Fatal error: Allowed memory size of 268 435 456 bytes exhausted

1.上传excel文件时,出现内存溢出的情况

在文件中分配大点的内存设置内存治标不治本,而且服务器的php.ini(memory_limit =
128M)有时候是很难改的。

所以在文件中设置。但是只有php.ini中的安全模式safe_mode开启时才可以设置

ini_set('memory_limit', '521M');

解决方法:

protected/extensions/ExcelHelper.php
$PHPReader->setReadDataOnly(true);  //在拿到数据后进行设置只读
    public static function importFromExcel($filePath, $blankRowDel = false)
    {
        set_time_limit(90);
        Yii::import('application.extensions.phpexcel.PHPExcel');
        $PHPExcel = new PHPExcel();
        //默认用excel2007读取excel,若格式不对,则用之前的版本进行读取
        $PHPReader = new PHPExcel_Reader_Excel2007();
        if (!$PHPReader->canRead($filePath)) {
            $PHPReader = new PHPExcel_Reader_Excel5();
            if (!$PHPReader->canRead($filePath)) {
                throw new Exception('can not read the excel file');
            }
        }
        $PHPReader->setReadDataOnly(true);

        $PHPExcel      = $PHPReader->load($filePath);
        $allSheetCount = $PHPExcel->getSheetCount();
        $excelData     = array();
        for ($currentSheetNum = 0; $currentSheetNum < $allSheetCount; $currentSheetNum++) {
            //读取excel文件中的第一个工作表
            $currentSheet = $PHPExcel->getSheet($currentSheetNum);
            //取得当前表名
            $currentSheetTitle = $currentSheet->getTitle();
            //取得最大的列号
            $allColumn = $currentSheet->getHighestColumn();
            //取得一共有多少行
            $allRow = $currentSheet->getHighestRow();
            // 从第一行获取列名
            $currentRow = 1;
            // 从第A列开始输出
            $colunmNameArray = array();
            $max_column_num  = PHPExcel_Cell::columnIndexFromString($allColumn);
            for ($current_column_num = 0; $current_column_num <= $max_column_num; $current_column_num++) {
                $currentColumn = PHPExcel_Cell::stringFromColumnIndex($current_column_num);
                $val           = $currentSheet->getCellByColumnAndRow($current_column_num, $currentRow)->getValue();
                if (empty($val)) {
                    continue;
                }
                if (is_object($val)) {
                    $colunmNameArray[$currentColumn] = '';
                    foreach ($val->getRichTextElements() as $cell) {
                        $colunmNameArray[$currentColumn] .= $cell->getText();
                    }
                } else {
                    $colunmNameArray[$currentColumn] = $val;
                }
            }

            //从第二行开始输出,因为excel表中第一行为列名
            $sheetData = array();
            for ($currentRow = 2; $currentRow <= $allRow; $currentRow++) {
                //从第A列开始输出 */
                $rowData   = array();
                $blankCell = 0;
                for ($current_column_num = 0; $current_column_num <= $max_column_num; $current_column_num++) {
                    $currentColumn = PHPExcel_Cell::stringFromColumnIndex($current_column_num);
                    $val           = $currentSheet->getCellByColumnAndRow($current_column_num, $currentRow)->getValue();
                    if (!isset($colunmNameArray[$currentColumn])) {
                        continue;
                    }
                    //如果输出汉字有乱码,则需将输出内容用iconv函数进行编码转换,如下将gb2312编码转为utf-8编码输出
                    if (is_object($val)) {
                        $rowData[$currentColumn] = '';
                        foreach ($val->getRichTextElements() as $cell) {
                            $rowData[$currentColumn] .= $cell->getText();
                        }
                    } else {
                        $rowData[$currentColumn] = $val;
                    }

                    if (empty($rowData[$currentColumn])) {
                        $blankCell++;
                    }
                }

                if (!$blankRowDel || chr($blankCell + 64) != $allColumn) {
                    $sheetData[] = $rowData;
                }
            }

            $excelData[$currentSheetTitle] = array(
                'header'  => $colunmNameArray,
                'content' => $sheetData,
            );
        }
        return $excelData;
    }

2.使用sql查询数据,查出来很多,导致内存溢出

sql语句在mysql中可以查询,但是使用php程序查询就报php内存溢出

(1)这个问题在php的官方网站叫缓冲查询和非缓冲查询。php的查询缺省模式是缓冲模式。也就是,查询数据结果一次全部提取到内存里供php程序额外的功能,比如说,计算行数,将指针指向某一行等。更重要的是程序对数据集反复进行二次查询和过滤操作。但这种缓冲查询模式的缺陷是消耗内存,也就是用空间换速度。

(2)另外一种查询模式是非缓冲查询,数据库服务器会一条一条的返回数据,而不是一次全部返回,这样的结果是php程序消耗较少的内存,但却增加了数据库服务器的压力,因为数据库会一直等待php来取数据,一直到数据全部取完。

1.首先查询数据库需要进行limit进行分页查询

2.如果不使用limit,使用非缓冲查询

1.mysql:
$conn = mysql_connect("localhost", "user", "pass");
$db   = mysql_select_db("world");
$uresult = mysql_unbuffered_query("SELECT Name FROM City");    //非缓冲查询
if ($uresult) {
   while ($row = mysql_fetch_assoc($uresult)) {
       echo $row['Name'] . PHP_EOL;
   }
}

2.pdo_mysql:
$pdo = new PDO("mysql:host=localhost;dbname=world", 'my_user', 'my_pass');
$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);    //设置这个属性,就为非缓冲查询
$uresult = $pdo->query("SELECT Name FROM City");
if ($uresult) {
   while ($row = $uresult->fetch(PDO::FETCH_ASSOC)) {
       echo $row['Name'] . PHP_EOL;
   }
}

3.mysqli:
$mysqli  = new mysqli("localhost", "user", "password", "world");
$uresult = $mysqli->query("SELECT Name FROM City", MYSQLI_USE_RESULT);
if ($uresult) {
   while ($row = $uresult->fetch_assoc()) {
       echo $row['Name'] . PHP_EOL;
   }
}
$uresult->close();

3.处理数组时出现内存溢出
(1)使用迭代生成器,可以通过继承Iterator接口实现
(2)使用关键词yield

function xrange($start, $end, $step = 1) {
    for ($i = $start; $i <= $end; $i += $step) {
        yield $i;
    }
}

foreach (xrange(1, 1000000) as $num) {
    echo $num, "\n";
}

相关文章

网友评论

      本文标题:PHP学习笔记-内存溢出解决方案

      本文链接:https://www.haomeiwen.com/subject/tczaoktx.html