簡單點說,當一個對象不再被使用(即失去了利用價值),但它的引用卻依然被其他對象所持有,導致JVM的垃圾回收機制無法回收釋放此對象,則該無用對象繼續占用內存空間(占著茅坑。。。),即內存泄漏了。所有內存都被占用且無法進行垃圾回收時,就會發生內存溢出。
全面點說,那要從java虛擬機運行時的數據區域說起,《深入理解java虛擬機:JVM高級特性與最佳實踐》這本書對此闡釋很明確,jvm所管理的內存將會包括以下幾個運行時的數據區域:

這幾個數據區除程序計數器外都會發生內存溢出問題(內存泄露的后果是內存溢出,但內存溢出并不都是有內存泄露引起的)。
程序計數器是一塊較小的內存空間,它可以看作是當前線程執行字節碼的行號指示器,在虛擬機的概念模型里,字節碼解釋器的工作就是通過改變程序計數器的值來選去下一條執行的字節碼指令,分之,循環,跳轉,異常處理,線程恢復等基礎功能都需要依賴這個程序計數器來完成。每條線程都持有一個獨立的程序計數器,各個計數器之間互不影響獨立存儲,這類內存區域被稱為“線程私有”內存。 如果執行java指令,此計數器中存有當前執行的字節碼指令的行數,若執行native方法,則這個計數器的值為空(Undefined)。此內存區域是唯一一個jvm規范沒有規定OutOfMemoryError的區域。
虛擬機棧也是一塊兒線程私有內存,它描述的是java方法執行的內存模型:每個方法在執行的時候會創建一個幀棧,這個幀棧用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息,每一個方法調用到執行完的過程就是幀棧從虛擬機棧入棧到出棧的過程。我們常說的“堆棧”中的棧就是指虛擬機棧,更精確的說是指虛擬機棧中存儲的局部變量表。局部變量表中存儲了編譯期可知的基本數類型,對象的引用,以及returnAddress類型——指向一條字節碼指令的地址。(long和double占兩個Slot——局部變量空間,其他類型占一個Slot) 當進入一個方法是,該方法對應的局部變量空間是確定了的,并且在方法執行期間不會改變。在jvm規范中,對這個區域規定了兩種異常狀況:如果請求的棧深度大于虛擬機所允許的最大深度,會拋出StackOverflowError;如果虛擬機可以動態擴展,并且在擴展時無法申請到足夠內存,則或拋出OutOfMemoryError。
本地方法棧同虛擬機棧功能類似,都是為了執行方法而服務的,只不過本地方法棧是服務于Native方法,jvm并沒有對此區域做具體的規定,同虛擬機棧一樣本地方法棧也會拋出上文兩個異常。
java堆我們很熟悉了,它是虛擬機管理的內存區域中最大的一塊,被所有線程所共享,在虛擬機啟動時被創建。堆存在的唯一目的是存儲對象的實例,但隨著技術的發展,堆的作用也不是那么“絕對”了。堆也是垃圾收集器管理的主要區域,jvm規范規定堆可以處于物理上不連續的內存空間中,只要邏輯上連續即可。堆的大小可以是固定的,也可以是擴展的,如果在堆中沒有完成實例分配,并且堆也無法在擴展時,將會拋出OutOfMemoryError異常。
方法區也是一塊被線程共享的區域,它用于存儲已經被虛擬機掛在的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據,雖然Jvm規范把方法區描述為堆的一個邏輯部分,但他卻有一個別名叫做Non-Heap(非堆),目的應該是與java堆區分開來。jvm對方法區的規范較為寬松,也可以不實現垃圾收集,當方法區無法滿足內存分配需求時,會拋出OutOfMemoryError異常。
運行時常量池是方法區的一部分,用于存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載后進入方法區時存放。它同時受到方法區內存的限制,當其無法申請內存時會拋出OutOfMemoryError異常。
直接內存并不是虛擬機運行時數據區的一部分,但這部分內存也會被頻繁的使用,當jvm動態擴展時各區域總內存超過了物理內存限制,也會拋出OutOfMemoryError異常。
在Android開發中我們主要關心的是java堆中的內存泄漏問題,Android中常見的內存泄漏有一下幾個地方: 資源使用完畢沒有關閉:數據庫cursor,流等 靜態變量、單例持有對象的引用會使該對象無法銷毀 無限循環的屬性動畫也會使activity無法銷毀 context被生命周期常于activity的對象持有導致activity無法銷毀 集合類持有無用對象的引用 匿名內部類或非靜態內部類實例會持有外部類的引用,導致無法回收 handler引用被MessageQueue持有直到消息被送達
新聞熱點
疑難解答