欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

编程时与内存相关问题的总结(内存碎片、内存泄漏等)

程序员文章站 2022-06-04 11:01:56
...

1、程序所占用系统内存分为哪几个部分

代码段:二进制的指令

只读段:字符串字面值、常量

全局数据段:初始化的静态变量、全局变量

静态数据段bss段:末初始化的静态变量、全局变量,在程序执行时会被清理为0。堆:由程序员管理 足够大(理论上可以是物理内存的极限),数据的释放受控制,可能会产生内存泄漏和内存碎片。

栈:由操作系统管理 大小有限(栈崩溃),数据的释放是不受控制的,栈内存几乎不会产生什么管理上的错误。

命令行、环境变量表:程序运行附加的数据

2、什么是内存虚拟内存

1、每个进程都会有独立虚拟内存空间(4G)。

2、进程永远只能使用虚拟内存、无法直接访问物理内存

3、虚拟内存不能直接使用,必须要与物理内存建立映射关系才能使用,否会产生段错误。

4、虚拟内存与物理内存的映射由操作系统动态维护。

5、使用虚拟内存<映射>物理内存的好处:

1、保障了操作系统的安全

2、可以使用超过4G

3、大于实际物理的内存(使用硬盘文件来模拟内存)。

6、0G~3G的虚拟地址属于用户,3G~4G的虚拟地址属于内核。

7、用户态的程序不能直接访问内核中的数据,必须经过系统调用进入内核态,当系统调用执行完毕后,再把数据分享给用户。

8、进程之间的内存地址是相互独立的(进程之间如果要协同工作必须要解决通信的问题)。

3、什么是内存映射

malloc:首次使用malloc申请内存时,malloc会向操作系统请求建映射关系,操作系统会帮malloc映射33页的内存,交给malloc管理,如果33页内存分配使用完了,操作系统会再分配33页。

只要映射过的内存就不会产生段错误,但是可能会破坏掉malloc的维护信息行造成接下来的申请或释放错误,也有可能产生脏数据。

清理内存:bzero strings.h/memset string.h

calloc 以nmemb*size的方式申请内存,而且会把内存清理为0。

realloc 增/减已有的内存,如果第一个参数为NULL也可以用来申请内存。

free 释放内存,记得指针置为NULL

alloc 分配当前函数的栈内存,当函数结束后会自动释放,只有部分操作系统支持。

一页内存=默认是4096byte getpagesize可以获取一页内存的大小。

brk/sbrk

POXIX标准定义的系统内存管理函数,它们共同管理一个内存末尾指针,brk和sbrk都具有内存的申请和释放功能,但他们两个配合使用比较方便。

mmap/munmap

Linux系统实现的内存管理函数,brk/sbrk就调用经它们,它不光可以对虚拟地址与物理内存进行映射,还可以对虚拟地址和文件进行映射(磁盘文件虚拟成内存使用)。

注意:brk/sbrk、mmap/munmap都是以页为单位。

4、什么是内存泄漏

内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果

在内存*用户使用的内存空间分为三部分:程序存储区静态存储区动态存储区。

一般情况下,开发人员使用系统提供的内存管理基本函数,如malloc、recalloc、calloc、free等,完成动态存储变量存储空间的分配和释放。但是,当开发程序中使用动态存储变量较多和频繁使用函数调用时,就会经常发生内存管理错误,例如:

1、分配一个内存块并使用其中未经初始化的内容

2、释放一个内存块,但继续引用其中的内容;

3、子函数中分配的内存空间在主函数出现异常中断时、或主函数对子函数返回的信息使用结束时,没有对分配的内存进行释放;

4、程序实现过程中分配的临时内存在程序结束时,没有释放临时内存。内存错误一般是不可再现的,开发人员不易在程序调试和测试阶段发现,即使花费了很多精力和时间,也无法彻底消除。

 

产生内存泄漏方式的分类:

