【轉】java中的記憶體溢位和記憶體洩漏

陳俊成發表於2016-10-03

原文地址:點選進入

記憶體溢位:
對於整個應用程式來說,JVM記憶體空間,已經沒有多餘的空間分配給新的物件。所以就發生記憶體溢位。

記憶體洩露:
在應用的整個生命週期內,某個物件一直存在,且物件佔用的記憶體空間越來越大,最終導致JVM記憶體洩露,
比如:快取的應用,如果不設定上限的話,快取的容量可能會一直增長。
靜態集合引用,如果該集合存放了無數個物件,隨著時間的推移也有可能使容量無限制的增長,最終導致JVM記憶體洩露。

記憶體洩露,是應用程式中的某個物件長時間的存活,並且佔用空間不斷增長,最終導致記憶體洩露。
是物件分配後,長時間的容量增長。

記憶體溢位,是針對整個應用程式的所有物件的分配空間不足,會造成記憶體溢位。

記憶體洩漏
記憶體洩漏指由於疏忽或錯誤造成程式未能釋放已經不再使用的記憶體的情況。記憶體洩漏並非指記憶體在物理上的消失,而是應用程式分配某段記憶體後,由於設計錯誤,失去了對該段記憶體的控制,因而造成了記憶體的浪費。記憶體洩漏與許多其他問題有著相似的症狀,並且通常情況下只能由那些可以獲得程式原始碼的程式設計師才可以分析出來。然而,有不少人習慣於把任何不需要的記憶體使用的增加描述為記憶體洩漏,即使嚴格意義上來說這是不準確的。
  一般我們常說的記憶體洩漏是指堆記憶體的洩漏。堆記憶體是指程式從堆中分配的,大小任意的(記憶體塊的大小可以在程式執行期決定),使用完後必須顯示釋放的記憶體。應用程式一般使用malloc,realloc,new等函式從堆中分配到一塊記憶體,使用完後,程式必須負責相應的呼叫free或delete釋放該記憶體塊,否則,這塊記憶體就不能被再次使用,我們就說這塊記憶體洩漏了。
  記憶體洩漏可以分為4類:
  1. 常發性記憶體洩漏。發生記憶體洩漏的程式碼會被多次執行到,每次被執行的時候都會導致一塊記憶體洩漏。
  2. 偶發性記憶體洩漏。發生記憶體洩漏的程式碼只有在某些特定環境或操作過程下才會發生。常發性和偶發性是相對的。對於特定的環境,偶發性的也許就變成了常發性的。所以測試環境和測試方法對檢測記憶體洩漏至關重要。
  3. 一次性記憶體洩漏。發生記憶體洩漏的程式碼只會被執行一次,或者由於演算法上的缺陷,導致總會有一塊僅且一塊記憶體發生洩漏。比如,在類的建構函式中分配記憶體,在解構函式中卻沒有釋放該記憶體,所以記憶體洩漏只會發生一次。
  4. 隱式記憶體洩漏。程式在執行過程中不停的分配記憶體,但是直到結束的時候才釋放記憶體。嚴格的說這裡並沒有發生記憶體洩漏,因為最終程式釋放了所有申請的記憶體。但是對於一個伺服器程式,需要執行幾天,幾周甚至幾個月,不及時釋放記憶體也可能導致最終耗盡系統的所有記憶體。所以,我們稱這類記憶體洩漏為隱式記憶體洩漏。

簡單點:
記憶體洩漏就是忘記釋放使用完畢的記憶體,讓下次使用有一定風險。
記憶體溢位就是一定的記憶體空間不能裝下所有的需要存放的資料,造成記憶體資料溢位。

