1. 程式人生 > >JAVA 記憶體洩露詳解(原因、例子及解決)

JAVA 記憶體洩露詳解(原因、例子及解決)

  Java的一個重要特性就是通過垃圾收集器(GC)自動管理記憶體的回收,而不需要程式設計師自己來釋放記憶體。理論上Java中所有不會再被利用的物件所佔用的記憶體,都可以被GC回收,但是Java也存在記憶體洩露,但它的表現與C++不同。


JAVA 中的記憶體管理

    要了解Java中的記憶體洩露,首先就得知道Java中的記憶體是如何管理的。

    在Java程式中,我們通常使用new為物件分配記憶體,而這些記憶體空間都在堆(Heap)上。

    下面看一個示例:

  1. publicclassSimple{
  2. publicstaticvoid main(String args[]){
  3. Object object1 =newObject();//obj1
  4. Object object2 =newObject();//obj2
  5. object2 = object1;
  6. //...此時,obj2是可以被清理的
  7. }
  8. }

    Java使用有向圖的方式進行記憶體管理:

    

    在有向圖中,我們叫作obj1是可達的,obj2就是不可達的,顯然不可達的可以被清理。

    記憶體的釋放,也即清理那些不可達的物件,是由GC決定和執行的,所以GC會監控每一個物件的狀態,包括申請、引用、被引用和賦值等。釋放物件的根本原則就是物件不會再被使用

  •     給物件賦予了空值null,之後再沒有呼叫過。
  •     另一個是給物件賦予了新值,這樣重新分配了記憶體空間。

    通常,會認為在堆上分配物件的代價比較大,但是GC卻優化了這一操作:C++中,在堆上分配一塊記憶體,會查詢一塊適用的記憶體加以分配,如果物件銷燬,這塊記憶體就可以重用;而Java中,就想一條長的帶子,每分配一個新的物件,Java的“堆指標”就向後移動到尚未分配的區域。所以,Java分配記憶體的效率,可與C++媲美。

    但是這種工作方式有一個問題:如果頻繁的申請記憶體,資源將會耗盡。這時GC就介入了進來,它會回收空間,並使堆中的物件排列更緊湊。這樣,就始終會有足夠大的記憶體空間可以分配。

    gc清理時的引用計數方式:當引用連線至新物件時,引用計數+1;當某個引用離開作用域或被設定為null時,引用計數-1,GC發現這個計數為0時,就回收其佔用的記憶體。這個開銷會在引用程式的整個生命週期發生,並且不能處理迴圈引用的情況。所以這種方式只是用來說明GC的工作方式,而不會被任何一種Java虛擬機器應用。

    多數GC採用一種自適應的清理方式(加上其他附加的用於提升速度的技術),主要依據是找出任何“活”的物件,然後採用“自適應的、分代的、停止-複製、標記-清理”式的垃圾回收器。具體不介紹太多,這不是本文重點。

JAVA 中的記憶體洩露

    Java中的記憶體洩露,廣義並通俗的說,就是:不再會被使用的物件的記憶體不能被回收,就是記憶體洩露。

    Java中的記憶體洩露與C++中的表現有所不同。

    在C++中,所有被分配了記憶體的物件,不再使用後,都必須程式設計師手動的釋放他們。所以,每個類,都會含有一個解構函式,作用就是完成清理工作,如果我們忘記了某些物件的釋放,就會造成記憶體洩露。

    但是在Java中,我們不用(也沒辦法)自己釋放記憶體,無用的物件由GC自動清理,這也極大的簡化了我們的程式設計工作。但,實際有時候一些不再會被使用的物件,在GC看來不能被釋放,就會造成記憶體洩露。

    我們知道,物件都是有生命週期的,有的長,有的短,如果長生命週期的物件持有短生命週期的引用,就很可能會出現記憶體洩露。我們舉一個簡單的例子:

  1. publicclassSimple{
  2. Objectobject;
  3. publicvoid method1(){
  4. object=newObject();
  5. //...其他程式碼
  6. }
  7. }

    這裡的object例項,其實我們期望它只作用於method1()方法中,且其他地方不會再用到它,但是,當method1()方法執行完成後,object物件所分配的記憶體不會馬上被認為是可以被釋放的物件,只有在Simple類建立的物件被釋放後才會被釋放,嚴格的說,這就是一種記憶體洩露。解決方法就是將object作為method1()方法中的區域性變數。當然,如果一定要這麼寫,可以改為這樣:

  1. publicclassSimple{
  2. Objectobject;
  3. publicvoid method1(){
  4. object=newObject();
  5. //...其他程式碼
  6. object=null;
  7. }
  8. }

    這樣,之前“new Object()”分配的記憶體,就可以被GC回收。

    到這裡,Java的記憶體洩露應該都比較清楚了。下面再進一步說明:

  •     在堆中的分配的記憶體,在沒有將其釋放掉的時候,就將所有能訪問這塊記憶體的方式都刪掉(如指標重新賦值),這是針對c++等語言的,Java中的GC會幫我們處理這種情況,所以我們無需關心。
  •     在記憶體物件明明已經不需要的時候,還仍然保留著這塊記憶體和它的訪問方式(引用),這是所有語言都有可能會出現的記憶體洩漏方式。程式設計時如果不小心,我們很容易發生這種情況,如果不太嚴重,可能就只是短暫的記憶體洩露。