以产生的方式来分类,内存泄漏可以分为四类:

  1. 常发性内存泄漏:发生内存泄漏的代码会被多次执行到,每次被执行时都会导致一块内存泄漏。
  2. 偶发性内存泄漏:发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
  3. 一次性内存泄漏:发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块且仅有一块内存发生泄漏。

4、隐式内存泄漏:程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终耗尽系统所有的内存。

5、什么是内存碎片,如何减少内存碎片

  内存碎片一般是由于空闲的连续空间比要申请的空间小,导致这些小内存块不能被利用。产生内存碎片的方法很简单,举个例: 
    假设有一块一共有100个单位的连续空闲内存空间,范围是0~99。如果你从中申请一块内存,如10个单位,那么申请出来的内存块就为0~9区间。这时候你继续申请一块内存,比如说5个单位大,第二块得到的内存块就应该为10~14区间。 
    如果你把第一块内存块释放,然后再申请一块大于10个单位的内存块,比如说20个单位。因为刚被释放的内存块不能满足新的请求,所以只能从15开始分配出20个单位的内存块。 
    现在整个内存空间的状态是0~9空闲,10~14被占用,15~24被占用,25~99空闲。其中0~9就是一个内存碎片了。如果10~14一直被占用,而以后申请的空间都大于10个单位,那么0~9就永远用不上了,造成内存浪费。 
    如果你每次申请内存的大小,都比前一次释放的内村大小要小,那么申请就总能成功

减少内存碎片:

大体思路自己编写内存管理模块,程序一开始就申请一大块内存(内存池),然后以后申请内存都在这个大内存中取,配合一定的技巧来减少内存碎片问题。在C++中我们可以重写operator new/operator delete来减少内存碎片出现的机会,

一、重写new和delete


    //FreeListBase.h  

    #define NULL 0  

    class FreeListBase  

    {  

    public:  

        FreeListBase(void){}  

        virtual ~FreeListBase(void){}  

    public:  

        void* operator new(size_t size);  

        void operator delete(void* p,size_t size);  

    private:  

        static FreeListBase* freelist;  

        FreeListBase* next;  

    };  

    //FreeListBase.cpp  

    void* FreeListBase::operator new(size_t size)  

    {  

        if(freelist != NULL)  

        {  

            FreeListBase* p = freelist;  

            freelist = freelist->next;  

            return p;  

        }  

        else  

            return ::operator new(size);  

    }  

    void FreeListBase::operator delete(void* vp,size_t size)  

    {  

        FreeListBase* p = static_cast<FreeListBase*>(vp);  

        p->next = freelist;  

        freelist = p;  

    }  

    //TemperatureUsingFreeList.h  

    #include "freelistbase.h"  

    class TemperatureUsingFreeList :  

        public FreeListBase  

    {  

    public:  

        TemperatureUsingFreeList(void){}  

        ~TemperatureUsingFreeList(void){}  

        inline int average() { return (maxTemp + minTemp)/2; }  

    private:  

        int ID;  

        int maxTemp;  

        int minTemp;  

        int currentTemp;  

    };   

