Java程式碼執行記憶體溢位詳解及解決方案

女_killer發表於2018-11-02

轉載自https://blog.csdn.net/jianghuchuanke/article/details/79970749

記憶體溢位與資料庫鎖表的問題,可以說是開發人員的噩夢,一般的程式異常,總是可以知道在什麼時候或是在什麼操作步驟上出現了異常,而且根據堆疊資訊也很容易定位到程式中是某處出現了問題。記憶體溢位與鎖表則不然,一般現象是操作一般時間後系統越來越慢,直到當機,但並不能明確是在什麼操作上出現的,發生的時間點也沒有規律,檢視日誌或檢視資料庫也不能定位出問題的程式碼。
更嚴重的是記憶體溢位與資料庫鎖表在系統開發和單元測試階段並不容易被發現,當系統正式上線一般時間後,操作的併發量上來了,資料也積累了一些,系統就容易出現記憶體溢位或是鎖表的現象,而此時系統又不能隨意停機或重啟,為修正BUG帶來很大的困難。
本文以筆者開發和支援的多個專案為例,與大家分享在開發過程中遇到的Java記憶體溢位和資料庫鎖表的檢測和處理解決過程。
2.記憶體溢位的分析
記憶體溢位是指應用系統中存在無法回收的記憶體或使用的記憶體過多,最終使得程式執行要用到的記憶體大於虛擬機器能提供的最大記憶體。為了解決Java中記憶體溢位問題,我們首先必須瞭解Java是如何管理記憶體的。Java的記憶體管理就是物件的分配和釋放問題。在Java中,記憶體的分配是由程式完成的,而記憶體的釋放是由垃圾收集器(Garbage Collection,GC)完成的,程式設計師不需要通過呼叫GC函式來釋放記憶體,因為不同的JVM實現者可能使用不同的演算法管理GC,有的是記憶體使用到達一定程度時,GC才開始工作,也有定時執行的,有的是中斷式執行GC。但GC只能回收無用並且不再被其它物件引用的那些物件所佔用的空間。Java的記憶體垃圾回收機制是從程式的主要執行物件開始檢查引用鏈,當遍歷一遍後發現沒有被引用的孤立物件就作為垃圾回收。
引起記憶體溢位的原因有很多種,常見的有以下幾種:
l 記憶體中載入的資料量過於龐大,如一次從資料庫取出過多資料;
l 集合類中有對物件的引用,使用完後未清空,使得JVM不能回收;
l 程式碼中存在死迴圈或迴圈產生過多重複的物件實體;
l 使用的第三方軟體中的BUG;
l 啟動引數記憶體值設定的過小;
3.記憶體溢位的解決
記憶體溢位雖然很棘手,但也有相應的解決辦法,可以按照從易到難,一步步的解決。
第一步,就是修改JVM啟動引數,直接增加記憶體。這一點看上去似乎很簡單,但很容易被忽略。JVM預設可以使用的記憶體為64M,Tomcat預設可以使用的記憶體為128MB,對於稍複雜一點的系統就會不夠用。在某專案中,就因為啟動引數使用的預設值,經常報“OutOfMemory”錯誤。因此,-Xms,-Xmx引數一定不要忘記加。
第二步,檢查錯誤日誌,檢視“OutOfMemory”錯誤前是否有其它異常或錯誤。在一個專案中,使用兩個資料庫連線,其中專用於傳送簡訊的資料庫連線使用DBCP連線池管理,使用者為不將簡訊發出,有意將資料庫連線使用者名稱改錯,使得日誌中有許多資料庫連線異常的日誌,一段時間後,就出現“OutOfMemory”錯誤。經分析,這是由於DBCP連線池BUG引起的,資料庫連線不上後,沒有將連線釋放,最終使得DBCP報“OutOfMemory”錯誤。經過修改正確資料庫連線引數後,就沒有再出現記憶體溢位的錯誤。
檢視日誌對於分析記憶體溢位是非常重要的,通過仔細檢視日誌,分析記憶體溢位前做過哪些操作,可以大致定位有問題的模組。
第三步,安排有經驗的程式設計人員對程式碼進行走查和分析,找出可能發生記憶體溢位的位置。重點排查以下幾點:
l 檢查程式碼中是否有死迴圈或遞迴呼叫。
l 檢查是否有大迴圈重複產生新物件實體。
l 檢查對資料庫查詢中,是否有一次獲得全部資料的查詢。一般來說,如果一次取十萬條記錄到記憶體,就可能引起記憶體溢位。這個問題比較隱蔽,在上線前,資料庫中資料較少,不容易出問題,上線後,資料庫中資料多了,一次查詢就有可能引起記憶體溢位。因此對於資料庫查詢儘量採用分頁的方式查詢。
l 檢查List、MAP等集合物件是否有使用完後,未清除的問題。List、MAP等集合物件會始終存有對物件的引用,使得這些物件不能被GC回收。
第四步,使用記憶體檢視工具動態檢視記憶體使用情況。某個專案上線後,每次系統啟動兩天後,就會出現記憶體溢位的錯誤。這種情況一般是程式碼中出現了緩慢的記憶體洩漏,用上面三個步驟解決不了,這就需要使用記憶體檢視工具了。
記憶體檢視工具有許多,比較有名的有:Optimizeit Profiler、JProbe Profiler、JinSight和Java1.5的Jconsole等。它們的基本工作原理大同小異,都是監測Java程式執行時所有物件的申請、釋放等動作,將記憶體管理的所有資訊進行統計、分析、視覺化。開發人員可以根據這些資訊判斷程式是否有記憶體洩漏問題。一般來說,一個正常的系統在其啟動完成後其記憶體的佔用量是基本穩定的,而不應該是無限制的增長的。持續地觀察系統執行時使用的記憶體的大小,可以看到在記憶體使用監控視窗中是基本規則的鋸齒形的圖線,如果記憶體的大小持續地增長,則說明系統存在記憶體洩漏問題。通過間隔一段時間取一次記憶體快照,然後對記憶體快照中物件的使用與引用等資訊進行比對與分析,可以找出是哪個類的物件在洩漏。
通過以上四個步驟的分析與處理,基本能處理記憶體溢位的問題。當然,在這些過程中也需要相當的經驗與敏感度,需要在實際的開發與除錯過程中不斷積累。
總體上來說,產生記憶體溢位是由於程式碼寫的不好造成的,因此提高程式碼的質量是最根本的解決辦法。有的人認為先把功能實現,有BUG時再在測試階段進行修正,這種想法是錯誤的。正如一件產品的質量是在生產製造的過程中決定的,而不是質量檢測時決定的,軟體的質量在設計與編碼階段就已經決定了,測試只是對軟體質量的一個驗證,因為測試不可能找出軟體中所有的BUG。


原因有很多種,比如:
1.資料量過於龐大;死迴圈 ;靜態變數和靜態方法過多;遞迴;無法確定是否被引用的物件;
2.虛擬機器不回收記憶體(記憶體洩漏);

說白了就是程式執行要用到的記憶體大於虛擬機器能提供的最大記憶體就發生記憶體溢位了。 記憶體溢位的問題要看業務和系統大小而定,對於某些系統可能記憶體溢位不常見,但某些系統還是很常見的解決的方法,

一個是優化程式程式碼,如果業務龐大,邏輯複雜,儘量減少全域性變數的引用,讓程式使用完變數的時候釋放該引用能夠讓垃圾回收器回收,釋放資源。
二就是物理解決,增大實體記憶體,然後通過:-Xms256m -Xmx256m -XX:MaxNewSize=256m -XX:MaxPermSize=256m的修改
一、記憶體溢位型別
1 、 java.lang.OutOfMemoryError: PermGen space
JVM 管理兩種型別的記憶體,堆和非堆。堆是給開發人員用的上面說的就是,是在 JVM 啟動時建立;非堆是留給 JVM 自己用的,用來存放類的資訊的。它和堆不同,執行期內 GC 不會釋放空間。如果 web app 用了大量的第三方 jar 或者應用有太多的 class 檔案而恰好 MaxPermSize 設定較小,超出了也會導致這塊記憶體的佔用過多造成溢位,或者 tomcat 熱部署時侯不會清理前面載入的環境,只會將 context 更改為新部署的,非堆存的內容就會越來越多。
2 、 java.lang.OutOfMemoryError: Java heap space
第一種情況是個補充,主要存在問題就是出現在這個情況中。其預設空間 ( 即 -Xms) 是實體記憶體的 1/64 ,最大空間 (-Xmx) 是實體記憶體的 1/4 。如果記憶體剩餘不到 40 %, JVM 就會增大堆到 Xmx 設定的值,記憶體剩餘超過 70 %, JVM 就會減小堆到 Xms 設定的值。所以伺服器的 Xmx 和 Xms 設定一般應該設定相同避免每次 GC 後都要調整虛擬機器堆的大小。假設實體記憶體無限大,那麼 JVM 記憶體的最大值跟作業系統有關,一般 32 位機是 1.5g 到 3g 之間,而 64 位的就不會有限制了。
注意:如果 Xms 超過了 Xmx 值,或者堆最大值和非堆最大值的總和超過了實體記憶體或者作業系統的最大限制都會引起伺服器啟動不起來。
垃圾回收 GC 的角色
JVM 呼叫 GC 的頻度還是很高的,主要兩種情況下進行垃圾回收:
當應用程式執行緒空閒;另一個是 java 記憶體堆不足時,會不斷呼叫 GC ,若連續回收都解決不了記憶體堆不足的問題時,就會報 out of memory 錯誤。因為這個異常根據系統執行環境決定,所以無法預期它何時出現。
根據 GC 的機制,程式的執行會引起系統執行環境的變化,增加 GC 的觸發機會。
為了避免這些問題,程式的設計和編寫就應避免垃圾物件的記憶體佔用和 GC 的開銷。顯示呼叫 System.GC() 只能建議 JVM 需要在記憶體中對垃圾物件進行回收,但不是必須馬上回收,
一個是並不能解決記憶體資源耗空的局面,另外也會增加 GC 的消耗。
二、 JVM 記憶體區域組成
簡單的說 java中的堆和棧
java把記憶體分兩種:一種是棧記憶體,另一種是堆記憶體
1。在函式中定義的基本型別變數和物件的引用變數都在函式的棧記憶體中分配;
2。堆記憶體用來存放由 new建立的物件和陣列
在函式(程式碼塊)中定義一個變數時, java就在棧中為這個變數分配記憶體空間,當超過變數的作用域後, java會自動釋放掉為該變數所分配的記憶體空間;在堆中分配的記憶體由 java虛擬機器的自動垃圾回收器來管理
堆的優勢是可以動態分配記憶體大小,生存期也不必事先告訴編譯器,因為它是在執行時動態分配記憶體的。缺點就是要在執行時動態分配記憶體,存取速度較慢;
棧的優勢是存取速度比堆要快,缺點是存在棧中的資料大小與生存期必須是確定的無靈活 性。
java 堆分為三個區: New 、 Old 和 Permanent
GC 有兩個執行緒:
新建立的物件被分配到 New 區,當該區被填滿時會被 GC 輔助執行緒移到 Old 區,當 Old 區也填滿了會觸發 GC 主執行緒遍歷堆記憶體裡的所有物件。 Old 區的大小等於 Xmx 減去 -Xmn
java棧存放
棧調整:引數有 +UseDefaultStackSize -Xss256K,表示每個執行緒可申請 256k的棧空間
每個執行緒都有他自己的 Stack
三、 JVM如何設定虛擬記憶體
提示:在 JVM中如果 98%的時間是用於 GC且可用的 Heap size 不足 2%的時候將丟擲此異常資訊。
提示: Heap Size 最大不要超過可用實體記憶體的 80%,一般的要將 -Xms和 -Xmx選項設定為相同,而 -Xmn為 1/4的 -Xmx值。
提示: JVM初始分配的記憶體由 -Xms指定,預設是實體記憶體的 1/64; JVM最大分配的記憶體由 -Xmx指定,預設是實體記憶體的 1/4。
預設空餘堆記憶體小於 40%時, JVM就會增大堆直到 -Xmx的最大限制;空餘堆記憶體大於 70%時, JVM會減少堆直到 -Xms的最小限制。因此伺服器一般設定 -Xms、 -Xmx相等以避免在每次 GC 後調整堆的大小。
提示:假設實體記憶體無限大的話, JVM記憶體的最大值跟作業系統有很大的關係。
簡單的說就 32位處理器雖然可控記憶體空間有 4GB,但是具體的作業系統會給一個限制,
這個限制一般是 2GB-3GB(一般來說 Windows系統下為 1.5G-2G, Linux系統下為 2G-3G), 而 64bit以上的處理器就不會有限制了
提示:注意:如果 Xms超過了 Xmx值,或者堆最大值和非堆最大值的總和超過了物理內 存或者作業系統的最大限制都會引起伺服器啟動不起來。
提示:設定 NewSize、 MaxNewSize相等, “new”的大小最好不要大於 “old”的一半,原因是 old區如果不夠大會頻繁的觸發 “主 ” GC ,大大降低了效能
JVM使用 -XX:PermSize設定非堆記憶體初始值,預設是實體記憶體的 1/64;
由 XX:MaxPermSize設定最大非堆記憶體的大小,預設是實體記憶體的 1/4。
解決方法:手動設定 Heap size
修改 TOMCAT_HOME/bin/catalina.bat
在“ echo “Using CATALINA_BASE: $CATALINA_BASE””上面加入以下行:
JAVA_OPTS=”-server -Xms800m -Xmx800m -XX:MaxNewSize=256m”
四、效能檢查工具使用
定位記憶體洩漏:
JProfiler 工具主要用於檢查和跟蹤系統(限於 Java 開發的)的效能。 JProfiler 可以通過時時的監控系統的記憶體使用情況,隨時監視垃圾回收,執行緒執行狀況等手段,從而很好的監視 JVM 執行情況及其效能。

  1. 應用伺服器記憶體長期不合理佔用,記憶體經常處於高位佔用,很難回收到低位;
  2. 應用伺服器極為不穩定,幾乎每兩天重新啟動一次,有時甚至每天重新啟動一次;
  3. 應用伺服器經常做 Full GC(Garbage Collection),而且時間很長,大約需要 30-40秒,應用伺服器在做 Full GC的時候是不響應客戶的交易請求的,非常影響系統效能。
    因為開發環境和產品環境會有不同,導致該問題發生有時會在產品環境中發生, 通常可以使用工具跟蹤系統的記憶體使用情況,在有些個別情況下或許某個時刻確實 是使用了大量記憶體導致 out of memory,這時應繼續跟蹤看接下來是否會有下降,

如果一直居高不下這肯定就因為程式的原因導致記憶體洩漏。
五、不健壯程式碼的特徵及解決辦法
1 、儘早釋放無用物件的引用。好的辦法是使用臨時變數的時候,讓引用變數在退出活動域後,自動設定為 null ,暗示垃圾收集器來收集該物件,防止發生記憶體洩露。
對於仍然有指標指向的例項, jvm 就不會回收該資源 , 因為垃圾回收會將值為 null 的物件作為垃圾,提高 GC 回收機制效率;
2 、我們的程式裡不可避免大量使用字串處理,避免使用 String ,應大量使用 StringBuffer ,每一個 String 物件都得獨立佔用記憶體一塊區域;
String str = “aaa”;

String str2 = “bbb”;

String str3 = str + str2;// 假如執行此次之後 str ,str2 以後再不被呼叫 , 那它就會被放在記憶體中等待 Java 的 gc 去回收 , 程式內過多的出現這樣的情況就會報上面的那個錯誤 , 建議在使用字串時能使用 StringBuffer 就不要用 String, 這樣可以省不少開銷;
3 、儘量少用靜態變數,因為靜態變數是全域性的, GC 不會回收的;
4 、避免集中建立物件尤其是大物件, JVM 會突然需要大量記憶體,這時必然會觸發 GC 優化系統記憶體環境;顯示的宣告陣列空間,而且申請數量還極大。
這是一個案例想定供大家警戒:
使用jspsmartUpload作檔案上傳,現在執行過程中經常出現java.outofMemoryError的錯誤,用top命令看看程式使用情況,發現記憶體不足2M,花了很長時間,發現是jspsmartupload的問題。把jspsmartupload元件的原始碼檔案(class檔案)反編譯成Java檔案,如夢方醒:
m_totalBytes = m_request.getContentLength();
m_binArray = new byte[m_totalBytes];
變數m_totalBytes表示使用者上傳的檔案的總長度,這是一個很大的數。如果用這樣大的數去宣告一個byte陣列,並給陣列的每個元素分配記憶體空間,而且m_binArray陣列不能馬上被釋放,JVM的垃圾回收確實有問題,導致的結果就是記憶體溢位。
jspsmartUpload為什末要這樣作,有他的原因,根據RFC1867的http上傳標準,得到一個檔案流,並不知道檔案流的長度。設計者如果想檔案的長度,只有操作servletinputstream一次才知道,因為任何流都不知道大小。只有知道檔案長度了,才可以限制使用者上傳檔案的長度。為了省去這個麻煩,jspsmartUpload設計者直接在記憶體中開啟檔案,判斷長度是否符合標準,符合就寫到伺服器的硬碟。這樣產生記憶體溢位,這只是我的一個猜測而已。
所以程式設計的時候,不要在記憶體中申請大的空間,因為web伺服器的記憶體有限,並且儘可能的使用流操作,例如
byte[] mFileBody = new byte[512];

     Blob vField= rs.getBlob("FileBody");   
  InputStream instream=vField.getBinaryStream();   
  FileOutputStream fos=new FileOutputStream(saveFilePath+CFILENAME);   
     int b;   
                  while( (b =instream.read(mFileBody)) != -1){   
                    fos.write(mFileBody,0,b);   
                     }   
    fos.close();   
  instream.close();  

5 、儘量運用物件池技術以提高系統效能;生命週期長的物件擁有生命週期短的物件時容易引發記憶體洩漏,例如大集合物件擁有大資料量的業務物件的時候,可以考慮分塊進行處理,然後解決一塊釋放一塊的策略。
6 、不要在經常呼叫的方法中建立物件,尤其是忌諱在迴圈中建立物件。可以適當的使用 hashtable , vector 建立一組物件容器,然後從容器中去取那些物件,而不用每次 new 之後又丟棄
7 、一般都是發生在開啟大型檔案或跟資料庫一次拿了太多的資料,造成 Out Of Memory Error 的狀況,這時就大概要計算一下資料量的最大值是多少,並且設定所需最小及最大的記憶體空間值。


相關文章