我是荔园微风,作为一名在IT界整整25年的老兵,今天我们来重新审视一下Windows程序员如何学习Linux环境内存管理。由于很多程序在Windows环境下开发好后,还要部署到Linux服务器上去,所以作为Windows程序员有必要学习Linux环境的内存管理知识。
说到这里,我有必要更正现在一个普遍的误区,有很多人认为内存管理知识不再重要。但其实,内存管理是计算机编程最为基本的知识领域之一。如今很多的脚本语言中确实不必考虑内存如何管理, 但是这并不能说明内存管理已经不再重要。在实际的编程中,理解自己的内存管理器的能力与局限性至关重要。在C/C++语言编程中仍需进行内存管理,这种内存管理增强并提高了C/C++程序的功能和灵活性。
我们现在先要了解内存的分类。一个程序中使用到的数据都是存放在内存空间中的,那么执行一个程序,考虑其高效性和灵活性等就要合理地分配内存。根据内存空间分配方式的不同,可以将内存分为动态内存和静态内存。下面分别对动态内存和静态内存进行讲解。
动态内存
通常当用户无法确定空间大小,或者空间太大、栈上无法分配时,会采用动态内存方式分配内存。比如你要用C语言写一个图书馆借书管理系统,你是无法确定整个系统要处理多少书的借与还工作,书可能很多,也可能很少,书是1000本,还是10000本,还是100000本?你是不是无法确定总数?那怎么办,一般就要用结构体来实现一本书,用链表来实现书册的管理,对每本书的操作那就只能用动态内存分配来做,把一个结构体拿出来,修改信息后再放回链表,就可以实现图书管理。在使用动态内存时,程序员可以自行控制内存的分配和释放。关于分配多少,何时分配与释放等信息,都由程序员根据需要随时实现。
关于动态内存的使用,很多程序员采取能不使用就不使用的原则,原因在于动态内存资源的敏感性。若能够正确地使用并且利用好动态内存,自然会为程序的实现带来效率;但是一旦用不好,就有可能导致整个项目的崩溃。因此,关于动态内存的使用,不同程度的人应遵循不同的使用原则,目的是为了能够使程序安全、正确并且快速地实现其功能。
事物都是有利有弊的,动态内存也不例外。当动态内存为程序带来了巨大的效率的同时,也为程序带来了巨大的风险。使用动态内存会使内存管理变得很复杂。当程序员根据自己的需要动态地分配完内存,就可以得心应手地使用。但是,当不再使用时,切记释放所占的内存空间。所谓的内存泄露就是将内存分配后没有释放,而导致的内存空间减少的现象。计算机的内存空间是有限的,当分配了过多的内存而没有及时释放时,很有可能导致内存不够用,也就是通常所说的内存耗尽。内存分配与释放是配对的,分配的内存在哪里,使用完毕就要释放哪里的内存。在一个大型的项目中,如若多次分配内存,那么释放这些内存的顺序就成为难题。
好,这里我们遇到一个概念,就是内存泄露。我们就来仔细说说这个问题。顺便我们再讲一下内存溢出。内存泄漏(memory leak)是指程序在申请内存后,无法释放已申请的内存空间,有时一次内存泄漏就会造成一些影响,更不要说多处内存泄漏后所引发的更严重的后果。内存溢出(out of memory)指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错内存溢出。
内存泄漏的积累最终会导致内存溢出。内存溢出就是你要的内存空间超过了系统实际分配给你的空间,此时系统相当于没法满足你的需求,就会报内存溢出的错误。内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。就相当于你租了个带铁围栏的田地,你存完东西之后把铁围栏锁上之后,把钥匙丢了,那么结果就是这片田地将无法供其他人使用,也无法被回收。内存溢出问题,比方说栈,栈满时再做进栈必定产生空间溢出叫上溢,栈空时再做退栈也产生空间溢出称为下溢。就是分配的内存不足以放下数据项序列,称为内存溢出。
内存溢出的原因主要有:内存中加载的数据量过于庞大,如一次从数据库取出过多数据;集合类中有对对象的引用,使用完后未清空;代码中存在死循环或循环产生过多重复的对象实体;使用的第三方软件中的BUG;启动参数内存值设定的过小。内存溢出的解决方案主要有:修改启动参数,直接增加内存。检查错误日志,查看错误前是否有其它异常或错误。对代码进行走查和分析,找出可能发生内存溢出的位置。重点排查以下几点:检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。检查代码中是否有死循环或递归调用。检查是否有大循环重复产生新对象实体。检查List、Map等集合对象是否有使用完后未清除的问题。List、Map等集合对象会始终存有对对象的引用,使得这些对象不能被回收。也可以使用内存查看工具动态查看内存使用情况。
内存泄漏的分类(按发生方式来分类)有如下几种:
常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。
静态内存
所谓静态内存是指在程序开始运行时由编译器分配的内存,它的分配是在程序开始编译时完成的,不占用CPU资源。程序中的各种变量,在编译源程序时系统就已经为其分配了所需的内存空间,当该变量在作用域内使用完毕时,系统会自动释放所占用的内存空间。变量的分配与释放,都无须程序员自行考虑。不必像动态内存那样,要掌握分配内存的大小、何时分配与释放等细节,因此使用静态内存对程序员来说很方便控制。
使用静态内存减少了很多内存资源的风险,如内存泄露、内存耗尽等问题,但减少了风险的同时也带来了弊端。在使用一个数组时,静态内存会预先定义数组的大小,定义数组前并不确定数组中会存放多少数据,若在使用时,存放在数组中的数据大于数组的容量,那么,就会出现溢出问题;然而存放在数组中的数据小于数组的容量很多时,就会造成内存空间的浪费。
静态内存是由编译器来分配的,释放是由变量的作用域所决定的,即当一个变量定义在一个自定义的功能函数中时,如果这个函数结束,该变量也会随之释放。这样,使用指针由子函数向主函数传递数据类的问题就无法实现了。因为子函数中的变量在子函数结束时,就会被释放,所以无法将值带回到主函数。但是事情总会有解决的办法,那就是可以在主函数中定义变量,在子函数中使用主函数中定义的变量传递值。
动态内存与静态内存总结
动态内存与静态内存是两种不同的分配内存的方式,那么下面我们针对它们在分配方式上存在什么样的区别进行总结。
(1)静态内存的分配是在程序开始编译时完成的,不占用CPU资源;而动态内存的分配是在程序运行时完成的,动态内存的分配与释放都是占用CPU资源的。
(2)静态内存是在栈上分配的;而动态内存是在堆上分配的。
(3)动态内存分配需要指针和引用数据类型的支持,而静态内存不需要。
(4)静态内存分配是在编译前就已经确定了内存块的大小,属于按计划分配内存;而动态内存的分配是在程序运行过程中,根据需要随时分配的,属于按需分配。
(5)静态内存的控制权是交给编译器的,而动态内存的控制权是由程序员决定的。
作者简介:荔园微风,1981年生,高级工程师,浙大工学硕士,软件工程项目主管,做过程序员、软件设计师、系统架构师,早期的Windows程序员,Visual Studio忠实用户,C/C++使用者,是一位在计算机界学习、拼搏、奋斗了25年的老将,经历了UNIX时代、桌面WIN32时代、Web应用时代、云计算时代、手机安卓时代、大数据时代、ICT时代、AI深度学习时代、智能机器时代,我不知道未来还会有什么时代,只记得这一路走来,充满着艰辛与收获,愿同大家一起走下去,充满希望的走下去。