8、內(nèi)存泄露和溢出場景及預(yù)防措施
內(nèi)存泄露(memory leak)袖肥,是指程序在申請內(nèi)存后障本,無法釋放已申請的內(nèi)存空間,即分配出去的內(nèi)存無法回收(不再使用的對象或者變量仍占內(nèi)存空間)橙困,在Java中內(nèi)存泄漏就是存在一些被分配的對象(可達(dá)的,卻是無用的)無法被gc回收耕餐。
內(nèi)存溢出(out of memory)凡傅,是指程序在申請內(nèi)存時(shí),沒有足夠的內(nèi)存空間供其使用肠缔,出現(xiàn)out of memory夏跷;比如申請了一個(gè)integer,但給它存了long才能存下的數(shù)明未,那就是內(nèi)存溢出槽华。可以看出內(nèi)存泄漏是內(nèi)存溢出的一種誘因趟妥,但不是唯一因素猫态。
memory leak會(huì)最終會(huì)導(dǎo)致out of memory!
Java判斷內(nèi)存空間是否符合垃圾回收標(biāo)準(zhǔn)有兩個(gè):給對象賦null且不再使用;給對象賦新值亲雪,重新分配內(nèi)存勇凭。
內(nèi)存泄漏的兩種情況:一是堆中申請的內(nèi)存沒釋放;二是對象已不再使用匆光,但還在內(nèi)存中保留著套像。
GC可以有效的解決第一種情況,但是無法保證情況二终息,所以Java存在的內(nèi)存泄漏主要是第二種夺巩。
以發(fā)生的方式來分類,內(nèi)存泄漏可以分為4類:
1. 常發(fā)性內(nèi)存泄漏周崭。發(fā)生內(nèi)存泄漏的代碼會(huì)被多次執(zhí)行到柳譬,每次被執(zhí)行的時(shí)候都會(huì)導(dǎo)致一塊內(nèi)存泄漏。
2. 偶發(fā)性內(nèi)存泄漏续镇。發(fā)生內(nèi)存泄漏的代碼只有在某些特定環(huán)境或操作過程下才會(huì)發(fā)生美澳。常發(fā)性和偶發(fā)性是相對的。對于特定的環(huán)境摸航,偶發(fā)性的也許就變成了常發(fā)性的制跟。所以測試環(huán)境和測試方法對檢測內(nèi)存泄漏至關(guān)重要。
3. 一次性內(nèi)存泄漏酱虎。發(fā)生內(nèi)存泄漏的代碼只會(huì)被執(zhí)行一次雨膨,或者由于算法上的缺陷,導(dǎo)致總會(huì)有一塊僅且一塊內(nèi)存發(fā)生泄漏读串。比如聊记,在類的構(gòu)造函數(shù)中分配內(nèi)存,在析構(gòu)函數(shù)中卻沒有釋放該內(nèi)存恢暖,所以內(nèi)存泄漏只會(huì)發(fā)生一次排监。
4. 隱式內(nèi)存泄漏。程序在運(yùn)行過程中不停的分配內(nèi)存杰捂,但是直到結(jié)束的時(shí)候才釋放內(nèi)存舆床。嚴(yán)格的說這里并沒有發(fā)生內(nèi)存泄漏,因?yàn)樽罱K程序釋放了所有申請的內(nèi)存嫁佳。但是對于一個(gè)服務(wù)器程序挨队,需要運(yùn)行幾天,幾周甚至幾個(gè)月脱拼,不及時(shí)釋放內(nèi)存也可能導(dǎo)致最終耗盡系統(tǒng)的所有內(nèi)存。所以坷备,我們稱這類內(nèi)存泄漏為隱式內(nèi)存泄漏熄浓。
從用戶使用程序的角度來看,內(nèi)存泄漏本身不會(huì)產(chǎn)生什么危害,作為一般的用戶赌蔑,根本感覺不到內(nèi)存泄漏的存在俯在。真正有危害的是內(nèi)存泄漏的堆積,這會(huì)最終消耗盡系統(tǒng)所有的內(nèi)存娃惯。從這個(gè)角度來說跷乐,一次性內(nèi)存泄漏并沒有什么危害,因?yàn)樗粫?huì)堆積趾浅,而隱式內(nèi)存泄漏危害性則非常大愕提,因?yàn)檩^之于常發(fā)性和偶發(fā)性內(nèi)存泄漏它更難被檢測到。
Java內(nèi)存泄漏的根本原因是什么呢皿哨?長生命周期的對象持有短生命周期對象的引用就很可能發(fā)生內(nèi)存泄漏浅侨,盡管短生命周期對象已經(jīng)不再需要,但是因?yàn)殚L生命周期持有它的引用而導(dǎo)致不能被回收证膨,這就是Java中內(nèi)存泄漏的發(fā)生場景如输。
具體主要有如下幾大類:
1、靜態(tài)集合類引起內(nèi)存泄露:
像HashMap央勒、Vector等的使用最容易出現(xiàn)內(nèi)存泄露不见,這些靜態(tài)變量的生命周期和應(yīng)用程序一致,他們所引用的所有的對象Object也不能被釋放崔步,因?yàn)樗麄円矊⒁恢北籚ector等引用著稳吮。
Static Vector v = new Vector(10);
for (int i = 1; i<100; i++)
{
????? Object o = newObject();
????? v.add(o);
????? o = null;
}
在這個(gè)例子中,循環(huán)申請Object 對象刷晋,并將所申請的對象放入一個(gè)Vector 中盖高,如果僅僅釋放引用本身(o=null),那么Vector 仍然引用該對象眼虱,所以這個(gè)對象對GC 來說是不可回收的喻奥。因此,如果對象加入到Vector 后捏悬,還必須從Vector 中刪除撞蚕,最簡單的方法就是將Vector對象設(shè)置為null。
2过牙、當(dāng)集合里面的對象屬性被修改后甥厦,再調(diào)用remove()方法時(shí)不起作用。
public class MainTest
{
??? public static void main(String[] args)
??? {
??????? MainTestmain = new MainTest();
??????? Setset = newHashSet<Person>();
??????? Personp1 = main.new Person("唐僧", 25);
??????? Personp2 = main.new Person("孫悟空", 26);
??????? Personp3 = main.new Person("豬八戒", 27);
??????? set.add(p1);
??????? set.add(p2);
??????? set.add(p3);
??????? System.out.println("總共有:" + set.size() + " 個(gè)元素!"); // 結(jié)果:總共有:3 個(gè)元素!
??????? p3.setAge(2); // 修改p3的年齡,此時(shí)p3元素對應(yīng)的hashcode值發(fā)生改變
??????? set.remove(p3); // 此時(shí)remove不掉寇钉,造成內(nèi)存泄漏
??????? set.add(p3); // 重新添加刀疙,居然添加成功
??????? System.out.println("總共有:" + set.size() + " 個(gè)元素!"); // 結(jié)果:總共有:4 個(gè)元素!
??? }
??? class Person
??? {
??????? private String name;
??????? private int age;
??????? publicPerson(String name, int age)
??????? {
??????????? super();
??????????? this.name = name;
??????????? this.age = age;
??????? }
??????? public String getName()
??????? {
??????????? return name;
??????? }
??????? public void setName(Stringname)
??????? {
??????????? this.name = name;
??????? }
??????? public int getAge()
??????? {
??????????? return age;
??????? }
??????? public void setAge(int age)
??????? {
???????? ???this.age = age;
??????? }
??????? @Override
??????? public String toString()
??????? {
??????????? return "Person [name=" + name + ", age=" + age + "]";
??????? }
??????? @Override
??????? public int hashCode()
??????? {
??????????? final int prime = 31;
??????????? int result = 1;
??????????? result = prime * result + getOuterType().hashCode();
??????????? result = prime * result + age;
??????????? result = prime * result + ((name == null) ? 0 : name.hashCode());
??????????? return result;
??????? }
??????? @Override
??????? public booleanequals(Object obj)
??????? {
??????????? if (this == obj)
??????????????? return true;
??????????? if (obj == null)
??????????????? return false;
??????????? if (getClass() !=obj.getClass())
??????????????? return false;
??????????? Personother = (Person) obj;
??????????? if(!getOuterType().equals(other.getOuterType()))
??????????????? return false;
??????????? if (age != other.age)
??????????????? return false;
??????????? if (name == null)
??????????? {
??????????????? if (other.name != null)
??????????????????? return false;
??????????? }
??????????? else if (!name.equals(other.name))
??????????????? return false;
??????????? return true;
??????? }
??????? privateMainTest getOuterType()
??????? {
??????????? returnMainTest.this;
??????? }
??? }
}
3、變量不合理的作用域
如果變量的定義范圍大于使用范圍扫倡,并且在使用完后沒有賦值為null的話谦秧,會(huì)出現(xiàn)內(nèi)存泄露。定義變量的時(shí)候,能定義為局部變量就不要定義為成員變量疚鲤,或者定義為成員變量的話锥累,在使用完變量后,把變量賦值為null集歇。
4桶略、使用非靜態(tài)內(nèi)部類
非靜態(tài)內(nèi)部類對象的構(gòu)建依賴于其外部類,內(nèi)部類對象會(huì)持有外部類對象的this引用诲宇,即時(shí)外部類對象不再被使用了际歼,其占用的內(nèi)存可能不會(huì)被GC回收,因?yàn)閮?nèi)部類的生命周期可能比外部類的生命周期要長焕窝,從而造成外部類對象不能被及時(shí)回收蹬挺。解決辦法是盡量使用靜態(tài)內(nèi)部類,靜態(tài)內(nèi)部類只是形式上在外部類的里面它掂,靜態(tài)內(nèi)部類不會(huì)持有外部類的引用巴帮,可以把靜態(tài)內(nèi)部類理解成是一個(gè)獨(dú)立的類,和外部類沒什么關(guān)系虐秋。
5榕茧、單例模式可能會(huì)造成內(nèi)存泄露
單例模式只允許應(yīng)用程序存在一個(gè)實(shí)例對象,并且這個(gè)實(shí)例對象的生命周期和應(yīng)用程序的生命周期一樣長客给,如果單例對象中擁有另一個(gè)對象的引用的話用押,這個(gè)被引用的對象就不能被及時(shí)回收。解決辦法是單例對象中持有的其他對象使用弱引用靶剑,弱引用對象在GC線程工作時(shí)蜻拨,其占用的內(nèi)存會(huì)被回收掉,如下示例:
public class SingleTon1 {???
??? private static finalSingleTon1 mInstance = null;???
??? privateWeakReference mContext;?
??? privateSingleTon1(WeakReference context) {???
??? mContext = context;?
??? }? ??
??? public static SingleTon1getInstance(WeakReference context) {???
??????? if (mInstance ==null) {???
??????????? synchronized(SingleTon1.class) {???
??????????????? if(mInstance == null) {???
???????????????????mInstance = new SingleTon1(context);???
??????????????? }???
??????????? }???
??????? }???
??????? returnmInstance;???
??? }???
}? ?
public class MyActivity extents Activity {?
??? public void onCreate(Bundle savedInstanceState){?
??????super.onCreate(savedInstanceState);?
??????setContentView(R.layout.main);?
?????? SingleTon1 singleTon1= SingleTon1.getInstance(new WeakReference(this));?
?? }?
}?
6桩引、監(jiān)聽器
在java 編程中缎讼,我們都需要和監(jiān)聽器打交道,通常一個(gè)應(yīng)用當(dāng)中會(huì)用到很多監(jiān)聽器坑匠,我們會(huì)調(diào)用一個(gè)控件的諸如addXXXListener()等方法來增加監(jiān)聽器血崭,但往往在釋放對象的時(shí)候卻沒有記住去刪除這些監(jiān)聽器,從而增加了內(nèi)存泄漏的機(jī)會(huì)厘灼。
7夹纫、各種連接
比如數(shù)據(jù)庫連接(dataSourse.getConnection()),網(wǎng)絡(luò)連接(socket)和io連接设凹,除非其顯式的調(diào)用了其close()方法將其連接關(guān)閉舰讹,否則是不會(huì)自動(dòng)被GC 回收的。對于Resultset 和Statement 對象可以不進(jìn)行顯式回收闪朱,但Connection 一定要顯式回收月匣,因?yàn)镃onnection 在任何時(shí)候都無法自動(dòng)回收匈睁,而Connection一旦回收,Resultset 和Statement 對象就會(huì)立即為NULL桶错。但是如果使用連接池,情況就不一樣了胀蛮,除了要顯式地關(guān)閉連接院刁,還必須顯式地關(guān)閉Resultset Statement 對象(關(guān)閉其中一個(gè),另外一個(gè)也會(huì)關(guān)閉)粪狼,否則就會(huì)造成大量的Statement 對象無法釋放退腥,從而引起內(nèi)存泄漏。這種情況下一般都會(huì)在try里面去的連接再榄,在finally里面釋放連接狡刘。
如何排查
(1)通過jps查找java進(jìn)程id。
(2)通過top -p [pid]發(fā)現(xiàn)內(nèi)存占用達(dá)到了最大值
(3)jstat -gccause pid 20000 每隔20秒輸出Full GC結(jié)果
(4)發(fā)現(xiàn)Full GC次數(shù)太多困鸥,基本就是內(nèi)存泄露了嗅蔬。生成dump文件,借助工具分析是哪個(gè)對象太多了疾就±绞酰基本能定位到問題在哪。
Full GC的原因
我們知道Full GC的觸發(fā)條件大致情況有以下幾種情況:
1. 程序執(zhí)行了System.gc() //建議jvm執(zhí)行fullgc猬腰,并不一定會(huì)執(zhí)行
2. 執(zhí)行了jmap -histo:live pid命令 //這個(gè)會(huì)立即觸發(fā)fullgc
3. 在執(zhí)行minor gc的時(shí)候進(jìn)行的一系列檢查
??? *執(zhí)行Minor GC的時(shí)候鸟废,JVM會(huì)檢查老年代中最大連續(xù)可用空間是否大于了當(dāng)前新生代所有對象的總大小。
??? *如果大于姑荷,則直接執(zhí)行Minor GC(這個(gè)時(shí)候執(zhí)行是沒有風(fēng)險(xiǎn)的)盒延。
??? *如果小于了,JVM會(huì)檢查是否開啟了空間分配擔(dān)保機(jī)制鼠冕,如果沒有開啟則直接改為執(zhí)行Full GC添寺。
??? *如果開啟了,則JVM會(huì)檢查老年代中最大連續(xù)可用空間是否大于了歷次晉升到老年代中的平均大小供鸠,如果小于則執(zhí)行改為執(zhí)行Full GC畦贸。
??? *如果大于則會(huì)執(zhí)行Minor GC,如果Minor GC執(zhí)行失敗則會(huì)執(zhí)行Full GC
對于我們的情況楞捂,可以初步排除1薄坏,2兩種情況,最有可能是4和5這兩種情況寨闹。為了進(jìn)一步排查原因胶坠,我們在線上開啟了 -XX:+HeapDumpBeforeFullGC。
注意:
JVM在執(zhí)行dump操作的時(shí)候是會(huì)發(fā)生stop the word事件的繁堡,也就是說此時(shí)所有的用戶線程都會(huì)暫停運(yùn)行沈善。
為了在此期間也能對外正常提供服務(wù)乡数,建議采用分布式部署,并采用合適的負(fù)載均衡算法闻牡。
內(nèi)存溢出種類:
引起內(nèi)存溢出的原因有很多種净赴,常見的有以下幾種:
1.內(nèi)存中加載的數(shù)據(jù)量過于龐大,如一次從數(shù)據(jù)庫取出過多數(shù)據(jù)罩润;
2.集合類中有對對象的引用玖翅,使用完后未清空,使得JVM不能回收割以;
3.代碼中存在死循環(huán)或循環(huán)產(chǎn)生過多重復(fù)的對象實(shí)體金度;
4.使用的第三方軟件中的BUG;
5.啟動(dòng)參數(shù)內(nèi)存值設(shè)定的過小
內(nèi)存溢出的解決方案:
第一步严沥,修改JVM啟動(dòng)參數(shù)猜极,直接增加內(nèi)存。(-Xms消玄,-Xmx參數(shù)一定不要忘記加跟伏。)
第二步,檢查錯(cuò)誤日志翩瓜,查看“OutOfMemory”錯(cuò)誤前是否有其它異吵昴罚或錯(cuò)誤。
第三步奥溺,對代碼進(jìn)行走查和分析辞色,找出可能發(fā)生內(nèi)存溢出的位置。
重點(diǎn)排查以下幾點(diǎn):
1.檢查對數(shù)據(jù)庫查詢中浮定,是否有一次獲得全部數(shù)據(jù)的查詢相满。一般來說,如果一次取十萬條記錄到內(nèi)存桦卒,就可能引起內(nèi)存溢出立美。這個(gè)問題比較隱蔽,在上線前方灾,數(shù)據(jù)庫中數(shù)據(jù)較少建蹄,不容易出問題,上線后裕偿,數(shù)據(jù)庫中數(shù)據(jù)多了洞慎,一次查詢就有可能引起內(nèi)存溢出。因此對于數(shù)據(jù)庫查詢盡量采用分頁的方式查詢嘿棘。
2.檢查代碼中是否有死循環(huán)或遞歸調(diào)用劲腿。
3.檢查是否有大循環(huán)重復(fù)產(chǎn)生新對象實(shí)體。
4.檢查對數(shù)據(jù)庫查詢中鸟妙,是否有一次獲得全部數(shù)據(jù)的查詢焦人。一般來說挥吵,如果一次取十萬條記錄到內(nèi)存,就可能引起內(nèi)存溢出花椭。這個(gè)問題比較隱蔽忽匈,在上線前,數(shù)據(jù)庫中數(shù)據(jù)較少矿辽,不容易出問題脉幢,上線后,數(shù)據(jù)庫中數(shù)據(jù)多了嗦锐,一次查詢就有可能引起內(nèi)存溢出。因此對于數(shù)據(jù)庫查詢盡量采用分頁的方式查詢沪曙。
5.檢查List奕污、MAP等集合對象是否有使用完后,未清除的問題液走。List碳默、MAP等集合對象會(huì)始終存有對對象的引用,使得這些對象不能被GC回收缘眶。
第四步嘱根,使用內(nèi)存查看工具動(dòng)態(tài)查看內(nèi)存使用情況
?
內(nèi)存不同區(qū)域溢出情況:
1,堆內(nèi)存溢出
場景:
1)設(shè)置的jvm內(nèi)存太小巷懈,對象所需內(nèi)存太大该抒,創(chuàng)建對象時(shí)分配空間,就會(huì)拋出這個(gè)異常顶燕。
堆內(nèi)存中主要存放對象凑保、數(shù)組等,只要不斷地創(chuàng)建這些對象涌攻,并且保證GC Roots到對象之間有可達(dá)路徑來避免垃圾收集回收機(jī)制清除這些對象欧引,當(dāng)這些對象所占空間超過最大堆容量時(shí),就會(huì)產(chǎn)生java.lang.OutOfMemoryError:Java heap space的異常恳谎。
2)流量/數(shù)據(jù)峰值芝此,應(yīng)用程序自身的處理存在一定的限額,比如一定數(shù)量的用戶或一定數(shù)量的數(shù)據(jù)因痛。而當(dāng)用戶數(shù)量或數(shù)據(jù)量突然激增并超過預(yù)期的閾值時(shí)婚苹,那么就會(huì)在峰值停止前正常運(yùn)行的操作將停止并觸發(fā)java . lang.OutOfMemoryError:Java heap space錯(cuò)誤。
堆內(nèi)存異常示例如下:
/**
?*設(shè)置最大堆最小堆:-Xms20m -Xmx20m
?*運(yùn)行時(shí)鸵膏,不斷在堆中創(chuàng)建OOMObject類的實(shí)例對象租副,且while執(zhí)行結(jié)束之前,GC Roots(代碼中的oomObjectList)到對象(每一個(gè)OOMObject對象)之間有可達(dá)路徑较性,垃圾收集器就無法回收它們用僧,最終導(dǎo)致內(nèi)存溢出结胀。
?*/
public class HeapOOM {
??? static class OOMObject {
??? }
??? public static voidmain(String[] args) {
???????List oomObjectList = new ArrayList<>();
??????? while (true) {
???????????oomObjectList.add(new OOMObject());
??????? }
??? }
}
運(yùn)行后會(huì)報(bào)異常,在堆棧信息中可以看到 java.lang.OutOfMemoryError: Java heap space 的信息责循,說明在堆內(nèi)存空間產(chǎn)生內(nèi)存溢出的異常糟港。
常見的原因 :
內(nèi)存加載的數(shù)據(jù)量太大:一次性從數(shù)據(jù)庫取太多數(shù)據(jù)
集合類中有對對象的引用,使用后未清空院仿,GC不能進(jìn)行回收秸抚。
代碼中存在循環(huán)產(chǎn)生過多的重復(fù)對象
啟動(dòng)參數(shù)堆內(nèi)存值小
解決方法:
首先,如果代碼沒有什么問題的情況下歹垫,可以適當(dāng)調(diào)整-Xms和-Xmx兩個(gè)jvm參數(shù)剥汤,使用壓力測試來調(diào)整這兩個(gè)參數(shù)達(dá)到最優(yōu)值。
其次排惨,盡量避免大的對象的申請吭敢,像文件上傳,大批量從數(shù)據(jù)庫中獲取暮芭,這是需要避免的鹿驼,盡量分塊或者分批處理,有助于系統(tǒng)的正常穩(wěn)定的執(zhí)行辕宏。
最后畜晰,盡量提高一次請求的執(zhí)行速度,垃圾回收越早越好瑞筐,否則凄鼻,大量的并發(fā)來了的時(shí)候,再來新的請求就無法分配內(nèi)存了聚假,就容易造成系統(tǒng)的雪崩野宜。
2,虛擬機(jī)棧/本地方法棧溢出
(1)StackOverflowError:當(dāng)線程請求的棧的深度大于虛擬機(jī)所允許的最大深度魔策,則拋出StackOverflowError匈子,簡單理解就是虛擬機(jī)棧中的棧幀數(shù)量過多(一個(gè)線程嵌套調(diào)用的方法數(shù)量過多)時(shí),就會(huì)拋出StackOverflowError異常闯袒。最常見的場景就是方法無限遞歸調(diào)用逛尚,
如下:
/**
?*設(shè)置每個(gè)線程的棧大型靶:-Xss256k
?*運(yùn)行時(shí)明郭,不斷調(diào)用doSomething()方法嬉挡,main線程不斷創(chuàng)建棧幀并入棧,導(dǎo)致棧的深度越來越大喷户,最終導(dǎo)致棧溢出唾那。
?*/
public class StackSOF {
??? private intstackLength=1;
??? public void doSomething(){
??????????? stackLength++;
??????????? doSomething();
??? }
??? public static voidmain(String[] args) {
??????? StackSOFstackSOF=new StackSOF();
??????? try {
???????????stackSOF.doSomething();
??????? }catch (Throwablee){//注意捕獲的是Throwable
???????????System.out.println("棧深度:"+stackSOF.stackLength);
??????????? throw e;
??????? }
??? }
}
上述代碼執(zhí)行后拋出:Exception in thread “Thread-0” java.lang.StackOverflowError的異常。
常見原因:
棧內(nèi)存溢出褪尝,一般由棧內(nèi)存的局部變量過爆了闹获,導(dǎo)致內(nèi)存溢出期犬,出現(xiàn)在遞歸方法,參數(shù)個(gè)數(shù)過多避诽,遞歸過深龟虎,遞歸沒有出口。
(2)OutOfMemoryError:如果虛擬機(jī)在擴(kuò)展棧時(shí)無法申請到足夠的內(nèi)存空間沙庐,則拋出OutOfMemoryError鲤妥。我們可以這樣理解,虛擬機(jī)中可以供棧占用的空間≈可用物理內(nèi)存 - 最大堆內(nèi)存 - 最大方法區(qū)內(nèi)存拱雏,比如一臺機(jī)器內(nèi)存為4G棉安,系統(tǒng)和其他應(yīng)用占用2G,虛擬機(jī)可用的物理內(nèi)存為2G铸抑,最大堆內(nèi)存為1G贡耽,最大方法區(qū)內(nèi)存為512M,那可供棧占有的內(nèi)存大約就是512M羡滑,假如我們設(shè)置每個(gè)線程棧的大小為1M,那虛擬機(jī)中最多可以創(chuàng)建512個(gè)線程算芯,超過512個(gè)線程再創(chuàng)建就沒有空間可以給棧了,就報(bào)OutOfMemoryError異常了熙揍。
棧上能夠產(chǎn)生OutOfMemoryError的示例如下:
/**
?*設(shè)置每個(gè)線程的棧大兄暗弧:-Xss2m
?*運(yùn)行時(shí),不斷創(chuàng)建新的線程(且每個(gè)線程持續(xù)執(zhí)行)届囚,每個(gè)線程對一個(gè)一個(gè)棧有梆,最終沒有多余的空間來為新的線程分配,導(dǎo)致OutOfMemoryError
?*/
public class StackOOM {
??? private static intthreadNum = 0;
??? public voiddoSomething() {
??????? try {
??????????? Thread.sleep(100000000);
??????? } catch(InterruptedException e) {
???????????e.printStackTrace();
??????? }
??? }
??? public static voidmain(String[] args) {
??????? final StackOOMstackOOM = new StackOOM();
??????? try {
??????????? while (true) {
??????????????? threadNum++;
??????????????? Threadthread = new Thread(new Runnable() {
???????????????????@Override
??????????????????? publicvoid run() {
???????????????????????stackOOM.doSomething();
??????????????????? }
??????????????? });
?????? ?????????thread.start();
??????????? }
??????? } catch (Throwablee) {
???????????System.out.println("目前活動(dòng)線程數(shù)量:" + threadNum);
??????????? throw e;
??????? }
??? }
}
上述代碼運(yùn)行后會(huì)報(bào)異常意系,在堆棧信息中可以看到 java.lang.OutOfMemoryError: unable to create new native thread的信息泥耀,無法創(chuàng)建新的線程,說明是在擴(kuò)展棧的時(shí)候產(chǎn)生的內(nèi)存溢出異常蛔添。
總結(jié):在線程較少的時(shí)候痰催,某個(gè)線程請求深度過大,會(huì)報(bào)StackOverflow異常迎瞧,解決這種問題可以適當(dāng)加大棧的深度(增加椏淙埽空間大小)凶硅,也就是把-Xss的值設(shè)置大一些缝裁,但一般情況下是代碼問題的可能性較大;在虛擬機(jī)產(chǎn)生線程時(shí)足绅,無法為該線程申請椊莅螅空間了韩脑,會(huì)報(bào)OutOfMemoryError異常,解決這種問題可以適當(dāng)減小棧的深度胎食,也就是把-Xss的值設(shè)置小一些扰才,每個(gè)線程占用的空間小了,總空間一定就能容納更多的線程厕怜,但是操作系統(tǒng)對一個(gè)進(jìn)程的線程數(shù)有限制衩匣,經(jīng)驗(yàn)值在3000~5000左右。在jdk1.5之前-Xss默認(rèn)是256k粥航,jdk1.5之后默認(rèn)是1M琅捏,這個(gè)選項(xiàng)對系統(tǒng)硬性還是蠻大的,設(shè)置時(shí)要根據(jù)實(shí)際情況递雀,謹(jǐn)慎操作柄延。
3,方法區(qū)溢出
前面說到缀程,方法區(qū)主要用于存儲(chǔ)虛擬機(jī)加載的類信息搜吧、常量、靜態(tài)變量杨凑,以及編譯器編譯后的代碼等數(shù)據(jù)滤奈,所以方法區(qū)溢出的原因就是沒有足夠的內(nèi)存來存放這些數(shù)據(jù)。
由于在jdk1.6之前字符串常量池是存在于方法區(qū)中的撩满,所以基于jdk1.6之前的虛擬機(jī)蜒程,可以通過不斷產(chǎn)生不一致的字符串(同時(shí)要保證和GC Roots之間保證有可達(dá)路徑)來模擬方法區(qū)的OutOfMemoryError異常;但方法區(qū)還存儲(chǔ)加載的類信息伺帘,所以基于jdk1.7的虛擬機(jī)昭躺,可以通過動(dòng)態(tài)不斷創(chuàng)建大量的類來模擬方法區(qū)溢出。
/**
?*設(shè)置方法區(qū)最大伪嫁、最小空間:-XX:PermSize=10m -XX:MaxPermSize=10m
?*運(yùn)行時(shí)领炫,通過cglib不斷創(chuàng)建JavaMethodAreaOOM的子類,方法區(qū)中類信息越來越多张咳,最終沒有可以為新的類分配的內(nèi)存導(dǎo)致內(nèi)存溢出
?*/
public class JavaMethodAreaOOM {
??? public static voidmain(final String[] args){
?????? try {
?????????? while (true){
?????????????? Enhancerenhancer=new Enhancer();
??????????????enhancer.setSuperclass(JavaMethodAreaOOM.class);
??????????????enhancer.setUseCache(false);
?????????????? enhancer.setCallback(newMethodInterceptor() {
?????????????????? @Override
?????????????????? publicObject intercept(Object o, Method method, Object[] objects, MethodProxymethodProxy) throws Throwable {
??????????????????????return methodProxy.invokeSuper(o,objects);
?????????????????? }
?????????????? });
??????????????enhancer.create();
?????????? }
?????? }catch (Throwable t){
??????????t.printStackTrace();
?????? }
??? }
}
上述代碼運(yùn)行后會(huì)報(bào)“java.lang.OutOfMemoryError: PermGen space”的異常驹吮,說明是在方法區(qū)出現(xiàn)了內(nèi)存溢出的錯(cuò)誤。
4晶伦,Metaspace內(nèi)存溢出
問題描述:
元空間的溢出碟狞,系統(tǒng)會(huì)拋出java.lang.OutOfMemoryError: Metaspace。出現(xiàn)這個(gè)異常的問題的原因是系統(tǒng)的代碼非常多或引用的第三方包非常多或者通過動(dòng)態(tài)代碼生成類加載等方法婚陪,導(dǎo)致元空間的內(nèi)存占用很大族沃。
以下是用循環(huán)動(dòng)態(tài)生成class的方式來模擬元空間的內(nèi)存溢出的。
解決方法:???
默認(rèn)情況下,元空間的大小僅受本地內(nèi)存限制脆淹。但是為了整機(jī)的性能常空,盡量還是要對該項(xiàng)進(jìn)行設(shè)置,以免造成整機(jī)的服務(wù)停機(jī)盖溺。
? 1)優(yōu)化參數(shù)配置漓糙,避免影響其他JVM進(jìn)程
-XX:MetaspaceSize,初始空間大小烘嘱,達(dá)到該值就會(huì)觸發(fā)垃圾收集進(jìn)行類型卸載昆禽,同時(shí)GC會(huì)對該值進(jìn)行調(diào)整:如果釋放了大量的空間,就適當(dāng)降低該值蝇庭;如果釋放了很少的空間醉鳖,那么在不超過MaxMetaspaceSize時(shí),適當(dāng)提高該值哮内。
-XX:MaxMetaspaceSize盗棵,最大空間,默認(rèn)是沒有限制的北发。
除了上面兩個(gè)指定大小的選項(xiàng)以外纹因,還有兩個(gè)與 GC 相關(guān)的屬性:
-XX:MinMetaspaceFreeRatio,在GC之后琳拨,最小的Metaspace剩余空間容量的百分比瞭恰,減少為分配空間所導(dǎo)致的垃圾收集 。
-XX:MaxMetaspaceFreeRatio从绘,在GC之后寄疏,最大的Metaspace剩余空間容量的百分比是牢,減少為釋放空間所導(dǎo)致的垃圾收集僵井。
2)慎重引用第三方包
對第三方包,一定要慎重選擇驳棱,不需要的包就去掉批什。這樣既有助于提高編譯打包的速度,也有助于提高遠(yuǎn)程部署的速度社搅。
3)關(guān)注動(dòng)態(tài)生成類的框架
對于使用大量動(dòng)態(tài)生成類的框架驻债,要做好壓力測試,驗(yàn)證動(dòng)態(tài)生成的類是否超出內(nèi)存的需求會(huì)拋出異常形葬。
5合呐,本機(jī)直接內(nèi)存溢出
本機(jī)直接內(nèi)存(DirectMemory)并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域笙以,但Java中用到NIO相關(guān)操作時(shí)(比如ByteBuffer的allocteDirect方法申請的是本機(jī)直接內(nèi)存)淌实,也可能會(huì)出現(xiàn)java.lang.OutOfMemoryError:Direct buffer memory異常。
如果你在直接或間接使用了ByteBuffer中的allocateDirect方法的時(shí)候,而不做clear的時(shí)候就會(huì)出現(xiàn)類似的問題拆祈。
解決方法:如果經(jīng)常有類似的操作恨闪,可以考慮設(shè)置參數(shù):-XX:MaxDirectMemorySize,并及時(shí)clear內(nèi)存放坏。
6咙咽,棧內(nèi)存溢出
問題描述
當(dāng)一個(gè)線程執(zhí)行一個(gè)Java方法時(shí),JVM將創(chuàng)建一個(gè)新的棧幀并且把它push到棧頂淤年。此時(shí)新的棧幀就變成了當(dāng)前棧幀钧敞,方法執(zhí)行時(shí),使用棧幀來存儲(chǔ)參數(shù)互亮、局部變量犁享、中間指令以及其他數(shù)據(jù)。
當(dāng)一個(gè)方法遞歸調(diào)用自己時(shí)豹休,新的方法所產(chǎn)生的數(shù)據(jù)(也可以理解為新的棧幀)將會(huì)被push到棧頂炊昆,方法每次調(diào)用自己時(shí),會(huì)拷貝一份當(dāng)前方法的數(shù)據(jù)并push到棧中威根。因此凤巨,遞歸的每層調(diào)用都需要?jiǎng)?chuàng)建一個(gè)新的棧幀。這樣的結(jié)果是洛搀,棧中越來越多的內(nèi)存將隨著遞歸調(diào)用而被消耗敢茁,如果遞歸調(diào)用自己一百萬次,那么將會(huì)產(chǎn)生一百萬個(gè)棧幀留美。這樣就會(huì)造成棧的內(nèi)存溢出StackOverflowError彰檬。
解決方法:
如果程序中確實(shí)有遞歸調(diào)用,出現(xiàn)棧溢出時(shí)谎砾,可以調(diào)高-Xss大小逢倍,就可以解決棧內(nèi)存溢出的問題了。遞歸調(diào)用防止形成死循環(huán)景图,否則就會(huì)出現(xiàn)棧內(nèi)存溢出较雕。
Full GC分析定位過程:
1,如何發(fā)現(xiàn)是否發(fā)生FULL GC和FULL GC是否頻繁
使用JDK自帶的輕量級小工具jstat
???? 語法結(jié)構(gòu):
Usage: jstat -help|-options
???????????? jstat-? [-t] [-h] [[]]
參數(shù)解釋:
Options — 選項(xiàng)挚币,我們一般使用 -gcutil 查看gc情況
vmid??? — VM的進(jìn)程號亮蒋,即當(dāng)前運(yùn)行的java進(jìn)程號
interval– 間隔時(shí)間,單位為秒或者毫秒
count?? —打印次數(shù)妆毕,如果缺省則打印無數(shù)次
比如/opt/taobao/java/bin/jstat –gcutil pid 5000
輸出結(jié)果:
S0??? S1???????? E????????? O????????? P??????? YGC?????YGCT??? FGC???? FGCT????GCT
0.00? 90.63????? 100.00???58.82????? 3.51???? 183?????2.059???? 0???? 0.000???2.059
0.00? 15.48????? 7.80?????60.99????? 3.51???? 185?????2.092???? 1???? 0.305???2.397
0.00? 15.48????? 18.10????47.90????? 3.51???? 185?????2.092???? 2???? 0.348???2.440
S0? — Heap上的 Survivor space 0 區(qū)已使用空間的百分比
S1? — Heap上的 Survivor space 1 區(qū)已使用空間的百分比
E?? — Heap上的 Eden space 區(qū)已使用空間的百分比
O?? — Heap上的 Old space 區(qū)已使用空間的百分比
P?? — Perm space區(qū)已使用空間的百分比
YGC — 從應(yīng)用程序啟動(dòng)到采樣時(shí)發(fā)生 Young GC 的次數(shù)
YGCT– 從應(yīng)用程序啟動(dòng)到采樣時(shí) Young GC 所用的時(shí)間(單位秒)
FGC — 從應(yīng)用程序啟動(dòng)到采樣時(shí)發(fā)生 Full GC 的次數(shù)
FGCT– 從應(yīng)用程序啟動(dòng)到采樣時(shí) Full GC 所用的時(shí)間(單位秒)
GCT — 從應(yīng)用程序啟動(dòng)到采樣時(shí)用于垃圾回收的總時(shí)間(單位秒)
通過FGC我們可以發(fā)現(xiàn)系統(tǒng)是否發(fā)生FULL GC和FULL GC的頻率
2慎玖,F(xiàn)ULL GC分析和問題定位
a. GC log收集和分析
(1)在JVM啟動(dòng)參數(shù)增加:"-verbose:gc-Xloggc:?-XX:+PrintGCDetails -XX:+PrintGCDateStamps"
PrintGCTimeStamp只能獲得相對時(shí)間,建議使用PrintGCDateStamps獲得full gc 發(fā)生的絕對時(shí)間
(2)如果采用CMS GC,仔細(xì)分析jstat FGC輸出和GC 日志會(huì)發(fā)現(xiàn)笛粘, CMS的每個(gè)并發(fā)GC周期則有兩個(gè)stop-the-world階段——initial mark與final re-mark趁怔,使得CMS的每個(gè)并發(fā)GC周期總共會(huì)更新full GC計(jì)數(shù)器兩次远舅,initial mark與final re-mark各一次
b. Dump JVM 內(nèi)存快照
/opt/taobao/java/bin/jmap -dump:format=b,file=dump.bin pid
這里有一個(gè)問題是什么時(shí)候進(jìn)行dump?
一種方法是前面提到的用jstat工具觀察,當(dāng)OLD區(qū)到達(dá)比較高的比例如60%痕钢,一般會(huì)很快觸發(fā)一次FULL GC,可以進(jìn)行一次DUMP,在FULL GC發(fā)生以后再DUMP一次图柏,這樣比較就可以發(fā)現(xiàn)到底是哪些對象導(dǎo)致不停的FULL GC
另外一種方法是通過配置JVM參數(shù)
-XX:+HeapDumpBeforeFullGC -XX:+HeapDumpAfterFullGC分別用于指定在full GC之前與之后生成heap dump
c. 利用MAT((Memory
Analyzer Tool)工具分析dump文件
關(guān)于MAT具體使用方法網(wǎng)上有很多介紹,這里不做詳細(xì)展開任连,這里需要注意的是:
(1)?? MAT缺省只分析reachable的對象蚤吹,unreachable的對象(將被收集掉的對象)被忽略,而分析FULL GC頻繁原因時(shí)unreachable
object也應(yīng)該同時(shí)被重點(diǎn)關(guān)注随抠。如果要顯示unreachable的對象細(xì)節(jié)必須用mat 1.1以上版本并且打開選項(xiàng)“keep unreachable object”
(2)?? 通常dump文件會(huì)好幾個(gè)G裁着,無法在windows上直接進(jìn)行分析,我們可以先把dump文件在linux上進(jìn)行分析拱她,再把分析好的文件拷貝到windows上二驰,在windows上用MAT打開分析文件。
內(nèi)存泄露主要有如下幾大類:
1)靜態(tài)集合類引起內(nèi)存泄漏:
像HashMap秉沼、Vector等的使用最容易出現(xiàn)內(nèi)存泄露桶雀,這些靜態(tài)變量的生命周期和應(yīng)用程序一致,他們所引用的所有的對象Object也不能被釋放唬复,因?yàn)樗麄円矊⒁恢北籚ector等引用著矗积。
static Vector v = new Vector();
for (int i = 1; i<100; i++)
{
??? Object o = new Object();
??? v.add(o);
??? o = null;
}
在這個(gè)例子中,代碼棧中存在Vector 對象的引用 v 和 Object 對象的引用 o 敞咧。在 For 循環(huán)棘捣,我們不斷的生成新的對象,然后將其添加到 Vector 對象中休建,之后將 o 引用置空乍恐。問題是當(dāng) o 引用被置空后,如果發(fā)生 GC测砂,我們創(chuàng)建的 Object 對象是否能夠被 GC 回收呢茵烈?答案是否定的。因?yàn)椋?GC 在跟蹤代碼棧中的引用時(shí)邑彪,會(huì)發(fā)現(xiàn) v 引用瞧毙,而繼續(xù)往下跟蹤胧华,就會(huì)發(fā)現(xiàn) v 引用指向的內(nèi)存空間中又存在指向 Object 對象的引用寄症。也就是說盡管o 引用已經(jīng)被置空,但是 Object 對象仍然存在其他的引用矩动,是可以被訪問到的有巧,所以 GC 無法將其釋放掉。如果在此循環(huán)之后悲没, Object 對象對程序已經(jīng)沒有任何作用篮迎,那么我們就認(rèn)為此 Java 程序發(fā)生了內(nèi)存泄漏。
2)當(dāng)集合里面的對象屬性被修改后,再調(diào)用remove()方法時(shí)不起作用甜橱。
public static void main(String[] args)
{
??? Set set =new HashSet();
??? Person p1 = newPerson("唐僧","pwd1",25);
??? Person p2 = newPerson("孫悟空","pwd2",26);
??? Person p3 = newPerson("豬八戒","pwd3",27);
??? set.add(p1);
??? set.add(p2);
??? set.add(p3);
???System.out.println("總共有:"+set.size()+"個(gè)元素!"); //結(jié)果:總共有:3 個(gè)元素!
??? p3.setAge(2); //修改p3的年齡,此時(shí)p3元素對應(yīng)的hashcode值發(fā)生改變
??? set.remove(p3); //此時(shí)remove不掉逊笆,造成內(nèi)存泄漏
??? set.add(p3); //重新添加,居然添加成功
???System.out.println("總共有:"+set.size()+"個(gè)元素!"); //結(jié)果:總共有:4 個(gè)元素!
??? for (Person person :set)
??? {
???????System.out.println(person);
??? }
}
3)各種連接
比如數(shù)據(jù)庫連接(dataSourse.getConnection())岂傲,網(wǎng)絡(luò)連接(socket)和io連接难裆,除非其顯式的調(diào)用了其close()方法將其連接關(guān)閉,否則是不會(huì)自動(dòng)被GC回收的镊掖。對于Resultset和Statement 對象可以不進(jìn)行顯式回收乃戈,但Connection一定要顯式回收,因?yàn)镃onnection 在任何時(shí)候都無法自動(dòng)回收亩进,而Connection一旦回收症虑,Resultset和Statement 對象就會(huì)立即為null。但是如果使用連接池归薛,情況就不一樣了谍憔,除了要顯式地關(guān)閉連接,還必須顯式地關(guān)閉Resultset Statement 對象(關(guān)閉其中一個(gè)主籍,另外一個(gè)也會(huì)關(guān)閉)韵卤,否則就會(huì)造成大量的Statement 對象無法釋放,從而引起內(nèi)存泄漏崇猫。這種情況下一般都會(huì)在try里面去的連接沈条,在finally里面釋放連接。
4)內(nèi)部類和外部模塊的引用
內(nèi)部類的引用是比較容易遺忘的一種诅炉,而且一旦沒釋放可能導(dǎo)致一系列的后繼類對象沒有釋放蜡歹。此外程序員還要小心外部模塊不經(jīng)意的引用,例如程序員A負(fù)責(zé)A模塊涕烧,調(diào)用了B模塊的一個(gè)方法如:
public void registerMsg(Object b);
這種調(diào)用就要非常小心了月而,傳入了一個(gè)對象,很可能模塊B就保持了對該對象的引用议纯,這時(shí)候就需要注意模塊B 是否提供相應(yīng)的操作去除引用父款。
5)單例模式
不正確使用單例模式是引起內(nèi)存泄漏的一個(gè)常見問題,單例對象在初始化后將在JVM的整個(gè)生命周期中存在(以靜態(tài)變量的方式)瞻凤,如果單例對象持有外部的引用憨攒,那么這個(gè)對象將不能被JVM正常回收阀参,導(dǎo)致內(nèi)存泄漏肝集,考慮下面的例子:
class A{
??? public A(){
??? ???????B.getInstance().setA(this);
??? }
??? ....
}
//B類采用單例模式
class B{
??? private A a;
??? private static Binstance=new B();
??? public B(){}
??? public static BgetInstance(){
??? ???????return instance;
??? }
??? public void setA(A a){
?????? ????this.a=a;
??? }
??? //getter...
}
顯然B采用singleton模式,它持有一個(gè)A對象的引用蛛壳,而這個(gè)A類的對象將不能被回收杏瞻。
6)監(jiān)聽器
在java編程中所刀,我們都需要和監(jiān)聽器打交道,通常一個(gè)應(yīng)用當(dāng)中會(huì)用到很多監(jiān)聽器捞挥,我們會(huì)調(diào)用一個(gè)控件的諸如addXXXListener()等方法來增加監(jiān)聽器浮创,但往往在釋放對象的時(shí)候卻沒有記住去刪除這些監(jiān)聽器,從而增加了內(nèi)存泄漏的機(jī)會(huì)砌函。
ThreadLocal 內(nèi)存泄漏問題
ThreadLocal的實(shí)現(xiàn)是這樣的:每個(gè)Thread 維護(hù)一個(gè) ThreadLocalMap映射表蒸矛,這個(gè)映射表的 key 是 ThreadLocal實(shí)例本身,value 是真正需要存儲(chǔ)的 Object胸嘴。
也就是說 ThreadLocal 本身并不存儲(chǔ)值雏掠,它只是作為一個(gè) key 來讓線程從 ThreadLocalMap獲取 value。值得注意的是圖中的虛線劣像,表示 ThreadLocalMap 是使用 ThreadLocal 的弱引用作為 Key 的乡话,弱引用的對象在 GC 時(shí)會(huì)被回收。
ThreadLocal為什么會(huì)內(nèi)存泄漏
ThreadLocalMap使用ThreadLocal的弱引用作為key耳奕,通常弱引用都會(huì)和引用隊(duì)列配合清理機(jī)制使用绑青,但是ThreadLocalMap是個(gè)例外,它并沒有這么做屋群。這意味著闸婴,廢棄項(xiàng)目的回收依賴于顯式的觸發(fā),否則就要等線程結(jié)束芍躏,今兒回收相應(yīng)的ThreadLocalMap邪乍。如果一個(gè)ThreadLocal沒有外部強(qiáng)引用來引用它,那么系統(tǒng) GC 的時(shí)候对竣,這個(gè)ThreadLocal勢必會(huì)被回收庇楞,這樣一來,ThreadLocalMap中就會(huì)出現(xiàn)key為null的Entry否纬,就沒有辦法訪問這些key為null的Entry的value吕晌,如果當(dāng)前線程再遲遲不結(jié)束的話,這些key為null的Entry的value就會(huì)一直存在一條強(qiáng)引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永遠(yuǎn)無法回收临燃,造成內(nèi)存泄漏睛驳。
其實(shí),ThreadLocalMap的設(shè)計(jì)中已經(jīng)考慮到這種情況膜廊,也加上了一些防護(hù)措施:在ThreadLocal的get(),set(),remove()的時(shí)候都會(huì)清除線程ThreadLocalMap里所有key為null的value乏沸。
但是這些被動(dòng)的預(yù)防措施并不能保證不會(huì)內(nèi)存泄漏:
使用static的ThreadLocal,延長了ThreadLocal的生命周期溃论,可能導(dǎo)致的內(nèi)存泄漏(參考ThreadLocal 內(nèi)存泄露的實(shí)例分析)屎蜓。
分配使用了ThreadLocal又不再調(diào)用get(),set(),remove()方法痘昌,那么就會(huì)導(dǎo)致內(nèi)存泄漏钥勋。
綜合上面的分析炬转,我們可以理解ThreadLocal內(nèi)存泄漏的前因后果,那么怎么避免內(nèi)存泄漏呢算灸?
每次使用完ThreadLocal扼劈,都調(diào)用它的remove()方法,清除數(shù)據(jù)菲驴。
在使用線程池的情況下荐吵,沒有及時(shí)清理ThreadLocal,不僅是內(nèi)存泄漏的問題赊瞬,更嚴(yán)重的是可能導(dǎo)致業(yè)務(wù)邏輯出現(xiàn)問題先煎。所以,使用ThreadLocal就跟加鎖完要解鎖一樣巧涧,用完就清理薯蝎。
參考書目:《深入理解JVM虛擬機(jī)》、《Java性能調(diào)優(yōu)指南》