摘要:當(dāng)出現(xiàn)這種運(yùn)行一段時(shí)間后的異常閃退,很有可能是以下三種原因?qū)е碌摹3绦蛟谶\(yùn)行過程中發(fā)生異?;蛘唛W退,可能就是有線程發(fā)生棧溢出導(dǎo)致的。
目錄
? ? ? ?Windows應(yīng)用軟件在交付給客戶使用或者試用后,可能會(huì)因?yàn)椴僮飨到y(tǒng)版本及硬件上的差異,出現(xiàn)這樣那樣的軟件異常問題。特別是項(xiàng)目即將交互等待客戶驗(yàn)收時(shí),出現(xiàn)多種莫名其妙的異常問題,是比較棘手的,在有限的時(shí)間內(nèi)去搞定這些異常問題的壓力也是比較大的。下面以以往遇到的多個(gè)項(xiàng)目問題為例,簡(jiǎn)單地說一下三種比較典型的軟件異常問題,希望能給大家提供一定的借鑒或參考。
? ? ? ?Windows應(yīng)用軟件在發(fā)布之前,在公司內(nèi)部進(jìn)行了詳細(xì)的測(cè)試,基本已經(jīng)達(dá)到穩(wěn)定狀態(tài),但公司內(nèi)部進(jìn)行的測(cè)試是有限的,有限的人力、有限的機(jī)器環(huán)境,始終是無(wú)法覆蓋所有的問題場(chǎng)景的。當(dāng)軟件發(fā)布到各式各樣的客戶手中,可能會(huì)因?yàn)?span style="color:#be191c;">操作系統(tǒng)及硬件上的差異,出現(xiàn)這樣那樣的異常問題。
? ? ? ?從操作系統(tǒng)上看,Windows操作系統(tǒng)就有多個(gè)大版本,比如XP、Win7、Win8、Win10,甚至Win11就要出來(lái)。除了大版本之外,每個(gè)同系列上還有各種子版本,在系統(tǒng)特性上都有著或大或小的差異。作為Windows軟件都要兼容這些常用的、不同版本的操作系統(tǒng),這給軟件的平穩(wěn)運(yùn)行帶來(lái)了挑戰(zhàn)。
? ? ? ?除了操作系統(tǒng)之外,還有各式各樣的硬件,對(duì)應(yīng)著各自的硬件驅(qū)動(dòng)程序,這給軟件的良好運(yùn)行帶來(lái)了更大的挑戰(zhàn)。所以,當(dāng)軟件拿到多個(gè)客戶的機(jī)器上運(yùn)行,出現(xiàn)這樣那樣的問題是在所難免的,作為軟件的提供方,我們只能盡力將出問題的概率降到最低,在出現(xiàn)問題后要第一時(shí)間去響應(yīng)、去解決。
? ? ? ?和客戶側(cè)的Windows終端應(yīng)用軟件相比,大多數(shù)服務(wù)器側(cè)的軟件則要幸運(yùn)的多,它們一般不用去面對(duì)各式各樣的軟硬件環(huán)境。因?yàn)榉?wù)器側(cè)的操作系統(tǒng)和硬件設(shè)備都是產(chǎn)品提供商定制好了,使用固定的硬件,使用固定版本的Linux操作系統(tǒng)(當(dāng)然也有服務(wù)器使用Windows Server等不同操作系統(tǒng)的),服務(wù)器側(cè)產(chǎn)品在發(fā)布之前已經(jīng)能保證在這些固定的軟件環(huán)境中持續(xù)穩(wěn)定的運(yùn)行。
? ? ? ? 回到本文的主題,本文研究的對(duì)象是客戶終端側(cè)的Windows C++軟件,下面就來(lái)切入本文的正題。今天我們要講的這幾類異常有個(gè)共同的特點(diǎn)就是,軟件剛啟動(dòng)時(shí)運(yùn)行還算平穩(wěn),CPU和內(nèi)存占用都比較正常,但運(yùn)行一段時(shí)間后或較長(zhǎng)一段時(shí)間后可能會(huì)莫名其妙地異常閃退。當(dāng)出現(xiàn)這種運(yùn)行一段時(shí)間后的異常閃退,很有可能是以下三種原因?qū)е碌?。一是發(fā)生了GDI對(duì)象泄露,二是發(fā)生了線程棧溢出,三是發(fā)生了內(nèi)存泄露。這三種異?;旧隙伎赡苁沁\(yùn)行一段時(shí)間才會(huì)出現(xiàn)的,甚至有時(shí)是很難復(fù)現(xiàn)的,因?yàn)檫@些異常可能是某些操作才會(huì)觸發(fā)的,如果用戶沒有執(zhí)行這些操作,可能就不會(huì)就不會(huì)爆出這些問題了。
? ? ? ?程序運(yùn)行一段時(shí)間后,當(dāng)GDI對(duì)象達(dá)到10000個(gè)左右,導(dǎo)致程序崩潰閃退。
Windows系統(tǒng)中,進(jìn)程中的GDI對(duì)象總數(shù)不能超過10000個(gè)。當(dāng)進(jìn)程的GDI對(duì)象總數(shù)接近或超過10000個(gè)時(shí)就會(huì)導(dǎo)致GDI繪圖出現(xiàn)異常,API函數(shù)調(diào)用返回失敗,甚至出現(xiàn)閃退崩潰。
? ? ? ?如果代碼中有Pen、Brush、Bitmap、Font、Region或DC等GDI對(duì)象泄露時(shí),且這段代碼會(huì)頻繁的執(zhí)行,可能指定某一操作后才會(huì)頻繁的觸發(fā)泄露代碼的執(zhí)行。在程序運(yùn)行的過程中GDI對(duì)象會(huì)快速的增長(zhǎng),當(dāng)GDI達(dá)到10000個(gè)左右時(shí),會(huì)出現(xiàn)各種GDI函數(shù)繪圖失敗的問題,可以通過GetLastError獲取繪制失敗的原因。
? ? ? 緊接著程序可能就會(huì)出現(xiàn)異常崩潰閃退了。多久能達(dá)到10000萬(wàn)個(gè)上限,和泄露的程度有關(guān)系,也和程序運(yùn)行的時(shí)間長(zhǎng)短有關(guān)。有時(shí)可能半個(gè)小時(shí)或幾個(gè)小時(shí)會(huì)出現(xiàn),有時(shí)需要長(zhǎng)時(shí)間拷機(jī)才會(huì)出現(xiàn)。至于拷機(jī),有多種形式,比如下班后的夜間拷機(jī),周末休息期間的長(zhǎng)時(shí)間拷機(jī)運(yùn)行。
? ? ? ?GDI對(duì)象泄露問題該如何感知并排查呢?可以先查看系統(tǒng)的任務(wù)管理器,持續(xù)觀察目標(biāo)進(jìn)程的GDI總數(shù)的變化,如果GDI對(duì)象有明顯增長(zhǎng)就說明可能存在GDI對(duì)象泄露了。然后再打開GDIView工具,看看具體各類型的GDI對(duì)象的數(shù)目:
找出數(shù)值異常的那一種GDI對(duì)象。知道發(fā)生泄露的GDI對(duì)象類型后,就可以結(jié)合代碼進(jìn)行逐步排查了。
? ? ? ?這種觀察方式需要人工進(jìn)行,對(duì)于無(wú)人值守的情況該怎么處理呢?比如最簡(jiǎn)單就是使用按鍵精靈等自動(dòng)化測(cè)試工具去拷機(jī),去執(zhí)行某一些操作,看看長(zhǎng)時(shí)間運(yùn)行是否會(huì)有問題,如果是監(jiān)測(cè)GDI對(duì)象是否有泄露,可以每隔若干時(shí)間就讓按鍵精靈調(diào)用截圖程序截一張桌面的圖片,并保存到指定的目錄中,第二天來(lái)上班后可以看這些圖片去判斷。
? ? ? ?比如如下的代碼,在函數(shù)結(jié)尾的時(shí)候不去刪除之前創(chuàng)建的GDI對(duì)象,就會(huì)導(dǎo)致GDI對(duì)象泄露:
// 拷貝桌面,lpRect 代表選定區(qū)域,bSave 標(biāo)記是否將圖片內(nèi)容保存到剪切板中HBITMAP CCatchScreenDlg::CopyScreenToBitmap( LPRECT lpRect ) { // 確保選定區(qū)域不為空矩形 if ( IsRectEmpty( lpRect ) ) { return NULL; } CUIString strLog; HWND hWndDeskTop = ::GetDesktopWindow(); //HDC hScrDC = ::CreateDC( _T("DISPLAY"), NULL, NULL, NULL ); HDC hScrDC = ::GetDC( hWndDeskTop ); // 為屏幕創(chuàng)建設(shè)備描述表 if ( hScrDC == NULL ) { strLog.Format( _T("[CCatchScreenDlg::CopyScreenToBitmap] 創(chuàng)建DISPLAY失敗, GetLastError: %d"), GetLastError() ); WriteScreenCatchLog( strLog ); return NULL; } HDC hMemDC = ::CreateCompatibleDC( hScrDC ); // 為屏幕設(shè)備描述表創(chuàng)建兼容的內(nèi)存設(shè)備描述表 if ( hMemDC == NULL ) { strLog.Format( _T("[CCatchScreenDlg::CopyScreenToBitmap]創(chuàng)建與hScrDC兼容的hMemDC失敗, GetLastError: %d"), GetLastError() ); WriteScreenCatchLog( strLog ); //::DeleteDC( hScrDC ); ::ReleaseDC( hWndDeskTop, hScrDC ); return NULL; } int nX = 0; int nY = 0; int nX2 = 0; int nY2 = 0; int nWidth = 0; int nHeight = 0; // 保證left小于right,top小于bottom CDirectRect rc = *lpRect; rc.Normalize(); // 獲得選定區(qū)域坐標(biāo) nX = rc.left; nY = rc.top; nX2 = rc.right; nY2 = rc.bottom; // 確保選定區(qū)域是可見的 if ( nX < 0 ) { nX = 0; } if ( nY < 0 ) { nY = 0; } if ( nX2 > m_xScreen ) { nX2 = m_xScreen; } if ( nY2 > m_yScreen ) { nY2 = m_yScreen; } nWidth = nX2 - nX; nHeight = nY2 - nY; //HBITMAP hBitmap = ::CreateCompatibleBitmap( hScrDC, nWidth, nHeight ); // 創(chuàng)建一個(gè)與屏幕設(shè)備描述表兼容的位圖 HBITMAP hBitmap = CreateDIBBitmap( nWidth, nHeight ); if ( hBitmap == NULL ) { strLog.Format( _T("[CCatchScreenDlg::CopyScreenToBitmap]創(chuàng)建與hScrDC兼容的Bitmap失敗, GetLastError: %d"), GetLastError() ); WriteScreenCatchLog( strLog ); //::DeleteDC( hScrDC ); ::ReleaseDC( hWndDeskTop, hScrDC ); ::DeleteDC( hMemDC ); return NULL; } ::SelectObject( hMemDC, hBitmap ); // 把新位圖選到內(nèi)存設(shè)備描述表中 BOOL bRet = FALSE; BOOL bProcessed = FALSE; if ( IsOSWin7OrAbove() ) { DEVMODE curDevMode; memset( &curDevMode, 0, sizeof(curDevMode) ); curDevMode.dmSize = sizeof(DEVMODE); BOOL bEnumRet = ::EnumDisplaySettings( NULL, ENUM_CURRENT_SETTINGS, &curDevMode ); //strLog.Format( _T("[CCatchScreenDlg::CopyScreenToBitmap]m_xScreen: %d, curDevMode.dmPelsWidth: %d"), // m_xScreen, curDevMode.dmPelsWidth ); //WriteScreenCatchLog( strLog ); if ( bEnumRet && m_xScreen < curDevMode.dmPelsWidth ) { bProcessed = TRUE; ::SetStretchBltMode( hMemDC, STRETCH_HALFTONE ); bRet = ::StretchBlt( hMemDC, 0, 0, nWidth, nHeight, hScrDC, 0, 0, curDevMode.dmPelsWidth, curDevMode.dmPelsHeight, SRCCOPY|CAPTUREBLT ); } } if ( !bProcessed ) { bRet = ::BitBlt( hMemDC, 0, 0, nWidth, nHeight, hScrDC, nX, nY, SRCCOPY | CAPTUREBLT ); // CAPTUREBLT - 該參數(shù)保證能夠截到透明窗口 } if ( !bRet ) { strLog.Format( _T("[CCatchScreenDlg::CopyScreenToBitmap]將hScrDC拷貝到hMemDC失敗, GetLastError: %d"), GetLastError() ); WriteScreenCatchLog( strLog ); //::DeleteDC( hScrDC ); ::ReleaseDC( hWndDeskTop, hScrDC ); ::DeleteDC( hMemDC ); ::DeleteObject( hBitmap ); return NULL; } if ( hScrDC != NULL ) { //::DeleteDC( hScrDC ); ::ReleaseDC( hWndDeskTop, hScrDC ); } if ( hMemDC != NULL ) { ::DeleteDC( hMemDC ); } return hBitmap; // hBitmap資源不能釋放,因?yàn)楹瘮?shù)外部要使用}
? ? ? ?如果某個(gè)時(shí)刻線程的函數(shù)調(diào)用堆棧中所有函數(shù)占用??臻g總數(shù)超過當(dāng)前線程的??臻g上限,就會(huì)產(chǎn)生Stack Overflow的線程棧溢出的異常,程序就會(huì)閃退崩潰。
每個(gè)線程的??臻g是有上限的,在Windows中,每個(gè)線程的??臻g默認(rèn)是1MB,在創(chuàng)建線程時(shí)可以自定義線程的??臻g上限值。
? ? ? ?線程棧溢出的異常,一般發(fā)生在剛進(jìn)入到被調(diào)用的函數(shù)時(shí),函數(shù)的??臻g是在函數(shù)入口的地方分配的。程序在運(yùn)行過程中發(fā)生異?;蛘唛W退,可能就是有線程發(fā)生棧溢出導(dǎo)致的。對(duì)應(yīng)棧溢出的異常,在VS中調(diào)試是看不到異常時(shí)的函數(shù)調(diào)用堆棧的,因?yàn)榘l(fā)生異常時(shí)進(jìn)程直接退出調(diào)試了,只能看到發(fā)生了棧溢出的提示文字,但看不到發(fā)生棧溢出時(shí)的函數(shù)調(diào)用堆棧。
? ? ? ?可以使用windbg查看到異常時(shí)的函數(shù)調(diào)用堆棧。將windbg附加到目標(biāo)進(jìn)程上,當(dāng)目標(biāo)進(jìn)程發(fā)生棧溢出時(shí)windbg就能捕獲到異常中斷下來(lái),此時(shí)輸入kn等命令就可以查看到此刻的函數(shù)調(diào)用堆棧,就能找到產(chǎn)生異常的線索了。
線程發(fā)生棧溢出一般有一下幾個(gè)原因:
1)函數(shù)遞歸調(diào)用的深度過深,函數(shù)一直沒返回,??臻g一直沒有釋放;
2)消息上觸發(fā)函數(shù)的死循環(huán)調(diào)用,函數(shù)始終退不出來(lái),函數(shù)的??臻g釋放;
3)定義了一個(gè)占用內(nèi)存很大的局部變量;
4)函數(shù)中使用switch...case語(yǔ)句,包含了大量的case分支,每個(gè)case分支中都定義了局部變量,導(dǎo)致當(dāng)前函數(shù)占用了大量的棧空間。
? ? ? ?對(duì)于switch...case...語(yǔ)句中有多個(gè)case分支的情況,case分支中的局部變量的聲明周期是在case分支中的,即代碼運(yùn)行到對(duì)應(yīng)的case分支中時(shí)該分支中的局部變量才有“生命”,但其實(shí)這個(gè)局部變量的棧空間已經(jīng)在函數(shù)入口處分配好??臻g了,并不是代碼執(zhí)行到case子句中才分配??臻g的。這點(diǎn)可以通過編寫測(cè)試代碼,查看函數(shù)入口處給當(dāng)前函數(shù)分配棧空間的匯編代碼就能看出來(lái)了,可以先頂一個(gè)變量查看匯編代碼看看分配了多少??臻g,然后再增加一個(gè)變量,看看分配的??臻g是否變大。
? ? ? ?當(dāng)代碼中有內(nèi)存泄露,當(dāng)將所在進(jìn)程的內(nèi)存耗盡時(shí),就會(huì)出現(xiàn)“run out of memory”的崩潰:
? ? ? ?和GDI泄露類似的,有可能是某一塊的代碼有內(nèi)存泄露,在執(zhí)行某些操作時(shí)才會(huì)執(zhí)行有內(nèi)存泄露的代碼,才會(huì)觸發(fā)內(nèi)存泄漏。同樣,何時(shí)會(huì)導(dǎo)致“run out of memory”的崩潰,與泄露的程度、運(yùn)行的時(shí)長(zhǎng)有直接的關(guān)系。
? ? ? ?對(duì)于32位程序,系統(tǒng)會(huì)給該進(jìn)程分配4GB的虛擬地址空間,一般是2GB的用戶態(tài)內(nèi)存和2GB內(nèi)核態(tài)的內(nèi)存。隨著泄露的內(nèi)存越來(lái)越多,將4GB的虛擬地址空間耗完了,就會(huì)出現(xiàn)“run out of memory”的崩潰了。
? ? ? ?發(fā)現(xiàn)和排查內(nèi)存泄露,也GDI泄露的處理方式也是類似的。先通過查看資源管理器中的目標(biāo)進(jìn)程的內(nèi)存占用情況,一般情況下,程序正常運(yùn)行時(shí)只會(huì)占用幾百M(fèi)B的內(nèi)存,如果在資源管理器中發(fā)現(xiàn)目標(biāo)進(jìn)程的內(nèi)存都漲到1GB以上,并且長(zhǎng)時(shí)間處于1GB以上的占用,且不會(huì)回落,那大概率是有內(nèi)存泄露了。
? ? ? ?現(xiàn)在有很多內(nèi)存泄露檢測(cè)工具,比如BoundsChecker,但是很多已經(jīng)過時(shí)了,不能使用了。Windows下主要用Windbg調(diào)試器去排查,Linux下主要使用Valgrind內(nèi)存檢測(cè)工程。關(guān)于windbg如何排查內(nèi)存泄露,參看我的這篇文章:
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/123735.html
摘要:最近有不少初學(xué)編程的朋友問他們比較傾向于和作為他們首選學(xué)習(xí)語(yǔ)言,但是學(xué)好呢還是學(xué)更有前途到底哪一門語(yǔ)言更有錢途呢這個(gè)問題問的好,很多初學(xué)者都會(huì)有類似的疑問,今天我就來(lái)給大家簡(jiǎn)單的解答一下。 ? ? ? ? 最近有不少初學(xué)編程的朋友問:他們比較傾向于Java和C++作為他們首選學(xué)習(xí)語(yǔ)言,但是...
摘要:然而,中依然有可能發(fā)生內(nèi)存泄漏。所以你的安卓快速定位解決內(nèi)存泄漏掘金昨天是個(gè)好日子,程序員的節(jié)日,在這里給所有的程序員送上一份遲到的祝福。應(yīng)用內(nèi)存泄漏的定位分析與解決策略掘金,大家好,我是。 Android 性能優(yōu)化之巧用軟引用與弱引用優(yōu)化內(nèi)存使用 - Android - 掘金前言: 從事Android開發(fā)的同學(xué)都知道移動(dòng)設(shè)備的內(nèi)存使用是非常敏感的話題,今天我們來(lái)看下如何使用軟引用與弱...
摘要:優(yōu)化項(xiàng)也會(huì)引發(fā)一些問題。檢查你的代碼是否工作并修復(fù)問題。從起,及以上的優(yōu)化級(jí)別默認(rèn)啟動(dòng)了這項(xiàng)設(shè)置。目前正在進(jìn)行改進(jìn)。代碼移植系列文章代碼移植主題系列文章是中文站點(diǎn)的一部分內(nèi)容。 作者:云荒杯傾歡迎加入Wasm和emscripten技術(shù)交流群,群聊號(hào)碼:939206522。 這是關(guān)于Emscripten的系列文章,更多文章請(qǐng)看下面鏈接。 Emscripten代碼移植系列文章 Emscr...
閱讀 9065·2021-11-18 10:02
閱讀 2610·2019-08-30 15:43
閱讀 2667·2019-08-30 13:50
閱讀 1385·2019-08-30 11:20
閱讀 2714·2019-08-29 15:03
閱讀 3634·2019-08-29 12:36
閱讀 935·2019-08-23 17:04
閱讀 625·2019-08-23 14:18