❶ 怎麼排查這些內存泄漏
最原始的內存泄露測試
重復多次操作關鍵的可疑的路徑,從內存監控工具中觀察內存曲線,是否存在不斷上升的趨勢且不會在程序返回時明顯回落。
這種方式可以發現最基本,也是最明顯的內存泄露問題,對用戶價值最大,操作難度小,性價比極高。
MAT內存分析工具
2.1 MAT分析heap的總內存佔用大小來初步判斷是否存在泄露
在Devices 中,點擊要監控的程序。
點擊Devices視圖界面中最上方一排圖標中的「Update Heap」
點擊Heap視圖
點擊Heap視圖中的「Cause GC」按鈕
到此為止需檢測的進程就可以被監視。Heap視圖中部有一個Type叫做data object,即數據對象,也就是我們的程序中大量存在的類類型的對象。在data object一行中有一列是「Total Size」,其值就是當前進程中所有java數據對象的內存總量,一般情況下,這個值的大小決定了是否會有內存泄漏。可以這樣判斷:
進入某應用,不斷的操作該應用,同時注意觀察data object的Total Size值,正常情況下Total Size值都會穩定在一個有限的范圍內,也就是說由於程序中的的代碼良好,沒有造成對象不被垃圾回收的情況。
所以說雖然我們不斷的操作會不斷的生成很多對象,而在虛擬機不斷的進行GC的過程中,這些對象都被回收了,內存佔用量會會落到一個穩定的水平;反之如果代碼中存在沒有釋放對象引用的情況,則data object的Total Size值在每次GC後不會有明顯的回落。隨著操作次數的增多Total Size的值會越來越大,直到到達一個上限後導致進程被殺掉。
2.2 MAT分析hprof來定位內存泄露的原因所在。
這是出現內存泄露後使用MAT進行問題定位的有效手段。
A)Dump出內存泄露當時的內存鏡像hprof,分析懷疑泄露的類:
B)分析持有此類對象引用的外部對象
C)分析這些持有引用的對象的GC路徑
D)逐個分析每個對象的GC路徑是否正常
從這個路徑可以看出是一個antiRadiationUtil工具類對象持有了MainActivity的引用導致MainActivity無法釋放。此時就要進入代碼分析此時antiRadiationUtil的引用持有是否合理(如果antiRadiationUtil持有了MainActivity的context導致節目退出後MainActivity無法銷毀,那一般都屬於內存泄露了)。
2.3 MAT對比操作前後的hprof來定位內存泄露的根因所在。
為查找內存泄漏,通常需要兩個 Dump結果作對比,打開 Navigator History面板,將兩個表的 Histogram結果都添加到 Compare Basket中去
A) 第一個HPROF 文件(usingFile > Open Heap Dump ).
B)打開Histogram view.
C)在NavigationHistory view里 (如果看不到就從Window >show view>MAT- Navigation History ), 右擊histogram然後選擇Add to Compare Basket .
D)打開第二個HPROF 文件然後重做步驟2和3.
E)切換到Compare Basket view, 然後點擊Compare the Results (視圖右上角的紅色」!」圖標)。
F)分析對比結果
可以看出兩個hprof的數據對象對比結果。
通過這種方式可以快速定位到操作前後所持有的對象增量,從而進一步定位出當前操作導致內存泄露的具體原因是泄露了什麼數據對象。
注意:
如果是用 MAT Eclipse 插件獲取的 Dump文件,不需要經過轉換則可在MAT中打開,Adt會自動進行轉換。
而手機SDk Dump 出的文件要經過轉換才能被 MAT識別,Android SDK提供了這個工具 hprof-conv (位於 sdk/tools下)
首先,要通過控制台進入到你的 android sdk tools 目錄下執行以下命令:
./hprof-conv xxx-a.hprof xxx-b.hprof
例如 hprof-conv input.hprof out.hprof
此時才能將out.hprof放在eclipse的MAT中打開。
手機管家內存泄露每日監控方案
目前手機管家的內存泄露每日監控會自動運行並輸出是否存在疑似泄露的報告郵件,不論泄露對象的大小。這其中涉及的核心技術主要是AspectJ,MLD自研工具(原理是虛引用)和UIAutomator。
3.1 AspectJ插樁監控代碼
手機管家目前使用一個ant腳本加入MLD的監控代碼,並通過AspectJ的語法實現插樁。
使用AspectJ的原因是可以靈活分離出項目源碼與監控代碼,通過不同的編譯腳本打包出不同用途的安裝測試包:如果測試包是經過Aspect插樁了MLD監控代碼的話,那麼運行完畢後會輸出指定格式的日誌文件,作為後續分析工作的數據基礎。
3.2 MLD實現監控核心邏輯
這是手機管家內的一個工具工程,正式打包不會打入,BVT等每日監控測試包可以打入。打入後可以通過諸如addObject介面(通過反射去檢查是否含有該工具並調用)來加入需要監控的檢測對象,這個工具會自動在指定時機(如退出管家)去檢測該對象是否發生泄漏。
這個內存泄露檢測的基本原理是:
虛引用主要用來跟蹤對象被垃圾回收器回收的活動。虛引用必須和引用隊列(ReferenceQueue)聯合使用(在虛引用函數就必須關聯指定)。當垃圾回收器准備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,自動把這個虛引用加入到與之關聯的引用隊列中。程序可以通過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。
基於以上原理,MLD工具在調用介面addObject加入監控類型時,會為該類型對象增加一個虛引用,注意虛引用並不會影響該對象被正常回收。因此可以在ReferenceQueue引用隊列中統計未被回收的監控對象是否超過指定閥值。
利用PhantomReferences(虛引用)和ReferenceQueue(引用隊列),當PhantomReferences被加入到相關聯的ReferenceQueue時,則視該對象已經或處於垃圾回收器回收階段了。
MLD監控原理核心
目前手機管家已對大部分類完成內存泄露的監控,包括各種activity,service和view頁面等,務求在技術上能帶給用戶最順滑的產品體驗。
接下來簡單介紹下這個工具的判斷核心。根據虛引用監控到的內存狀態,需要通過多種策略來判斷是否存在內存泄露。
(1)最簡單的方式就是直接在加入監控時就為該類型設定最大存在個數,舉個例子,各個DAO對象理論上只能存在最多一個,因此一旦出現兩個相同的DAO,那一般都是泄露了;
(2)第二種情況是在頁面退出程序退出時,檢索gc後無法釋放的對象列表,這些對象類型也會成為內存泄露的懷疑對象;
(3)最後一種情況比較復雜,基本原理是根據歷史操作判斷對象數量的增長幅度。根據對象的增長通過最小二乘法擬合出該對象類型的增長速度,如果超過經驗值則會列入疑似泄露的對象列表。
3.3 UIAutomator完成重復操作的自動化
最後一步就很簡單了。這么多反復的UI操作,讓人工來點就太浪費人力了。我們使用UIAutomator來進行自動化操作測試。
目前手機管家的每日自動化測試已覆蓋各個功能的主路徑,並通過配置文件的方式來靈活驅動用例的增刪改查,最大限度保證了隨著版本推移用例的復用價值。
至此手機管家的內存泄露測試方案介紹完畢,也歡迎各路牛人交流溝通更多更強的內存泄露工具盒方案!
騰訊Bugly簡介
Bugly是騰訊內部產品質量監控平台的外發版本,其主要功能是App發布以後,對用戶側發生的Crash以及卡頓現象進行監控並上報,讓開發同學可以第一時間了解到App的質量情況,及時機型修改。目前騰訊內部所有的產品,均在使用其進行線上產品的崩潰監控。
❷ 怎麼查看java程序是否有內存泄露
一般來說內存泄漏有兩種情況。一種情況,在堆中的分配的內存,在沒有將其釋放掉的時候,就將所有能訪問這塊內存的方式都刪掉(如指針重新賦值);另一種情況則是在內存對象明明已經不需要的時候,還仍然保留著這塊內存和它的訪問方式(引用)。第一種情況,在Java中已經由於垃圾回收機制的引入,得到了很好的解決。所以,Java中的內存泄漏,主要指的是第二種情況。
可能光說概念太抽象了,大家可以看一下這樣的例子:
1 Vector v=new Vector(10);
2 for (int i=1;i<100; i++){
3 Object o=new Object();
4 v.add(o);
5 o=null;
6 }
在這個例子中,代碼棧中存在Vector對象的引用v和Object對象的引用o。在For循環中,我們不斷的生成新的對象,然後將其添加到Vector對象中,之後將o引用置空。問題是當o引用被置空後,如果發生GC,我們創建的Object對象是否能夠被GC回收呢?答案是否定的。因為,GC在跟蹤代碼棧中的引用時,會發現v引用,而繼續往下跟蹤,就會發現v引用指向的內存空間中又存在指向Object對象的引用。也就是說盡管o引用已經被置空,但是Object對象仍然存在其他的引用,是可以被訪問到的,所以GC無法將其釋放掉。如果在此循環之後,Object對象對程序已經沒有任何作用,那麼我們就認為此Java程序發生了內存泄漏。
盡管對於C/C++中的內存泄露情況來說,Java內存泄露導致的破壞性小,除了少數情況會出現程序崩潰的情況外,大多數情況下程序仍然能正常運行。但是,在移動設備對於內存和CPU都有較嚴格的限制的情況下,Java的內存溢出會導致程序效率低下、佔用大量不需要的內存等問題。這將導致整個機器性能變差,嚴重的也會引起拋出OutOfMemoryError,導致程序崩潰。
❸ 如何定位java內存泄露
1、為什麼會發生內存泄漏
Java如何檢測內在泄漏呢?我們需要一些工具進行檢測,並發現內存泄漏問題,不然很容易發生down機問題。
編寫java程序最為方便的地方就是我們不需要管理內存的分配和釋放,一切由jvm來進行處理,當java對象不再被應用時,等到堆內存不夠用時,jvm會進行垃圾回收,清除這些對象佔用的堆內存空間,如果對象一直被應用,jvm無法對其進行回收,創建新的對象時,無法從Heap中獲取足夠的內存分配給對象,這時候就會導致內存溢出。而出現內存泄露的地方,一般是不斷的往容器中存放對象,而容器沒有相應的大小限制或清除機制。容易導致內存溢出。
當伺服器應用佔用了過多內存的時候,如何快速定位問題呢?現在,Eclipse MAT的出現使這個問題變得非常簡單。EclipseMAT是著名的SAP公司貢獻的一個工具,可以在Eclipse網站下載到它,完全免費的。
要定位問題,首先你需要獲取伺服器jvm某刻內存快照。jdk自帶的jmap可以獲取內存某一時刻的快照,導出為dmp文件後,就可以用Eclipse MAT來分析了,找出是那個對象使用內存過多。
2、內存泄漏的現象:
常常地,程序內存泄漏的最初跡象發生在出錯之後,在你的程序中得到一個OutOfMemoryError。這種典型的情況發生在產品環境中,而在那裡,你希望內存泄漏盡可能的少,調試的可能性也達到最小。也許你的測試環境和產品的系統環境不盡相同,導致泄露的只會在產品中暴露。這種情況下,你需要一個低負荷的工具來監聽和尋找內存泄漏。同時,你還需要把這個工具同你的系統聯系起來,而不需要重新啟動他或者機械化你的代碼。也許更重要的是,當你做分析的時候,你需要能夠同工具分離而使得系統不會受到干擾。
一個OutOfMemoryError常常是內存泄漏的一個標志,有可能應用程序的確用了太多的內存;這個時候,你既不能增加JVM的堆的數量,也不能改變你的程序而使得他減少內存使用。但是,在大多數情況下,一個OutOfMemoryError是內存泄漏的標志。一個解決辦法就是繼續監聽GC的活動,看看隨時間的流逝,內存使用量是否會增加,如果有,程序中一定存在內存泄漏。
3、發現內存泄漏
1. jstat -gc pid
可以顯示gc的信息,查看gc的次數,及時間。
其中最後五項,分別是young gc的次數,young gc的時間,full gc的次數,full gc的時間,gc的總時間。
2.jstat -gccapacity pid
可以顯示,VM內存中三代(young,old,perm)對象的使用和佔用大小,
如:PGCMN顯示的是最小perm的內存使用量,PGCMX顯示的是perm的內存最大使用量,
PGC是當前新生成的perm內存佔用量,PC是但前perm內存佔用量。
其他的可以根據這個類推,OC是old內純的佔用量。
3.jstat -gcutil pid
統計gc信息統計。
4.jstat -gcnew pid
年輕代對象的信息。
5.jstat -gcnewcapacity pid
年輕代對象的信息及其佔用量。
6.jstat -gcold pid
old代對象的信息。
7.stat -gcoldcapacity pid
old代對象的信息及其佔用量。
8.jstat -gcpermcapacity pid
perm對象的信息及其佔用量。
9.jstat -class pid
顯示載入class的數量,及所佔空間等信息。
10.jstat -compiler pid
顯示VM實時編譯的數量等信息。
11.stat -printcompilation pid
當前VM執行的信息。
一些術語的中文解釋:
S0C:年輕代中第一個survivor(倖存區)的容量(位元組)
S1C:年輕代中第二個survivor(倖存區)的容量(位元組)
S0U:年輕代中第一個survivor(倖存區)目前已使用空間(位元組)
S1U:年輕代中第二個survivor(倖存區)目前已使用空間(位元組)
EC:年輕代中Eden(伊甸園)的容量(位元組)
EU:年輕代中Eden(伊甸園)目前已使用空間(位元組)
OC:Old代的容量(位元組)
OU:Old代目前已使用空間(位元組)
PC:Perm(持久代)的容量(位元組)
PU:Perm(持久代)目前已使用空間(位元組)
YGC:從應用程序啟動到采樣時年輕代中gc次數
YGCT:從應用程序啟動到采樣時年輕代中gc所用時間(s)
FGC:從應用程序啟動到采樣時old代(全gc)gc次數
FGCT:從應用程序啟動到采樣時old代(全gc)gc所用時間(s)
GCT:從應用程序啟動到采樣時gc用的總時間(s)
NGCMN:年輕代(young)中初始化(最小)的大小(位元組)
NGCMX:年輕代(young)的最大容量(位元組)
NGC:年輕代(young)中當前的容量(位元組)
OGCMN:old代中初始化(最小)的大小(位元組)
OGCMX:old代的最大容量(位元組)
OGC:old代當前新生成的容量(位元組)
PGCMN:perm代中初始化(最小)的大小(位元組)
PGCMX:perm代的最大容量(位元組)
PGC:perm代當前新生成的容量(位元組)
S0:年輕代中第一個survivor(倖存區)已使用的占當前容量百分比
S1:年輕代中第二個survivor(倖存區)已使用的占當前容量百分比
E:年輕代中Eden(伊甸園)已使用的占當前容量百分比
O:old代已使用的占當前容量百分比
P:perm代已使用的占當前容量百分比
S0CMX:年輕代中第一個survivor(倖存區)的最大容量(位元組)
S1CMX:年輕代中第二個survivor(倖存區)的最大容量(位元組)
ECMX:年輕代中Eden(伊甸園)的最大容量(位元組)
DSS:當前需要survivor(倖存區)的容量(位元組)(Eden區已滿)
TT:持有次數限制
MTT:最大持有次數限制
如果定位內存泄漏問題我一般使用如下命令:
Jstat -gcutil15469 2500 70
其中深藍色的部分就為內存泄漏的部分,java的堆內存一共只有481.5M而內存泄漏的部分獨自佔有了336.2M所以本次的內存泄漏很明顯,那麼我就來看看那個方法導致的內存泄漏:
從上圖我們可以發現紅線圈著的方法佔用了堆內存的67.75%,如果能把這個測試結果交給開發,開發是不是應該很好定位呢。所以作為一名高級測試工程師,我們需要學習的東西太多。
雖然不確定一定是內存泄漏,但是可以准確的告訴開發問題出現的原因,有一定的說服力。
❹ java是否有內存泄露和內存溢出
java中的內存溢出和內存泄漏
內存溢出:
對於整個應用程序來說,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
有兩種最基本的內存收集方式:一種稱為ing或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回收,這樣在一定程度上是可以避免內存溢出問題的。
❺ java中內存泄露有幾種如何分析泄露原因
一、Java內存回收機制
不論哪種語言的內存分配方式,都需要返回所分配內存的真實地址,也就是返回一個指針到內存塊的首地址。Java中對象是採用new或者反射的方法創建的,這些對象的創建都是在堆(Heap)中分配的,所有對象的回收都是由Java虛擬機通過垃圾回收機制完成的。GC為了能夠正確釋放對象,會監控每個對象的運行狀況,對他們的申請、引用、被引用、賦值等狀況進行監控,Java會使用有向圖的方法進行管理內存,實時監控對象是否可以達到,如果不可到達,則就將其回收,這樣也可以消除引用循環的問題。在Java語言中,判斷一個內存空間是否符合垃圾收集標准有兩個:一個是給對象賦予了空值null,以下再沒有調用過,另一個是給對象賦予了新值,這樣重新分配了內存空間。
二、Java內存泄露引起原因
首先,什麼是內存泄露?經常聽人談起內存泄露,但要問什麼是內存泄露,沒幾個說得清楚。內存泄露是指無用對象(不再使用的對象)持續佔有內存或無用對象的內存得不到及時釋放,從而造成的內存空間的浪費稱為內存泄露。內存泄露有時不嚴重且不易察覺,這樣開發者就不知道存在內存泄露,但有時也會很嚴重,會提示你Out of memory。
那麼,Java內存泄露根本原因是什麼呢?長生命周期的對象持有短生命周期對象的引用就很可能發生內存泄露,盡管短生命周期對象已經不再需要,但是因為長生命周期對象持有它的引用而導致不能被回收,這就是java中內存泄露的發生場景。具體主要有如下幾大類:
1、靜態集合類引起內存泄露:
像HashMap、Vector等的使用最容易出現內存泄露,這些靜態變數的生命周期和應用程序一致,他們所引用的所有的對象Object也不能被釋放,因為他們也將一直被Vector等引用著。
例:
Static Vector v = new Vector(10);
for (int i = 1; i<100; i++)
{
Object o = new Object();
v.add(o);
o = null;
}//
在這個例子中,循環申請Object 對象,並將所申請的對象放入一個Vector 中,如果僅僅釋放引用本身(o=null),那麼Vector 仍然引用該對象,所以這個對象對GC 來說是不可回收的。因此,如果對象加入到Vector 後,還必須從Vector 中刪除,最簡單的方法就是將Vector對象設置為null。
2、當集合裡面的對象屬性被修改後,再調用remove()方法時不起作用。
例:
public static void main(String[] args)
{
Set<Person> set = new HashSet<Person>();
Person p1 = new Person("唐僧","pwd1",25);
Person p2 = new Person("孫悟空","pwd2",26);
Person p3 = new Person("豬八戒","pwd3",27);
set.add(p1);
set.add(p2);
set.add(p3);
System.out.println("總共有:"+set.size()+" 個元素!"); //結果:總共有:3 個元素!
p3.setAge(2); //修改p3的年齡,此時p3元素對應的hashcode值發生改變
set.remove(p3); //此時remove不掉,造成內存泄漏
set.add(p3); //重新添加,居然添加成功
System.out.println("總共有:"+set.size()+" 個元素!"); //結果:總共有:4 個元素!
for (Person person : set)
{
System.out.println(person);
}
}
3、監聽器
在java 編程中,我們都需要和監聽器打交道,通常一個應用當中會用到很多監聽器,我們會調用一個控制項的諸如addXXXListener()等方法來增加監聽器,但往往在釋放對象的時候卻沒有記住去刪除這些監聽器,從而增加了內存泄漏的機會。
4、各種連接
比如資料庫連接(dataSourse.getConnection()),網路連接(socket)和io連接,除非其顯式的調用了其close()方法將其連接關閉,否則是不會自動被GC 回收的。對於Resultset 和Statement 對象可以不進行顯式回收,但Connection 一定要顯式回收,因為Connection 在任何時候都無法自動回收,而Connection一旦回收,Resultset 和Statement 對象就會立即為NULL。但是如果使用連接池,情況就不一樣了,除了要顯式地關閉連接,還必須顯式地關閉Resultset Statement 對象(關閉其中一個,另外一個也會關閉),否則就會造成大量的Statement 對象無法釋放,從而引起內存泄漏。這種情況下一般都會在try裡面去的連接,在finally裡面釋放連接。
5、內部類和外部模塊等的引用
內部類的引用是比較容易遺忘的一種,而且一旦沒釋放可能導致一系列的後繼類對象沒有釋放。此外程序員還要小心外部模塊不經意的引用,例如程序員A 負責A 模塊,調用了B 模塊的一個方法如:
public void registerMsg(Object b);
這種調用就要非常小心了,傳入了一個對象,很可能模塊B就保持了對該對象的引用,這時候就需要注意模塊B 是否提供相應的操作去除引用。
6、單例模式
不正確使用單例模式是引起內存泄露的一個常見問題,單例對象在被初始化後將在JVM的整個生命周期中存在(以靜態變數的方式),如果單例對象持有外部對象的引用,那麼這個外部對象將不能被jvm正常回收,導致內存泄露,考慮下面的例子:
class A{
public A(){
B.getInstance().setA(this);
}
....
}
//B類採用單例模式
class B{
private A a;
private static B instance=new B();
public B(){}
public static B getInstance(){
return instance;
}
public void setA(A a){
this.a=a;
}
//getter...
}
顯然B採用singleton模式,它持有一個A對象的引用,而這個A類的對象將不能被回收。想像下如果A是個比較復雜的對象或者集合類型會發生什麼情況