技术头条 - 一个快速在微博传播文章的方式     搜索本站
您现在的位置首页 --> 系统运维 --> PERL内存管理

PERL内存管理

浏览:3228次  出处信息

需求:
想要实现这么一个功能,现有一个字符串文件,比如说是有abcdefghijklmn,另外有一个文件是这样的信息:
5 e
7 g
11 k
前面一列是位置(从1开始记),后面是字符,我现在想验证这个文件这样的信息有多少是对的,多少是错的。
具体的做法:
把前面的字符串存到数组里,用下标做索引,然后通过这个数组来校验文件二。
主要实现代码入下:

my $a;
while (<>){
    chomp;
    $a .= $_;
}
my @a = split //, $a

当该代码在读取一个全是字符的100M大小的文件存到一个标量里,然后按空分开存到一
个数组里,为什么内存飞涨呢?大概要十几G的内存。
理论上是不需要这么多内存的啊?perl对此到底是如何分配内存的?

Perl数组的存放机制:

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

下面这句代码:

@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的总和。是否需要避免这么写:

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)这种语句还是要特别注意,尽量少用为妙。

我们读文件的时候,有时会这样写, 把文件内容都读入内存,以提高文件处理速度:

Open F ,”path.txt ”  or die “open file error : $!”;
@content=;

但是,如果文件大小大于内存能够容纳的容量,那么装入内存后,不但不能提高代码运行速度,频繁的内存交换操作,反而会导致代码效率极低。
这个时候,我们应该选择用:

Open F ,”path.txt ”  or die “open file error : $!”; 

While(){
  …..
};

什么是引用计数(reference count)?
简单的说,当建立一个变量(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)

地址0×869f72c被重用多次。具体工作状态如下:
第一次进入循环,$_为0:
my $tmp=123; 局部变量$tmp建立,对应地址0×869f72c,引用计数被设置为1.
my $addr=\$tmp; $tmp被$addr引用,引用计数+1,成为2.
$array[$_/2]=$addr; $tmp被$array[0]引用,引用计数成为3.
这个时候,第一次循环结束,$tmp和$addr超出作用域。所以对应的地址0×869f72c,引用计数减2。目前0×869f72c引用计数为1.
第二次进入循环,$_为1:
my $tmp=123; 局部变量$tmp建立,对应地址0×869eb44,引用计数被设置为1.
my $addr=\$tmp; $tmp被$addr引用,对应地址0×869eb44,引用计数+1,成为2.
$array[$_/2]=$addr; $tmp被$array[0]引用,对应地址0×869eb44,引用计数成为3.
这个时候,由于$array[0]原来的值(对地址0×869f72c的引用)被覆盖,所以地址0×869f72c的引用计数减1,地址0×869f72c的引用计数为0.PERL自动删除该地址的值。
第三次进入循环,$_为2:
my $tmp=123; 局部变量$tmp建立,对应地址0×869f72c,引用计数被设置为1.
地址0×869f72c被重新分配使用。

接着看看下面两个例子:
例1:

my $val ="1234abc";
$r =\$val;
$s ="$r";
print "\$r is $r \n";
print "\$s is $s \n";
print "\$\$r is $$r \n";
print "\$\$s is $$s \n";
可以打印出1234abc.
$r is SCALAR(0x8b106d8)
$s is SCALAR(0x8b106d8)
$$r is 1234abc
$$s is

为什么$r 和$s打印出来的结果一样,而$$r和$$s打印出来的结果却不一样呢?

例2:

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

这个例子是否有错误呢:

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

例2其实会导致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保存值的。

那么上面的IV(地址这些是代码表什么)

以下是代码片段:
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(0×9cf77c0)还有一个是 0×9cdc6e4,那么这地址是什么关系呢?
其实,0×9cdc6e4地址的一开始4个字节,保存的就是:c0.77.cf.09。然后0×9cf77c0保存的才是值:11.

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

而在代码里面使用\$mem得到的地址是第一个地址。如第一个例子是:SCALAR(0×957f6e4),第二个例子是:SCALAR(0×957f6e4)
如果用这个地址来获取最终数值或者字符串的内容,那么将是挺麻烦的一件事情。
可以使用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]);
}

―――――――-分隔线――――――――――-

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";

运行结果是:

以下是代码片段:
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

$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”;

建议继续学习:

  1. Linux内存点滴 用户进程内存空间    (阅读:11054)
  2. ps - 按进程消耗内存多少排序    (阅读:11012)
  3. Linux Used内存到底哪里去了?    (阅读:9751)
  4. Linux操作系统的内存使用方法详细解析    (阅读:8721)
  5. linux内核研究笔记(一)内存管理 – page介绍    (阅读:8036)
  6. 几个内存相关面试题(c/c++)    (阅读:7826)
  7. 内存越界的概念和调试方法    (阅读:6144)
  8. 必看!linux系统如何查看内存使用情况    (阅读:6021)
  9. Innodb分表太多或者表分区太多,会导致内存耗尽而宕机    (阅读:6013)
  10. 如何查看Linux 硬件配置信息    (阅读:5718)
QQ技术交流群:445447336,欢迎加入!
扫一扫订阅我的微信号:IT技术博客大学习
© 2009 - 2024 by blogread.cn 微博:@IT技术博客大学习

京ICP备15002552号-1