內(nèi)存問題的可能情況
內(nèi)存泄漏(memory leak)指由于疏忽或錯(cuò)誤造成程序未能釋放已經(jīng)不再使用的內(nèi)存谦炒。內(nèi)存泄漏并非指內(nèi)存在物理上的消失眶根,而是應(yīng)用程序分配某段內(nèi)存后,由于設(shè)計(jì)錯(cuò)誤竭缝,導(dǎo)致在釋放該段內(nèi)存之前就失去了對(duì)該段內(nèi)存的控制希太,從而造成了內(nèi)存的浪費(fèi)。這樣一直的內(nèi)存泄漏下去琅锻,最后會(huì)導(dǎo)致機(jī)器的內(nèi)存不足卦停。在最糟糕的情況下向胡,過多的可用內(nèi)存被分配掉導(dǎo)致全部或部分設(shè)備停止正常工作,或者應(yīng)用程序崩潰惊完。
內(nèi)存溢出(out of memory)是指程序在申請(qǐng)內(nèi)存時(shí)僵芹,沒有足夠的內(nèi)存空間供其使用,出現(xiàn)out of memory小槐;比如申請(qǐng)了一個(gè)integer,但給它存了long才能存下的數(shù)拇派,那就是內(nèi)存溢出。不同的編程語言凿跳,對(duì)內(nèi)存溢出的處理也不一樣件豌。在某些情況下,可能直接導(dǎo)致程序異晨厥龋或者崩潰茧彤。在另外一些情況下,可能是程序無法正常運(yùn)行疆栏,或者性能下降曾掂。
內(nèi)存越界 是指向系統(tǒng)申請(qǐng)一塊內(nèi)存后,使用時(shí)卻超出申請(qǐng)范圍壁顶。比如一些操作內(nèi)存的函數(shù):sprintf珠洗、strcpy、strcat若专、vsprintf许蓖、memcpy、memset调衰、memmove蛔糯。當(dāng)造成內(nèi)存泄漏的代碼運(yùn)行時(shí),所帶來的錯(cuò)誤是無法避免的窖式,通常會(huì)造成
1.破壞了堆中內(nèi)存內(nèi)存分配信息數(shù)據(jù)
2.破壞了程序其他對(duì)象的內(nèi)存空間
3.破壞了空閑內(nèi)存塊
緩沖區(qū)溢出(棧溢出)指程序?yàn)榱伺R時(shí)存取數(shù)據(jù)的需要,一般會(huì)分配一些內(nèi)存空間稱為緩沖區(qū)动壤。如果向緩沖區(qū)中寫入緩沖區(qū)無法容納的數(shù)據(jù)萝喘,機(jī)會(huì)造成緩沖區(qū)以外的存儲(chǔ)單元被改寫,稱為緩沖區(qū)溢出琼懊。而棧溢出是緩沖區(qū)溢出的一種阁簸,原理也是相同的。分為上溢出和下溢出。其中,上溢出是指棧滿而又向其增加新的數(shù)據(jù)粗卜,導(dǎo)致數(shù)據(jù)溢出撤嫩;下溢出是指空棧而又進(jìn)行刪除操作等,導(dǎo)致空間溢出增炭。
內(nèi)存泄漏分類
- 常發(fā)性內(nèi)存泄漏踩蔚。發(fā)生內(nèi)存泄漏的代碼會(huì)被多次執(zhí)行到砚嘴,每次被執(zhí)行的時(shí)候都會(huì)導(dǎo)致一塊內(nèi)存泄漏檬输。
- 偶發(fā)性內(nèi)存泄漏照瘾。發(fā)生內(nèi)存泄漏的代碼只有在某些特定環(huán)境或操作過程下才會(huì)發(fā)生。常發(fā)性和偶發(fā)性是相對(duì)的丧慈。對(duì)于特定的環(huán)境析命,偶發(fā)性的也許就變成了常發(fā)性的。所以測試環(huán)境和測試方法對(duì)檢測內(nèi)存泄漏至關(guān)重要逃默。
- 一次性內(nèi)存泄漏鹃愤。發(fā)生內(nèi)存泄漏的代碼只會(huì)被執(zhí)行一次,或者由于算法上的缺陷完域,導(dǎo)致總會(huì)有一塊僅且一塊內(nèi)存發(fā)生泄漏软吐。比如,在類的構(gòu)造函數(shù)中分配內(nèi)存筒主,在析構(gòu)函數(shù)中卻沒有釋放該內(nèi)存关噪,所以內(nèi)存泄漏只會(huì)發(fā)生一次。
- 隱式內(nèi)存泄漏乌妙。程序在運(yùn)行過程中不停的分配內(nèi)存使兔,但是直到結(jié)束的時(shí)候才釋放內(nèi)存。嚴(yán)格的說這里并沒有發(fā)生內(nèi)存泄漏藤韵,因?yàn)樽罱K程序釋放了所有申請(qǐng)的內(nèi)存虐沥。但是對(duì)于一個(gè)服務(wù)器程序,需要運(yùn)行幾天泽艘,幾周甚至幾個(gè)月欲险,不及時(shí)釋放內(nèi)存也可能導(dǎo)致最終耗盡系統(tǒng)的所有內(nèi)存。所以匹涮,我們稱這類內(nèi)存泄漏為隱式內(nèi)存泄漏天试。
用戶感知
從有計(jì)算機(jī)開始,就有內(nèi)存的的存在然低,使用好內(nèi)存對(duì)成為一個(gè)優(yōu)秀的軟件工程師至關(guān)重要喜每。
但是從用戶使用程序的角度來看,內(nèi)存泄漏本身不會(huì)產(chǎn)生什么危害雳攘,作為一般的用戶带兜,根本感覺不到內(nèi)存泄漏的存在。我們平時(shí)使用的PC或者M(jìn)AC吨灭,或者應(yīng)用程序即使存在內(nèi)存泄漏刚照,基本也不會(huì)有感知,普通用戶根本不會(huì)長時(shí)間去使用一個(gè)應(yīng)用程序喧兄。但是長時(shí)間的使用操作系統(tǒng)是非常有可能的无畔,在以前的Windows系統(tǒng)啊楚,經(jīng)常性的藍(lán)屏,問題多數(shù)就跟內(nèi)存的管理使用有關(guān)檩互,尤其是內(nèi)存泄漏的問題多特幔。
真正有危害的是內(nèi)存泄漏的堆積,這會(huì)最終消耗盡系統(tǒng)所有的內(nèi)存闸昨。從這個(gè)角度來說蚯斯,一次性內(nèi)存泄漏并沒有什么危害,因?yàn)樗粫?huì)堆積饵较,而隱式內(nèi)存泄漏危害性則非常大拍嵌,因?yàn)檩^之于常發(fā)性和偶發(fā)性內(nèi)存泄漏它更難被檢測到。
解決方案與預(yù)防
最簡單的內(nèi)存泄漏
如下的代碼:
#include <stdio.h>
#include <stdlib.h>
void foo() {
char *p = (char*) malloc(10);
p[0] = 'a';
printf("%s \n", p);
}
int main(int argc, char** argv) {
foo();
return 0;
}
這段代碼看著沒什么問題循诉,申請(qǐng)的內(nèi)存沒有free掉横辆,但是程序退出的時(shí)候會(huì)自動(dòng)回收。確實(shí)是這樣子茄猫,但是如果調(diào)用foo()函數(shù)的是線程狈蚤,每一個(gè)線程都這樣只申請(qǐng)內(nèi)存不釋放內(nèi)存,那么每開一次新的線程就會(huì)申請(qǐng)一次內(nèi)存划纽,最后內(nèi)存就會(huì)不斷的攀升脆侮,最后吃完機(jī)器的內(nèi)存。這是非常的明顯的例子勇劣,這樣的錯(cuò)誤一般是不應(yīng)該發(fā)生的靖避,只需要由如下的編碼習(xí)慣:
- malloc和free函數(shù)配對(duì)使用,一個(gè)malloc對(duì)應(yīng)一個(gè)free
- new和delete或delete[]配對(duì)使用
那么申請(qǐng)的內(nèi)存就會(huì)釋放比默,可以避免掉這種簡單錯(cuò)誤幻捏。
C語言函數(shù)庫一些不安全的函數(shù)
如下代碼:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char** argv) {
char* dst = "hello";
const char* src = "Hi world";
char* p = strcpy(dst, src);
printf("%s \n", p);
printf("%s \n", dst);
return 0;
}
這段代碼顯然是有問題的,編譯結(jié)果后命咐,運(yùn)行直接出錯(cuò)篡九,不會(huì)有任何結(jié)果。
C和C++不能夠自動(dòng)地做邊界檢查醋奠,邊界檢查的代價(jià)是效率瓮下,于是常常就出現(xiàn)了越界的行為。一般來講钝域,C 在大多數(shù)情況下注重效率。然而锭魔,獲得效率的代價(jià)是例证,C 程序員必須十分警覺以避免緩沖區(qū)溢出問題。而且這種行為往往是C語言未定義(Undefined Behavior)的行為迷捧,具體怎么處理织咧,還得看編譯器的臉色胀葱。
C語言標(biāo)準(zhǔn)庫中的許多字符串處理和IO流讀取函數(shù)是導(dǎo)致緩沖區(qū)溢出的罪魁禍?zhǔn)住N覀冇斜匾私膺@些函數(shù)笙蒙,在編程中多加小心抵屿。如下列出一些不安全的函數(shù):
strcpy()
strcat()
sprintf()、vsprintf()
gets()
getchar()捅位、fgetc()轧葛、getc()、read()
scanf()艇搀、sscanf()尿扯、fscanf()、vfscanf()焰雕、vscanf()衷笋、vsscanf()
getenv()
以上的這些函數(shù)現(xiàn)在基本都有安全替代的版本,建議大家都使用安全替代版本矩屁。
內(nèi)存泄漏檢測工具
在一般的小型項(xiàng)目到還好辟宗,基本的問題都能在寫代碼的時(shí)候察覺。但是在大規(guī)模項(xiàng)目上吝秕,一個(gè)人要管理編寫的代碼可能上萬行泊脐,還要與同事分工合作,不太可能一行一行的代碼檢查郭膛。所以機(jī)智的前人們?cè)缇烷_發(fā)了一些好用的內(nèi)存檢測工具晨抡,雖然這些內(nèi)存檢測工具不能把所有的問題都檢測出來,但是常見的問題都是沒問題则剃。
valgrind內(nèi)存檢測工具集
Valgrind通常用來成分析程序性能及程序中的內(nèi)存泄露錯(cuò)誤耘柱,他是一套工具集。
1棍现、memcheck:檢查程序中的內(nèi)存問題调煎,如泄漏、越界己肮、非法指針等士袄。
2、callgrind:檢測程序代碼的運(yùn)行時(shí)間和調(diào)用過程谎僻,以及分析程序性能娄柳。
3、cachegrind:分析CPU的cache命中率艘绍、丟失率赤拒,用于進(jìn)行代碼優(yōu)化。
4、helgrind:用于檢查多線程程序的競態(tài)條件挎挖。
5这敬、massif:堆棧分析器,指示程序中使用了多少堆內(nèi)存等信息蕉朵。
6崔涂、lackey:
7、nulgrind:
這幾個(gè)工具的使用是通過命令:valgrind --tool=name 程序名來分別調(diào)用的始衅,當(dāng)不指定tool參數(shù)時(shí)默認(rèn)是 --tool=memcheck冷蚂。更多的詳細(xì)使用請(qǐng)參考手冊(cè)。
對(duì)以上會(huì)出錯(cuò)的代碼做檢測:
valgrind --leak-check=full --show-reachable=yes --trace-children=yes ./main
結(jié)果如下:
==31754== Memcheck, a memory error detector
==31754== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==31754== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==31754== Command: ./main
==31754==
==31754==
==31754== Process terminating with default action of signal 11 (SIGSEGV)
==31754== Bad permissions for mapped region at address 0x108784
==31754== at 0x4C32E00: strcpy (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==31754== by 0x1086C1: main (in /home/xingyaoma/main)
==31754==
==31754== HEAP SUMMARY:
==31754== in use at exit: 0 bytes in 0 blocks
==31754== total heap usage: 0 allocs, 0 frees, 0 bytes allocated
==31754==
==31754== All heap blocks were freed -- no leaks are possible
==31754==
==31754== For counts of detected and suppressed errors, rerun with: -v
==31754== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
Segmentation fault (core dumped)
結(jié)果會(huì)有許多的顯示觅闽,按照結(jié)果的提示帝雇,就可以在代碼中去找一些問題了。
在前面的示例中并沒有把所有可能內(nèi)存的問題都列出來蛉拙,只是其中的一些情況尸闸。在實(shí)際開發(fā)中,配合valgrind工具孕锄,我們可以開發(fā)出穩(wěn)定高效可靠的C/C++代碼吮廉。