首页 > PERL/PHP技术 > [perl] 变量内存占用及引用计数

[perl] 变量内存占用及引用计数

2014年1月10日 发表评论 阅读评论

一、变量内存占用
场景:
读入一个非常大的文本文件,大概180m,处理逻辑很简单,可以忽略
伪代码如下

#!/usr/bin/perl

open(FH,"ip.txt");

#my @a=<FH>;

while (<FH>){
    chomp;
    $a .= $_;
}

1. 当直接while 读取文件句柄时,使用pmap查看程序占用内存在220m左右,可以接受
2. 当把文件句柄读入数组时,使用pmap查看程序占用内存2.1g左右,使用惊人

以上场景即可引出perl的内存分配机制。
perlmem1

从上图知道,perl 的数组或者哈希,保存的不是数据或者字符,而是一个一个的标量变量(scalar)。
让我们来看一看标量所占用的内存大小 使用Devel::Size模块可以得到变量内存大小

#!/usr/bin/perl
use Devel::Size qw(size total_size);

my $a=123;
print "scalar int: ".total_size($a)." bytes\n";

my $b="123";
print "scalar string: ".total_size($b)." bytes\n";

my @a=(123,123,123);
print "scalar int array: ".total_size(\@a)." bytes\n";

my @b=("123","123","123");
print "scalar string array: ".total_size(\@b)." bytes\n";

my %a=(a=>'abc',b=>'abc');
print "scalar string hash: ".total_size(\%a)." bytes\n";

执行结果是
scalar int: 24 bytes
scalar string: 48 bytes
scalar int array: 168 bytes
scalar string array: 240 bytes
scalar string hash: 342 bytes

可以看出 一个标量字符串占48字节,而且放到数组中,又有所增加,
如果我把文本文件的13357363行都读入数组,那么内存使用肯定是惊人的。

所以在读入大文件处理时,最好用

while (<FH>){
    chomp;
    $a .= $_;
}

这种方式,切记再把数据读入数组中,而且要特别注意数组的规模

二、引用计数 (CU上toniz有个帖子,写的很好,直接搬过来)
简单的说,当建立一个变量(a)的时候,该变量(a)的引用计数置1。当其它变量(b)引用变量(a)的时候,引用计数+1.当引用该变量(b)失去对变量(a)的引用时,变量(a)的引用计数-1;当变量(a) 超出自身作用域的时候,变量(a)引用计数减1. perl将自动删除那些引用计数为0的变量的值。
举下面的例子来说明PERL是如何回收再利用的

my @array;
for(0..10){
                my $tmp=123;
                my $addr=\$tmp;
                print "$_ get addr $addr\n";
                $array[$_/2]=$addr;
}
print "result: \n";
print "$_\n" foreach(@array);

0 get addr SCALAR(0x869f72c)
1 get addr SCALAR(0x869eb44)
2 get addr SCALAR(0x869f72c)
3 get addr SCALAR(0x86e1640)
4 get addr SCALAR(0x869f72c)
5 get addr SCALAR(0x86e15f8)
6 get addr SCALAR(0x869f72c)
7 get addr SCALAR(0x86e1544)
8 get addr SCALAR(0x869f72c)
9 get addr SCALAR(0x86e1568)
10 get addr SCALAR(0x869f72c)
result: 
SCALAR(0x869eb44)
SCALAR(0x86e1640)
SCALAR(0x86e15f8)
SCALAR(0x86e1544)
SCALAR(0x86e1568)
SCALAR(0x869f72c)

地址0x869f72c被重用多次。具体工作状态如下:
第一次进入循环,$_为0:
my $tmp=123; 局部变量$tmp建立,对应地址0x869f72c,引用计数被设置为1.
my $addr=\$tmp; $tmp被$addr引用,引用计数+1,成为2.
$array[$_/2]=$addr; $tmp被$array[0]引用,引用计数成为3.
这个时候,第一次循环结束,$tmp和$addr超出作用域。所以对应的地址0x869f72c,引用计数减2。目前0x869f72c引用计数为1.

第二次进入循环,$_为1:
my $tmp=123; 局部变量$tmp建立,对应地址0x869eb44,引用计数被设置为1.
my $addr=\$tmp; $tmp被$addr引用,对应地址0x869eb44,引用计数+1,成为2.
$array[$_/2]=$addr; $tmp被$array[0]引用,对应地址0x869eb44,引用计数成为3.
这个时候,由于$array[0]原来的值(对地址0x869f72c的引用)被覆盖,所以地址0x869f72c的引用计数减1,地址0x869f72c的引用计数为0.PERL自动删除该地址的值。

第三次进入循环,$_为2:
my $tmp=123; 局部变量$tmp建立,对应地址0x869f72c,引用计数被设置为1.
地址0x869f72c被重新分配使用。

看一个内存泄露的例子:

{
my @data1 = qw(one won);
my @data2 = qw(two too to);
push @data2, \@data1;
push @data1, \@data2;
}