一些容易發生記憶體洩露的例子和解決方法

    像上面例子中的情況很容易發生,也是我們最容易忽略並引發記憶體洩露的情況,解決的原則就是儘量減小物件的作用域(比如Android studio中,上面的程式碼就會發出警告,並給出的建議是將類的成員變數改寫為方法內的區域性變數)以及手動設定null值。

    至於作用域,需要在我們編寫程式碼時多注意;null值的手動設定,我們可以看一下Java容器LinkedList原始碼(可參考:Java之LinkedList原始碼解讀(JDK 1.8))的刪除指定節點的內部方法:

  1. //刪除指定節點並返回被刪除的元素值
  2. E unlink(Node<E> x){
  3. //獲取當前值和前後節點
  4. final E element = x.item;
  5. finalNode<E>next= x.next;
  6. finalNode<E> prev = x.prev;
  7. if(prev ==null){
  8. first =next;//如果前一個節點為空(如當前節點為首節點),後一個節點成為新的首節點
  9. }else{
  10. prev.next=next;//如果前一個節點不為空,那麼他先後指向當前的下一個節點
  11. x.prev =null;
  12. }
  13. if(next==null){
  14. last= prev;//如果後一個節點為空(如當前節點為尾節點),當前節點前一個成為新的尾節點
  15. }else{
  16. next.prev = prev;//如果後一個節點不為空,後一個節點向前指向當前的前一個節點
  17. x.next=null;
  18. }
  19. x.item =null;
  20. size--;
  21. modCount++;
  22. return element;
  23. }

    除了修改節點間的關聯關係,我們還要做的就是賦值為null的操作,不管GC何時會開始清理,我們都應及時的將無用的物件標記為可被清理的物件。

    我們知道Java容器ArrayList是陣列實現的(可參考:Java之ArrayList原始碼解讀(JDK 1.8)),如果我們要為其寫一個pop()(彈出)方法,可能會是這樣:

  1. public E pop(){
  2. if(size ==0)
  3. returnnull;
  4. else
  5. return(E) elementData[--size];
  6. }

    寫法很簡潔,但這裡卻會造成記憶體溢位:elementData[size-1]依然持有E型別物件的引用,並且暫時不能被GC回收。我們可以如下修改:

  1. public E pop(){
  2. if(size ==0)
  3. returnnull;
  4. else{
  5. E e =(E) elementData[--size];
  6. elementData[size]=null;
  7. return e;
  8. }
  9. }

    我們寫程式碼並不能一味的追求簡潔,首要是保證其正確性。

    容器使用時的記憶體洩露

    在很多文章中可能看到一個如下記憶體洩露例子:

  1. Vector v =newVector();
  2. for(int i =1; i<100; i++)
  3. {
  4. Object o =newObject();
  5. v.add(o);
  6. o =null;
  7. }

    可能很多人一開始並不理解,下面我們將上面的程式碼完整一下就好理解了:

  1. void method(){
  2. Vector vector =newVector();
  3. for(int i =1; i<100; i++)
  4. {
  5. Objectobject=newObject();
  6. vector.add(object);
  7. object=null;
  8. }
  9. //...對vector的操作
  10. //...與vector無關的其他操作
  11. }

    這裡記憶體洩露指的是在對vector操作完成之後,執行下面與vector無關的程式碼時,如果發生了GC操作,這一系列的object是沒法被回收的,而此處的記憶體洩露可能是短暫的,因為在整個method()方法執行完成後,那些物件還是可以被回收。這裡要解決很簡單,手動賦值為null即可:

  1. void method(){
  2. Vector vector =newVector();
  3. for(int i =1; i<100; i++)
  4. {
  5. Objectobject=newObject();
  6. vector.add(object);
  7. object=null;
  8. }
  9. //...對v的操作
  10. vector =null;
  11. //...與v無關的其他操作
  12. }

    上面Vector已經過時了,不過只是使用老的例子來做記憶體洩露的介紹。我們使用容器時很容易發生記憶體洩露,就如上面的例子,不過上例中,容器時方法內的區域性變數,造成的記憶體洩漏影響可能不算很大(但我們也應該避免),但是,如果這個容器作為一個類的成員變數,甚至是一個靜態(static)的成員變數時,就要更加註意記憶體洩露了。

    下面也是一種使用容器時可能會發生的錯誤:

  1. publicclassCollectionMemory{
  2. publicstaticvoid main(String s[]){
  3. Set<MyObject> objects =newLinkedHashSet<MyObject>();
  4. objects.add(newMyObject());
  5. objects.add(newMyObject());
  6. objects.add(newMyObject());
  7. System.out.println(objects.size());
  8. while(true){
  9. objects.add(newMyObject());
  10. }
  11. }
  12. }
  13. classMyObject{
  14. //設定預設陣列長度為99999更快的發生OutOfMemoryError
  15. List<String> list =newArrayList<>(99999);
  16. }

    執行上面的程式碼將很快報錯:

  1. 3
  2. Exceptionin thread "main" java.lang.OutOfMemoryError:Java heap space
  3. at java.util.ArrayList.<init>(ArrayList.java:152)
  4. at com.anxpp.memory.MyObject.<init>(CollectionMemory.java:21)
  5. at com.anxpp.memory.CollectionMemory.main(CollectionMemory.java:16)

    如果足夠了解Java的容器,上面的錯誤是不可能發生的。這裡也推薦一篇本人介紹Java容器的文章:...

    容器Set只存放唯一的元素,是通過物件的equals()方法來比較的,但是Java中所有類都直接或間接繼承至Object類,Object類的equals()方法比較的是物件的地址,上例中,就會一直新增元素直到記憶體溢位。

    所以,上例嚴格的說是容器的錯誤使用導致的記憶體溢位。

    就Set而言,remove()方法也是通過equals()方法來刪除匹配的元素的,如果一個物件確實提供了正確的equals()方法,但是切記不要在修改這個物件後使用remove(Object o),這也可能會發生記憶體洩露。

    各種提供了close()方法的物件

    比如資料庫連線(dataSourse.getConnection()),網路連線(socket)和io連線,以及使用其他框架的時候,除非其顯式的呼叫了其close()方法(或類似方法)將其連線關閉,否則是不會自動被GC回收的。其實原因依然是長生命週期物件持有短生命週期物件的引用。

    可能很多人使用過