主要從以下幾部分來說明,關於記憶體和記憶體洩露、溢位的概念,區分記憶體洩露和記憶體溢位;記憶體的區域劃分,瞭解GC回收機制;重點關注如何去監控和發現記憶體問題;此外分析出問題還要如何解決記憶體問題。

  下面就開始本篇的內容:

  第一部分 概念

  眾所周知,java中的記憶體由java虛擬機器自己去管理的,他不像C++需要自己去釋放。籠統地去講,java的記憶體分配分為兩個部分,一個是資料堆,一個是棧。程式在執行的時候一般分配資料堆,把區域性的臨時的變數都放進去,生命週期和程式有關係。但是如果程式設計師宣告瞭static的變數,就直接在棧中執行的,程式銷燬了,不一定會銷燬static變數。

  另外為了保證java記憶體不會溢位,java中有垃圾回收機制。 System.gc()即垃圾收集機制是指jvm用於釋放那些不再使用的物件所佔用的記憶體。java語言並不要求jvm有gc,也沒有規定gc如何工作。垃圾收集的目的在於清除不再使用的物件。gc通過確定物件是否被活動物件引用來確定是否收集該物件。

  而其中,記憶體溢位就是你要求分配的java虛擬機器記憶體超出了系統能給你的,系統不能滿足需求,於是產生溢位。

  記憶體洩漏是指你向系統申請分配記憶體進行使用(new),可是使用完了以後卻不歸還(delete),結果你申請到的那塊記憶體你自己也不能再訪問,該塊已分配出來的記憶體也無法再使用,隨著伺服器記憶體的不斷消耗,而無法使用的記憶體越來越多,系統也不能再次將它分配給需要的程式,產生洩露。一直下去,程式也逐漸無記憶體使用,就會溢位。

  第二部分 原理

  JAVA垃圾回收及對記憶體區劃分

  在Java虛擬機器規範中,提及瞭如下幾種型別的記憶體空間:

  ◇ 棧記憶體(Stack):每個執行緒私有的。

  ◇ 堆記憶體(Heap):所有執行緒公用的。

  ◇ 方法區(Method Area):有點像以前常說的“程式程式碼段”,這裡面存放了每個載入類的反射資訊、類函式的程式碼、編譯時常量等資訊。

  ◇ 原生方法棧(Native Method Stack):主要用於JNI中的原生程式碼,平時很少涉及。

  而Java的使用的是堆記憶體,java堆是一個執行時資料區,類的例項(物件)從中分配空間。Java虛擬機器(JVM)的堆中儲存著正在執行的應用程式所建立的所有物件,“垃圾回收”也是主要是和堆記憶體(Heap)有關。

  垃圾回收的概念就是JAVA虛擬機器(JVM)回收那些不再被引用的物件記憶體的過程。一般我們認為正在被引用的物件狀態為“alive”,而沒有被應用或者取不到引用屬性的物件狀態為“dead”。垃圾回收是一個釋放處於”dead”狀態的物件的記憶體的過程。而垃圾回收的規則和演算法被動態的作用於應用執行當中,自動回收。

  JVM的垃圾回收器採用的是一種分代(generational )回收策略,用較高的頻率對年輕的物件(young generation)進行掃描和回收,這種叫做minor collection,而對老物件(old generation)的檢查回收頻率要低很多,稱為major collection。這樣就不需要每次GC都將記憶體中所有物件都檢查一遍,這種策略有利於實時觀察和回收。

  (Sun JVM 1.3 有兩種最基本的記憶體收集方式:一種稱為copying或scavenge,將所有仍然生存的物件搬到另外一塊記憶體後,整塊記憶體就可回收。這種方法有效率,但需要有一定的空閒記憶體,拷貝也有開銷。這種方法用於minor collection。另外一種稱為mark-compact,將活著的物件標記出來,然後搬遷到一起連成大塊的記憶體,其他記憶體就可以回收了。這種方法不需要佔用額外的空間,但速度相對慢一些。這種方法用於major collection. )

  一些物件被建立出來只是擁有短暫的生命週期,比如 iterators 和本地變數。另外一些物件被建立是擁有很長的生命週期,比如持久化物件等。

  垃圾回收器的分代策略是把記憶體區劃分為幾個代,然後為每個代分配一到多個記憶體區塊。當其中一個代用完了分配給他的記憶體後,JVM會在分配的記憶體區內執行一個區域性的GC(也可以叫minor collection)操作,為了回收處於“dead”狀態的物件所佔用的記憶體。區域性GC通常要比Full GC快很多。

JVM定義了兩個代,年輕代(yong generation)(有時稱為“nursery”託兒所)和老年代(old generation)。年輕代包括 “Eden space(伊甸園)”和兩個“survivor spaces”。虛擬記憶體初始化的時候會把所有物件都分配到 Eden space,並且大部分物件也會在該區域被釋放。 當進行 minor GC的時候,VM會把剩下的沒有釋放的物件從Eden space移動到其中一個survivor spaces當中。此外,VM也會把那些長期存活在survivor spaces 裡的物件移動到 老生代的“tenured” space中。當 tenured generation 被填滿後,就會產生Full GC,Full GC會相對比較慢因為回收的內容包括了所有的 live狀態的物件。pemanet generation這個代包括了所有java虛擬機器自身使用的相對比較穩定的資料物件,比如類和物件方法等。

  關於代的劃分,可以從下圖中獲得一個概況:

  第三部分 總結

  記憶體溢位主要是由於程式碼編寫時對某些方法、類應用不合理,或者沒有預估到臨時物件會佔用很大記憶體量,或者把過多的資料放入JVM快取,或者效能壓力大導致訊息堆積而佔用記憶體,以至於在效能測試時,生成龐大數量的臨時物件,GC時沒有做出有效回收甚至根本就不能回收,造成記憶體空間不足,記憶體溢位。

  如果編碼之前,對記憶體使用量進行預估,對放在記憶體中的資料進行評估,保證有用的資訊儘快釋放,無用的資訊能夠被GC回收,這樣在一定程度上是可以避免記憶體溢位問題的。

相關文章