免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 49438 | 回复: 52
打印 上一主题 下一主题

PERL深入探讨--内存管理[1]【原创】 [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2010-10-08 16:46 |只看该作者 |倒序浏览
本帖最后由 toniz 于 2010-10-09 17:17 编辑

之前遇到过这样一个问题:
需求:
想要实现这么一个功能,现有一个字符串文件,比如说是有abcdefghijklmn,另外有一个文件是这样的信息:
5       e
7       g
11      k
前面一列是位置(从1开始记),后面是字符,我现在想验证这个文件这样的信息有多少是对的,多少是错的。
具体的做法
把前面的字符串存到数组里,用下标做索引,然后通过这个数组来校验文件二。
主要实现代码入下:
my $a;
while (<>){
    chomp;
    $a .= $_;
}
my @a = split //, $a
当该代码在读取一个全是字符的100M大小的文件存到一个标量里,然后按空分开存到一
个数组里,为什么内存飞涨呢?大概要十几G的内存。
理论上是不需要这么多内存的啊?perl对此到底是如何分配内存的?



Perl数组的存放机制:

从上图知道,perl 的数组或者哈希,保存的不是数据或者字符,而是一个一个的标量变量(scalar)。

下面这句代码:
  1. @a=(1..500_000)
复制代码
由于perl数组机制与c数组储存机制不一样,perl数组是离散的。这样的话会定义500_000个标量变量。占用的内存会远大于C.

所以,如果不了解perl数组的内存机制,编代码的时候,就可能会出现程序提示out of memory错误,但却找不到原因。
一个100M的文件,大概是30多W的字符。如果一个数组来保存每个字符,perl就需要定义30多W的标量变量。这个规模有多大,可想而知。因此,我们写代码的时候,要特别注意数组的规模。避免是使用超大规模的数组,上面的例子可以用substr或者vec来解决问题


接下来再看一个例子:
如果我们想要做一个加法,计算1到5_000_000的总和。是否需要避免这么写:
  1. for(1..5_000_000){$i += $_;}
复制代码
这样写是否合适,会否导致内存被大量使用。

答案是:
This situation has been fixed in Perl5.005. Use of ".." in a "for" loop will iterate over the range,
without creating the entire range.
也就是说Perl5.005之前的版本会有这问题,5.005之后,for语句有了针对该现象的处理机制,所以可以放心在for循环里面放心使用range operate(范围操作符)。而不必去担心内存。
但是如果是在for循环外,如上面的@a=(1..5_000_000)这种语句还是要特别注意,尽量少用为妙。


我们读文件的时候,有时会这样写, 把文件内容都读入内存,以提高文件处理速度:
  1. Open F ,”path.txt ”  or die “open file error : $!”;
  2. @content=<F>;
复制代码
但是,如果文件大小大于内存能够容纳的容量,那么装入内存后,不但不能提高代码运行速度,频繁的内存交换操作,反而会导致代码效率极低。
这个时候,我们应该选择用:
  1. Open F ,”path.txt ”  or die “open file error : $!”;
  2. While(<F>){
  3.   …..
  4. };

复制代码
什么是引用计数(reference  count)?
简单的说,当建立一个变量(a)的时候,该变量(a)的引用计数置1。当其它变量(b)引用变量(a)的时候,引用计数+1.当引用该变量(b)失去对变量(a)的引用时,变量(a)的引用计数-1;当变量(a) 超出自身作用域的时候,变量(a)引用计数减1. perl将自动删除那些引用计数为0的变量的值。

举下面的例子来说明PERL是如何回收再利用的
  1. my @array;
  2. for(0..10){
  3.                 my $tmp=123;
  4.                 my $addr=\$tmp;
  5.                 print "$_ get addr $addr\n";
  6.                 $array[$_/2]=$addr;
  7. }
  8. print "result: \n";
  9. print "$_\n" foreach(@array);
复制代码
打印信息如下:
  1. 0 get addr SCALAR(0x869f72c)
  2. 1 get addr SCALAR(0x869eb44)
  3. 2 get addr SCALAR(0x869f72c)
  4. 3 get addr SCALAR(0x86e1640)
  5. 4 get addr SCALAR(0x869f72c)
  6. 5 get addr SCALAR(0x86e15f8)
  7. 6 get addr SCALAR(0x869f72c)
  8. 7 get addr SCALAR(0x86e1544)
  9. 8 get addr SCALAR(0x869f72c)
  10. 9 get addr SCALAR(0x86e1568)
  11. 10 get addr SCALAR(0x869f72c)
  12. result:
  13. SCALAR(0x869eb44)
  14. SCALAR(0x86e1640)
  15. SCALAR(0x86e15f8)
  16. SCALAR(0x86e1544)
  17. SCALAR(0x86e1568)
  18. 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被重新分配使用。


接着看看下面两个例子:
例1:
  1. my $val ="1234abc";
  2. $r =\$val;
  3. $s ="$r";
  4. print "\$r is $r \n";
  5. print "\$s is $s \n";
  6. print "\$\$r is $$r \n";
  7. print "\$\$s is $$s \n";
  8. 可以打印出1234abc.
  9. $r is SCALAR(0x8b106d8)
  10. $s is SCALAR(0x8b106d8)
  11. $$r is 1234abc
  12. $$s is
复制代码
为什么$r 和$s打印出来的结果一样,而$$r和$$s打印出来的结果却不一样呢?

例2:
  1. {
  2. my @data1 = qw(one won);
  3. my @data2 = qw(two too to);
  4. push @data2, \@data1;
  5. push @data1, \@data2;
  6. }
复制代码
这个例子是否有错误呢:

例1其实是因为$s被字符化了,失去了引用的效果,那么,有没有办法通过字符化的变量$s,来找到$val的值呢?

例2其实会导致memory leak(内存泄露)。因为一直存在对自身的引用,所以该部分内存一直不会被释放
检查代码里面是否存在自引用,可以使用Devel::Cycle模块。
  1. use Devel::Cycle;
  2. {
  3. my @data1 = qw(one won);
  4. my @data2 = qw(two too to);
  5. push @data2, \@data1;
  6. push @data1, \@data2;
  7. find_cycle(\@data1);  
  8. }
复制代码
那么,有没有办法直接证明这个写法存在内存泄露呢?
这里引出一个问题:退出了变量的作用域,那么我们如何去证明这个变量是否还存在。
也就是说我们必须去读内存数据,perl是否能够做到读取某一特定地址的值呢?



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

那么上面的IV(地址这些是代码表什么)
  1. Working with SVs
  2. 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).
  3. 还要加上,如果是 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可以直接获取某一地址的内容的方法,那么我们就可以证明上面的代码存在内存泄露。