相關推薦

JAVA 記憶體洩露原因例子解決

  Java的一個重要特性就是通過垃圾收集器(GC)自動管理記憶體的回收,而不需要程式設計師自己來釋放記憶體。理論上Java中所有不會再被利用的物件所佔用的記憶體,都可以被GC回收,但是Java也存在記憶體洩露,但它的表現與C++不同。 JAVA 中的記憶體管理

JAVA 記憶體洩露-值得收藏的好文

非常好的文章, 轉載自:http://blog.csdn.net/anxpp/article/details/51325838     Java的一個重要特性就是通過垃圾收集器(GC)自動管理記憶體的回收,而不需要程式設計師自己來釋放記憶體。理論上Java

java正則表示式匹配切割和替換

正則表示式:符合一定規則的表示式。作用:用於專門操作字串。特點:用於一些特定的符號來表示一些程式碼操作,這樣就簡化書寫。所以學習正則表示式,就是在學習一些特殊符號的使用。好處:可以簡化對字串的複雜操作。弊端:符號定義越多,正則越長,閱讀性越差。 具體操作功能: 1,匹配:

Java記憶體機制new操作的執行原理

1.Java的記憶體機制  Java 把記憶體劃分成兩種:一種是棧記憶體,另一種是堆記憶體。在函式中定義的一些基本型別的變數和物件的引用變數都是在函式的棧記憶體中分配,當在一段程式碼塊定義一個變數時,Java 就在棧中為這個變數分配記憶體空間,當超過變數的作用域後(比如,在函式A中呼叫函式B,在函式B中定義變

Java中CAS悲觀鎖與樂觀鎖

前言:在JDK1.5之前Java語言是靠synchronized關鍵字保證同步的,這會導致有鎖鎖機制存在以下問題: (1)在多執行緒競爭下,加鎖、釋放鎖會導致比較多的上下文切換和排程延時,引起效能問題。 (2)一個執行緒持有鎖會導致其它所有需要此鎖的執行緒掛

javaScript -- touch事件touchstarttouchmove和touchend

HTML5中新添加了很多事件,但是由於他們的相容問題不是很理想,應用實戰性不是太強,所以在這裡基本省略,咱們只分享應用廣泛相容不錯的事件,日後隨著相容情況提升以後再陸續新增分享。今天為大家介紹的事件主要是觸控事件:touchstart、touchmove和tou

【C++】動態記憶體分配new/new[]和delete/delete[]

一、為什麼需要動態記憶體分配? 在C++程式中,所有記憶體需求都是在程式執行之前通過定義所需的變數來確定的。 但是可能存在程式的記憶體需求只能在執行時確定的情況。 例如,當需要的記憶體取決於使用者輸入。 在這些情況下,程式需要動態分配記憶體,C ++語言將運算子new和de

CGI原理,配置訪問

一.基本原理 CGI:通用閘道器介面(Common Gateway Interface)是一個Web伺服器主機提供資訊服務的標準介面。通過CGI介面,Web伺服器就能夠獲取客戶端提交的資訊,轉交給伺服器端的CGI程式進行處理,最後返回結果給客戶端。 組成CGI通訊系統的是兩

【深入Java虛擬機器】之記憶體區域Eden SpaceSurvivor SpaceOld GenCode Cache和Perm Gen

1.記憶體區域劃分 限定商用虛擬機器基本都採用分代收集演算法進行垃圾回收。根據物件的生命週期的不同將記憶體劃分為幾塊,然後根據各塊的特點採用最適當的收集演算法。大批物件死去、少量物件存活的,使用複製演算法,複製成本低;物件存活率高、沒有額外空間進行分配擔保的,採用標記-清除演算法

Java四大域物件ServletContextSessionRequestpageContext域物件

一、ServletContext 1、生命週期:當Web應用被載入進容器時建立代表整個web應用的ServletContext物件,當伺服器關閉或Web應用被移除時,ServletContext物件跟著銷燬。 2、作用範圍:整個Web應用。 3、作用:

Java記憶體分配(堆記憶體記憶體常量池)

  Java程式是執行在JVM(Java虛擬機器)上的,因此Java的記憶體分配是在JVM中進行的,JVM是記憶體分配的基礎和前提。Java程式的執行會涉及以下的記憶體區域: 1. 暫存器:JVM內部虛擬暫存器,存取速度非常快,程式不可控制。 2. 棧:存放

Java 原子操作類AtomicIntegerAtomicIntegerArray等

當程式更新一個變數時,如果多執行緒同時更新這個變數,可能得到期望之外的值,比如變數i=1,A執行緒更新i+1,B執行緒也更新i+1,經過兩個執行緒操作之後可能i不等於3,而是等於2。因為A和B執行緒在更新變數i的時候拿到的i都是1,這就是執行緒不安全的更新操作,通常我們會使

JVM記憶體區域Eden SpaceSurvivor SpaceOld GenCode Cache和Perm Gen

JVM區域總體分兩類,heap區和非heap區。 heap區又分為: Eden Space(伊甸園)、 Survivor Space(倖存者區)、 Old Gen(老年代)。 非heap區又分: Code Cache(程式碼快取區); Perm Gen(永

java 泛型普通泛型 萬用字元 泛型介面,泛型陣列,泛型方法,泛型巢狀

JDK1.5 令我們期待很久,可是當他釋出的時候卻更換版本號為5.0。這說明Java已經有大幅度的變化。本文將講解JDK5.0支援的新功能-----Java的泛型. 1、Java泛型  其實Java

Java虛擬機器------執行時記憶體結構

  首先通過一張圖瞭解 Java程式的執行流程:      我們編寫好的Java原始碼程式,通過Java編譯器javac編譯成Java虛擬機器識別的class檔案(位元組碼檔案),然後由 JVM 中的類載入器載入編譯生成的位元組碼檔案,載入完畢之後再由 JVM 執行引擎去執行。在載入完畢到執行過程中,J

Java虛擬機器------記憶體分配

  我們說Java是自動進行記憶體管理的,所謂自動化就是,不需要程式設計師操心,Java會自動進行記憶體分配和記憶體回收這兩方面。   前面我們介紹過如何通過垃圾回收器來回收記憶體,那麼本篇部落格我們來聊聊如何進行分配記憶體。   物件的記憶體分配,往大方向上講,就是堆上進行分配(但也有可能經過JIT編譯

java.util包——Connection接口

操作 相同 元素 叠代 cat roo soft true nbsp Connection接口介紹   Connection接口是java集合的root接口,沒有實現類,只有子接口和實現子接口的各種容器。主要用來表示java集合這一大的抽象概念。   Connection接

轉-Linux啟動過程inittabrc.sysinitrcX.drc.local

dha mage 模塊 都是 交換 如何配置 mas 完全 打開 http://blog.chinaunix.net/space.php?uid=10167808&do=blog&id=26042 1)BIOS自檢2)啟動Grub/Lilo3)加載內

Java技術——Java泛型

cal 5.1 try 既然 參數 top 兩種 泛型編程 編譯器 1.為什麽需要泛型轉載請註明出處:http://blog.csdn.net/seu_calvin/article/details/52230032泛型在Java中有很重要的地位,網上很多文章羅列各種理論,不

java類型轉換自動轉換和強制轉換

代碼 oid 高精 log 相加 println 類型轉換詳解 範圍 void 自動轉換 class Hello { public static void main(String[] args) { //自動轉換 int a = 5; byte b = 6