会导致memory leak(内存泄露)。因为一直存在对自身的引用,所以该部分内存一直不会被释放.
检查代码里面是否存在自引用,可以使用Devel::Cycle模块。

use Devel::Cycle;
{
my @data1 = qw(one won);
my @data2 = qw(two too to);
push @data2, \@data1;
push @data1, \@data2;
find_cycle(\@data1);  
}

那么,有没有办法直接证明这个写法存在内存泄露呢?
这里引出一个问题:退出了变量的作用域,那么我们如何去证明这个变量是否还存在。
也就是说我们必须去读内存数据,perl是否能够做到读取某一特定地址的值呢?
介绍一个模块Devel::Peek,它可以打印出变量的具体信息。
比如下面这个例子:

    use Devel::Peek;
    my  $mem=11;
    Dump($mem);

打印结果是:
SV = IV(0x9cf77c0) at 0x9cdc6e4
REFCNT = 1
FLAGS = (PADBUSY,PADMY,IOK,pIOK)
IV = 11
如果是字符串的话:
use Devel::Peek;
my $mem=”1234abcd”;
Dump($mem);
打印出来的信息如下:
SV = PV(0x957fb00) at 0x957f6e4
REFCNT = 1
FLAGS = (PADBUSY,PADMY,POK,pPOK)
PV = 0x95954c0 “1234abcd”\0
CUR = 8
LEN = 12
解释上面的标示:
REFCNT就是该变量的引用计数。
FLAGS是。。。
perl有三种主要的数据类型:
SV Scalar Value
AV Array Value
HV Hash Value
这里就举标量变量的例子,因为array和hash到最后也是用scalar保存值的。

Working with SVs
An SV can be created and loaded with one command. 
There are five types of values that can be loaded: an integer value (IV), 
an unsigned integer value (UV), a double (NV), a string (PV), and another scalar (SV).
还要加上,如果是 SV = RV(地址) ,RV是引用。

perl保存数字的时候,会有两个地址,一个是IV(0x9cf77c0)还有一个是 0x9cdc6e4,那么这地址是什么关系呢?
其实,0x9cdc6e4地址的一开始4个字节,保存的就是:c0.77.cf.09。然后0x9cf77c0保存的才是值:11.

那么字符串变量的存储呢?
首先,地址0x957f6e4的前四字节保存的是:00.fb.57.09 ,也就是上面的PV(0x957fb00)
然后,地址0x957fb00的前四字节保存的是:c0.54.59.09 ,也就是0x95954c0这个地址。
最后,地址0x95954c0保存的才是: 31.32.33.34.61.62.63.64 ,也即是字符串内容:1234abcd

而在代码里面使用\$mem得到的地址是第一个地址。如第一个例子是:SCALAR(0x957f6e4),第二个例子是:SCALAR(0x957f6e4)
如果用这个地址来获取最终数值或者字符串的内容,那么将是挺麻烦的一件事情。
可以使用pack来解决这个问题,看下面的代码:
$a=pack( ‘p’, $mem);
printf (“%vx\n”,$a);
打印出来的是:c0.54.59.09 ,也就是字符串保存的最终地址。
那么使用unpack(‘p’,$a )来获取该地址的内容了。
‘p’和’P'的区别可以看下:perldoc perlpacktut

既然已经找到PERL可以直接获取某一地址的内容的方法,那么我们就可以证明上面的代码存在内存泄露。
验证代码如下:
正常的代码:

    my $a;
    {
    my @data1 = qw(one won);
    my @data2 = qw(two too to);
    $a=pack( 'p', $data1[1] );
    }
    print unpack('p',$a )."\n"; 

因为打印的时候,已经在@data1的作用域外,引用计数(referen count)为0,perl自动删除该变量。所以打印出乱码
内存泄露的代码:

    my $a;
    {
    my @data1 = qw(one won);
    my @data2 = qw(two too to);
    $a=pack( 'p', $data1[1] );
    push @data2, \@data1;
    push @data1, \@data2;
    }
    print unpack('p',$a )."\n"; 

可以看到,这里还能打印出won,也就是说数组@data1的内存并没被删除,这里就造成了内存泄露。
纠正方法1:退出作用域时,删除自引用。

    {
    my @data1 = qw(one won);
    my @data2 = qw(two too to);
    push @data2, \@data1;
    push @data1, \@data2;
    @data1=();
    @data1=();
    }

纠正方法2:
使用弱引用,Scalar::Util模块的weaken方法提供该功能,具体代码如下:

    use Scalar::Util qw/weaken/;
    {
    my @data1 = qw(one won);
    my @data2 = qw(two too to);
    push @data2, \@data1;
    push @data1, \@data2;
    weaken($data1[2]);
    weaken($data2[3]);
    }

分类: PERL/PHP技术 标签:
  1. xiehc
    2014年1月10日17:43 | #1

    引用计数单起一篇就好了

  1. 本文目前尚无任何 trackbacks 和 pingbacks.