验证代码如下:
正常的代码:
  1. my $a;
  2. {
  3. my @data1 = qw(one won);
  4. my @data2 = qw(two too to);
  5. $a=pack( 'p', $data1[1] );
  6. }
  7. print unpack('p',$a )."\n";
复制代码
因为打印的时候,已经在@data1的作用域外,引用计数(referen count)为0,perl自动删除该变量。所以打印出乱码。

内存泄露的代码:
  1. my $a;
  2. {
  3. my @data1 = qw(one won);
  4. my @data2 = qw(two too to);
  5. $a=pack( 'p', $data1[1] );
  6. push @data2, \@data1;
  7. push @data1, \@data2;
  8. }
  9. print unpack('p',$a )."\n";
复制代码
可以看到,这里还能打印出won,也就是说数组@data1的内存并没被删除,这里就造成了内存泄露。



纠正方法1:退出作用域时,删除自引用。
  1. {
  2. my @data1 = qw(one won);
  3. my @data2 = qw(two too to);
  4. push @data2, \@data1;
  5. push @data1, \@data2;
  6. @data1=();
  7. @data1=();
  8. }
复制代码
纠正方法2:
使用弱引用,Scalar::Util模块的weaken方法提供该功能,具体代码如下:
  1. use Scalar::Util qw/weaken/;
  2. {
  3. my @data1 = qw(one won);
  4. my @data2 = qw(two too to);
  5. push @data2, \@data1;
  6. push @data1, \@data2;
  7. weaken($data1[2]);
  8. weaken($data2[3]);
  9. }
复制代码
----------------------分隔线-------------------------------


flw老大指点了一下,单单从一个变量地址的内容是否被改变去判断变量内存是否被释放,是没有依据的。
所以需要增加获取变量的引用计数来证明该内存泄露现象。
根据这个提示,我重新整了个代码:

use Devel::Peek;
my $d;
{
  my @data1 = qw(one won);
  my @data2 = qw(two too to);
  push @data2, \@data1;        #注释掉可以证明是否是内存泄露
  push @data1, \@data2;       #注释掉可以证明是否是内存泄露
  my $er= \$data1[1];            #帮助判断哪个字节的内容是引用计数。证明是地址后面紧跟的一个字段。如果注释掉那个字节的数值将减1。
  my $ereee= \$data1[1];      #帮助判断哪个字节的内容是引用计数。证明是地址后面紧跟的一个字段。如果注释掉那个字节的数值将减1。               
  my $cc=\$data1[1];             #用来获得第一个地址,实际就是 SV = PV(0x8d36c14) at 0x8d35dd8这个里面0x8d35dd8的地址。
  $cc=~s/[SCALAR\(\)x]//g;    #地址字符化,并去掉不相关信息。
  print "$cc 1\n";                                               
  $cc=~s/(.{2})(.{2})(.{2})(.{2})/$4$3$2$1/; #因为我的操作系统是little-ending的,所以做个转换。
  print "$cc 2\n";
  $d= pack('H*',$cc);
  $f2= unpack('P100',$d );      #获取0x8d35dd8该地址后面连续100个字节的内容。
  print unpack('H*',$f2)."\n"; #打印成16进制,第三行结果里面146cd30803....中的03就是引用计数.
  Dump(\$data1[1]);                      #Dump这个变量内容以便对比
}
print $data1[1];                     #证明$data1[1]这个变量在代码中已经不能使用。       
$f2= unpack('P100',$d );       #但打印出0x8d35dd8这个地址内容的时候,还能够看到这个变量的引用计数还是1.没有被删除,导致内存泄露。
print unpack('H*',$f2)."\n";

