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