二、其他做法,大体思路是一样的,都是先开辟一块大的内存,再进行内存管理。


    //BigChunkStack.H  

    #define NULL 0  

    #define INITSIZE 30  

    class BigChunkStack  

    {  

        struct elem  

        {  

            int id;                   

            int previousElemSize;    //上一个元素的大小  

            int namesize;            //name的大小  

            char* name;  

        };  

    public:  

        BigChunkStack(void);  

        ~BigChunkStack(void);  

    public:  

        void push(const char* s,const int nr);  

        void pop(char* s,int& nr);  

        int grow();  

        int shrink();  

    private:  

        int totalSize;  

        int emptyElemSize;  

        int lastElemSize;     //最后一个加入的元素的大小  

        char* pool;           //内存池指针  

        int MAXSIZE;          //内存池的大小  

    };  

    //BigChunkStack.cpp  

    BigChunkStack::BigChunkStack(void)  

    {  

        totalSize = 0;  

        emptyElemSize = sizeof(elem);  

        lastElemSize = 0;  

        pool = NULL;  

        MAXSIZE = 0;  

    }  

    BigChunkStack::~BigChunkStack(void)  

    {  

    }  

    void BigChunkStack::push(const char* s,const int nr)  

    {  

        assert(s != NULL);  

        int newStringSize = strlen(s) + 1;  

        int newElemSize = newStringSize + emptyElemSize;  

        if((totalSize + newElemSize) > MAXSIZE)  

        {  

            if(!grow())  

            {  

                cerr<<"Error,Stack Overflow!"<<endl;  

                return ;  

            }  

        }  

        elem* newElem = (elem*)(pool+totalSize);  

        newElem->name = (char*)(pool+totalSize+emptyElemSize);  

        newElem->id = nr;  

        newElem->namesize = newStringSize;  

        newElem->previousElemSize = lastElemSize;  

        strcpy(newElem->name,s);  

        lastElemSize = newElemSize;  

        totalSize += newElemSize;  

    }  

    void BigChunkStack::pop(char* s,int& nr)  

    {  

        if(totalSize*4 <= MAXSIZE)  

            shrink();  

        if(totalSize != 0)  

        {  

            totalSize -= lastElemSize;  

            elem* popElem = (elem*)(pool+totalSize);  

            lastElemSize = popElem->previousElemSize;  

            strcpy(s,popElem->name);  

            nr = popElem->id;  

        }  

        else  

        {  

            cerr<<"Error,Stack Underflow!!"<<endl;  

        }  

    }  

    int BigChunkStack::grow()  

    {  

        if(MAXSIZE == 0)  

        {  

            pool = new char[INITSIZE];  

            if(pool == NULL)  

                return 0;  

            MAXSIZE = INITSIZE;  

            return 1;  

        }  

        else  

        {  

            MAXSIZE *= 2;  

            char* tempPool = (char*)realloc(pool,MAXSIZE);  

            if(tempPool == NULL)  

                return 0;  

            pool = tempPool;  

            return 1;  

        }  

    }  

    int BigChunkStack::shrink()  

    {  

        if(MAXSIZE/2 >= INITSIZE)  

        {  

            char* tempPool = (char*)realloc(pool,MAXSIZE/2);  

            if(tempPool == NULL) return 0;  

            pool = tempPool;  

            MAXSIZE /= 2;  

            return 1;  

        }  

    }  

6、栈空间的最大值是多少

栈空间:自动内存空间,其中数据的大小在编译时确定,数据的分配和释放也由编译器在函数进入和退出时插入指令完成,数据生命周期和函数一样。

堆空间:动态(手动)内存空间,其中数据的大小和初始值在运行时确定,数据生命周期不定。

Linux默认的用户栈空间的大小是8MBwindows下,栈的大小是2MB,而申请堆空间的大小一般小于2GB。对于堆空间来说,默认是没有最大值的。

7、什么是缓冲区溢出

缓冲区溢出是指当计算机向缓冲区内填充数据位数时超过了缓冲区本身的容量溢的数

据覆盖在合法数据上,

 危害:

缓冲区溢出中, 最为危险的是堆栈溢出,因为入侵者可以利用堆栈溢出,在函数返回时改变返回程序的地址,让其跳转到任意地址,带来的危害一种是程序崩溃导致拒绝服务,另外一种就是跳转并且执行一段恶意代码, 比如得到 shell,然后为所欲为。通过往程序的缓冲区写超出其长度的内容,造成缓冲区的溢出,从而破坏程序的堆栈,使程序转而执行其它指令,以达到攻击的目的。 造成缓冲区溢出的原因是程序中没有仔细检查用户输入的参数 .

 

 

相关标签: 内存管理