$ff= unpack('P4',$d );           #追踪到保存字符串的地址,并打印出结果,以证明这里确实是内存泄露了。
$fff= unpack('P4',$ff );
$ffff= unpack('P4',$fff );
print unpack('H*',$ff)."\n";       
print unpack('H*',$fff)."\n";       
print unpack('H*',$ffff)."\n";       
print $ffff."\n";


运行结果是:
08d35dd8 1
d85dd308 2
146cd3080300000004000404e8c7d408010000000c00000064acd308010000000a00000090acd308010000000a000000bcacd308010000000a000000d8d7d408010000000d600000000000000100000000000000409dd308030000000b000020c8d8d4080a
SV = RV(0x8d5f654) at 0x8d77ba0
  REFCNT = 1
  FLAGS = (TEMP,ROK)
  RV = 0x8d35dd8
  SV = PV(0x8d36c14) at 0x8d35dd8
    REFCNT = 4
    FLAGS = (POK,pPOK)
    PV = 0x8d494d8 "won"\0
    CUR = 3
    LEN = 4
146cd3080100000004000404e8c7d408010000000c00000064acd308010000000a00000090acd308010000000a000000bcacd308010000000a000000d8d7d408010000000d600000000000000100000000000000409dd308030000000b000020c8d8d4080a

论坛徽章:
78
双子座
日期:2013-10-15 08:50:09天秤座
日期:2013-10-16 18:02:08白羊座
日期:2013-10-18 13:35:33天蝎座
日期:2013-10-18 13:37:06狮子座
日期:2013-10-18 13:40:31双子座
日期:2013-10-22 13:58:42戌狗
日期:2013-10-22 18:50:04CU十二周年纪念徽章
日期:2013-10-24 15:41:34巨蟹座
日期:2013-10-24 17:14:56处女座
日期:2013-10-24 17:15:30双子座
日期:2013-10-25 13:49:39午马
日期:2013-10-28 15:02:15
2 [报告]
发表于 2010-10-08 18:10 |只看该作者


支持

论坛徽章:
0
3 [报告]
发表于 2010-10-08 21:52 |只看该作者

论坛徽章:
46
15-16赛季CBA联赛之四川
日期:2018-03-27 11:59:132015年亚洲杯之沙特阿拉伯
日期:2015-04-11 17:31:45天蝎座
日期:2015-03-25 16:56:49双鱼座
日期:2015-03-25 16:56:30摩羯座
日期:2015-03-25 16:56:09巳蛇
日期:2015-03-25 16:55:30卯兔
日期:2015-03-25 16:54:29子鼠
日期:2015-03-25 16:53:59申猴
日期:2015-03-25 16:53:29寅虎
日期:2015-03-25 16:52:29羊年新春福章
日期:2015-03-25 16:51:212015亚冠之布里斯班狮吼
日期:2015-07-13 10:44:56
4 [报告]
发表于 2010-10-08 22:13 |只看该作者
学习了 pack 'p' 的强大用法

论坛徽章:
0
5 [报告]
发表于 2010-10-08 22:47 |只看该作者
本帖最后由 珞水的大叔 于 2010-10-08 22:49 编辑

pack和unpack一直都很少用
不看这篇文章都不知道它们的用途这么有趣
very good
toniz辛苦啦

论坛徽章:
0
6 [报告]
发表于 2010-10-09 09:08 |只看该作者

论坛徽章:
0
7 [报告]
发表于 2010-10-09 10:28 |只看该作者
{:3_190:}豁然开朗

论坛徽章:
1
2015年辞旧岁徽章
日期:2015-03-03 16:54:15
8 [报告]
发表于 2010-10-09 10:46 |只看该作者
用乱码不乱码的办法来验证是不是已经把内存释放了,
这是错误的。

论坛徽章:
0
9 [报告]
发表于 2010-10-09 12:20 |只看该作者
FLW老大给说下要如何整?

论坛徽章:
0
10 [报告]
发表于 2010-10-09 12:25 |只看该作者
学习了{:3_198:}
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP