少年阿賓

          那些青春的歲月

            BlogJava :: 首頁 :: 聯系 :: 聚合  :: 管理
            500 Posts :: 0 Stories :: 135 Comments :: 0 Trackbacks

          內存溢出與數據庫鎖表的問題,可以說是開發人員的噩夢,一般的程序異常,總是可以知道在什么時候或是在什么操作步驟上出現了異常,而且根據堆棧信息也很容易定位到程序中是某處出現了問題。內存溢出與鎖表則不然,一般現象是操作一般時間后系統越來越慢,直到死機,但并不能明確是在什么操作上出現的,發生的時間點也沒有規律,查看日志或查看數據庫也不能定位出問題的代碼。

          更嚴重的是內存溢出與數據庫鎖表在系統開發和單元測試階段并不容易被發現,當系統正式上線一般時間后,操作的并發量上來了,數據也積累了一些,系統就容易出現內存溢出或是鎖表的現象,而此時系統又不能隨意停機或重啟,為修正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””上面加入以下行:

          1. 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 對象都得獨立占用內存一塊區域;

          1. String str = “aaa”;   
          2.   
          3. String str2 = “bbb”;   
          4.   
          5. String str3 = str + str2;// 假如執行此次之后 str ,str2 以后再不被調用 , 那它就會被放在內存中等待 Java 的 gc 去回收 , 程序內過多的出現這樣的情況就會報上面的那個錯誤 , 建議在使用字符串時能使用 StringBuffer 就不要用 String, 這樣可以省不少開銷;   

          3 、盡量少用靜態變量,因為靜態變量是全局的, GC 不會回收的;

          4 、避免集中創建對象尤其是大對象, JVM 會突然需要大量內存,這時必然會觸發 GC 優化系統內存環境;顯示的聲明數組空間,而且申請數量還極大。

          這是一個案例想定供大家警戒:

          使用jspsmartUpload作文件上傳,現在運行過程中經常出現java.outofMemoryError的錯誤,用top命令看看進程使用情況,發現內存不足2M,花了很長時間,發現是jspsmartupload的問題。把jspsmartupload組件的源碼文件(class文件)反編譯成Java文件,如夢方醒:

          1. m_totalBytes = m_request.getContentLength();        
          2. m_binArray = new byte[m_totalBytes];      

          變量m_totalBytes表示用戶上傳的文件的總長度,這是一個很大的數。如果用這樣大的數去聲明一個byte數組,并給數組的每個元素分配內存空間,而且m_binArray數組不能馬上被釋放,JVM的垃圾回收確實有問題,導致的結果就是內存溢出。

          jspsmartUpload為什末要這樣作,有他的原因,根據RFC1867的http上傳標準,得到一個文件流,并不知道文件流的長度。設計者如果想文件的長度,只有操作servletinputstream一次才知道,因為任何流都不知道大小。只有知道文件長度了,才可以限制用戶上傳文件的長度。為了省去這個麻煩,jspsmartUpload設計者直接在內存中打開文件,判斷長度是否符合標準,符合就寫到服務器的硬盤。這樣產生內存溢出,這只是我的一個猜測而已。

          所以編程的時候,不要在內存中申請大的空間,因為web服務器的內存有限,并且盡可能的使用流操作,例如

          1. byte[] mFileBody = new byte[512];   
          2.          Blob vField= rs.getBlob("FileBody");   
          3.       InputStream instream=vField.getBinaryStream();   
          4.       FileOutputStream fos=new FileOutputStream(saveFilePath+CFILENAME);   
          5.          int b;   
          6.                       while( (b =instream.read(mFileBody)) != -1){   
          7.                         fos.write(mFileBody,0,b);   
          8.                          }   
          9.         fos.close();   
          10.       instream.close();  

          5 、盡量運用對象池技術以提高系統性能;生命周期長的對象擁有生命周期短的對象時容易引發內存泄漏,例如大集合對象擁有大數據量的業務對象的時候,可以考慮分塊進行處理,然后解決一塊釋放一塊的策略。

          6 、不要在經常調用的方法中創建對象,尤其是忌諱在循環中創建對象。可以適當的使用 hashtable , vector 創建一組對象容器,然后從容器中去取那些對象,而不用每次 new 之后又丟棄

          7 、一般都是發生在開啟大型文件或跟數據庫一次拿了太多的數據,造成 Out Of Memory Error 的狀況,這時就大概要計算一下數據量的最大值是多少,并且設定所需最小及最大的內存空間值。

          posted on 2012-10-25 23:16 abin 閱讀(636) 評論(0)  編輯  收藏 所屬分類: JVM
          主站蜘蛛池模板: 乐都县| 安西县| 宁波市| 温宿县| 夹江县| 南召县| 东城区| 武鸣县| 正镶白旗| 东源县| 湘潭市| 边坝县| 新和县| 浮山县| 弋阳县| 郧西县| 荔波县| 灵台县| 嘉黎县| 镇坪县| 武功县| 中山市| 高唐县| 仙居县| 梨树县| 井冈山市| 宁国市| 西青区| 东方市| 安乡县| 馆陶县| 南和县| 武陟县| 辽阳市| 渝中区| 宣武区| 呼和浩特市| 庆元县| 石首市| 华宁县| 墨玉县|