1. 既然有 GC 機(jī)制县恕,為什么還會有內(nèi)存泄露的情況
理論上 Java 因?yàn)橛欣厥諜C(jī)制(GC)不會存在內(nèi)存泄露問題(這也是 Java 被廣泛使用于服務(wù)器端編程的一個(gè)重要原因)构眯。然而在實(shí)際開發(fā)中趁舀,可能會存在無用但可達(dá)的對象,這些對象不能被 GC 回收案疲,因此也會導(dǎo)致內(nèi)存泄露的發(fā)生封恰。
例如 hibernate 的 Session(一級緩存)中的對象屬于持久態(tài),垃圾回收器是不會回收這些對象的褐啡,然而這些對象中可能存在無用的垃圾對象诺舔,如果不及時(shí)關(guān)閉(close)或清空(flush)一級緩存就可能導(dǎo)致內(nèi)存泄露。
下面例子中的代碼也會導(dǎo)致內(nèi)存泄露备畦。
1. import java.util.Arrays;
2. import java.util.EmptyStackException;
3. public class MyStack<T> {
4. private T[] elements;
5. private int size = 0;
6. private static final int INIT_CAPACITY = 16;
7. public MyStack() {
8. elements = (T[]) new Object[INIT_CAPACITY];
9. }
10. public void push(T elem) {
11. ensureCapacity();
12. elements[size++] = elem;
13. }
14. public T pop() {
15. if(size == 0)throw new EmptyStackException();
16. return elements[--size];
17. }
18. private void ensureCapacity() {
19. if(elements.length == size) {
20. elements = Arrays.copyOf(elements, 2 * size + 1);
21. }
22. }
23. }
上面的代碼實(shí)現(xiàn)了一個(gè)棧(先進(jìn)后出(FILO))結(jié)構(gòu)低飒,乍看之下似乎沒有什么明顯的問題,它甚至可以通過你編寫的各種單元測試懂盐。然而其中的 pop 方法卻存在內(nèi)存泄露的問題褥赊,當(dāng)我們用 pop 方法彈出棧中的對象時(shí),該對象不會被當(dāng)作垃圾回收允粤,即使使用棧的程序不再引用這些對象崭倘,因?yàn)闂?nèi)部維護(hù)著對這些對象的過期引用(obsoletereference)。在支持垃圾回收的語言中类垫,內(nèi)存泄露是很隱蔽的司光,這種內(nèi)存泄露其實(shí)就是無意識的對象保持。如果一個(gè)對象引用被無意識的保留起來了悉患,那么垃圾回收器不會處理這個(gè)對象残家,也不會處理該對象引用的其他對象,即使這樣的對象只有少數(shù)幾個(gè)售躁,也可能會導(dǎo)致很多的對象被排除在垃圾回收之外坞淮,從而對性能造成重大影響,極端情況下會引發(fā) Disk Paging (物理內(nèi)存與硬盤的虛擬內(nèi)存交換數(shù)據(jù))陪捷,甚至造成 OutOfMemoryError回窘。
2. Java 中為什么會有 GC 機(jī)制呢?
Java 中為什么會有 GC 機(jī)制呢市袖?
? 安全性考慮啡直;-- for security.
? 減少內(nèi)存泄露;-- erase memory leak in some degree.
? 減少程序員工作量。-- Programmers don't worry about memory releasing.
3. 對于 Java 的 GC 哪些內(nèi)存需要回收
內(nèi)存運(yùn)行時(shí) JVM 會有一個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū)來管理內(nèi)存酒觅。它主要包括 5 大部分:程序計(jì)數(shù)器(Program Counter
Register)撮执、虛擬機(jī)棧(VM Stack)续捂、本地方法棧(Native Method Stack)槐臀、方法區(qū)(Method Area)、堆(Heap).
而其中程序計(jì)數(shù)器鼎天、虛擬機(jī)棧颜凯、本地方法棧是每個(gè)線程私有的內(nèi)存空間谋币,隨線程而生,隨線程而亡装获。例如棧中每一個(gè)棧幀中分配多少內(nèi)存基本上在類結(jié)構(gòu)確定是哪個(gè)時(shí)就已知了瑞信,因此這 3 個(gè)區(qū)域的內(nèi)存分配和回收都是確定的,無需考慮內(nèi)存回收的問題穴豫。
但方法區(qū)和堆就不同了,一個(gè)接口的多個(gè)實(shí)現(xiàn)類需要的內(nèi)存可能不一樣逼友,我們只有在程序運(yùn)行期間才會知道會創(chuàng)建哪些對象精肃,這部分內(nèi)存的分配和回收都是動(dòng)態(tài)的,GC 主要關(guān)注的是這部分內(nèi)存帜乞。
總而言之司抱,GC 主要進(jìn)行回收的內(nèi)存是 JVM 中的方法區(qū)和堆;
3. Java 的 GC 什么時(shí)候回收垃圾黎烈?
在面試中經(jīng)常會碰到這樣一個(gè)問題(事實(shí)上筆者也碰到過):如何判斷一個(gè)對象已經(jīng)死去习柠?
很容易想到的一個(gè)答案是:對一個(gè)對象添加引用計(jì)數(shù)器。每當(dāng)有地方引用它時(shí)照棋,計(jì)數(shù)器值加 1资溃;當(dāng)引用失效時(shí),計(jì)數(shù)器值減 1.而當(dāng)計(jì)數(shù)器的值為 0 時(shí)這個(gè)對象就不會再被使用烈炭,判斷為已死溶锭。是不是簡單又直觀。然而符隙,很遺憾趴捅。這種做法是錯(cuò)誤的!為什么是錯(cuò)的呢霹疫?事實(shí)上拱绑,用引用計(jì)數(shù)法確實(shí)在大部分情況下是一個(gè)不錯(cuò)的解決方案,而在實(shí)際的應(yīng)用中也有不少案例丽蝎,但它卻無法解決對象之間的循環(huán)引用問題猎拨。比如對象 A 中有一個(gè)字段指向了對象 B,而對象 B 中也有一個(gè)字段指向了對象 A,而事實(shí)上他們倆都不再使用迟几,但計(jì)數(shù)器的值永遠(yuǎn)都不可能為 0消请,也就不會被回收,然后就發(fā)生了內(nèi)存泄露类腮。
所以臊泰,正確的做法應(yīng)該是怎樣呢?
在 Java蚜枢,C#等語言中缸逃,比較主流的判定一個(gè)對象已死的方法是:可達(dá)性分析(Reachability Analysis).
所有生成的對象都是一個(gè)稱為"GC Roots"的根的子樹。從 GC Roots 開始向下搜索厂抽,搜索所經(jīng)過的路徑稱為引用鏈(Reference Chain)需频,當(dāng)一個(gè)對象到 GC Roots 沒有任何引用鏈可以到達(dá)時(shí),就稱這個(gè)對象是不可達(dá)的(不可引用的)筷凤,也就是可以被 GC 回收了昭殉。無論是引用計(jì)數(shù)器還是可達(dá)性分析,判定對象是否存活都與引用有關(guān)藐守!那么挪丢,如何定義對象的引用呢?
我們希望給出這樣一類描述:當(dāng)內(nèi)存空間還夠時(shí)卢厂,能夠保存在內(nèi)存中乾蓬;如果進(jìn)行了垃圾回收之后內(nèi)存空間仍舊非常緊張,則可以拋棄這些對象慎恒。所以根據(jù)不同的需求任内,給出如下四種引用,根據(jù)引用類型的不同融柬,GC 回收時(shí)也會有不同的操作:
- 強(qiáng)引用(Strong Reference):Object obj = new Object();只要強(qiáng)引用還存在死嗦,GC 永遠(yuǎn)不會回收掉被引用的對象。
- 軟引用(Soft Reference):描述一些還有用但非必需的對象丹鸿。在系統(tǒng)將會發(fā)生內(nèi)存溢出之前越走,會把這些對象列入回收范圍進(jìn)行二次回收(即系統(tǒng)將會發(fā)生內(nèi)存溢出了,才會對他們進(jìn)行回收靠欢。)
- 弱引用(Weak Reference):程度比軟引用還要弱一些廊敌。這些對象只能生存到下次 GC 之前。當(dāng) GC 工作時(shí)门怪,無論內(nèi)存是否足夠都會將其回收(即只要進(jìn)行 GC骡澈,就會對他們進(jìn)行回收。)
- 虛引用(Phantom Reference):一個(gè)對象是否存在虛引用掷空,完全不會對其生存時(shí)間構(gòu)成影響肋殴。
關(guān)于方法區(qū)中需要回收的是一些廢棄的常量和無用的類囤锉。
1.廢棄的常量的回收。這里看引用計(jì)數(shù)就可以了护锤。沒有對象引用該常量就可以放心的回收了官地。
2.無用的類的回收。什么是無用的類呢烙懦?
A.該類所有的實(shí)例都已經(jīng)被回收驱入。也就是 Java 堆中不存在該類的任何實(shí)例;
B.加載該類的 ClassLoader 已經(jīng)被回收氯析;
C.該類對應(yīng)的 java.lang.Class 對象沒有任何地方被引用亏较,無法在任何地方通過反射訪問該類的方法。
總而言之:
- 對于堆中的對象掩缓,主要用可達(dá)性分析判斷一個(gè)對象是否還存在引用雪情,如果該對象沒有任何引用就應(yīng)該被回收。而根據(jù)我們實(shí)際對引用的不同需求你辣,又分成了 4 種引用巡通,每種引用的回收機(jī)制也是不同的。
- 對于方法區(qū)中的常量和類舍哄,當(dāng)一個(gè)常量沒有任何對象引用它扁达,它就可以被回收了。而對于類蠢熄,如果可以判定它為無用類,就可以被回收了炉旷。
4.在開發(fā)中遇到過內(nèi)存溢出么签孔?原因有哪些?解決方法有哪些窘行?
引起內(nèi)存溢出的原因有很多種饥追,常見的有以下幾種:
- 內(nèi)存中加載的數(shù)據(jù)量過于龐大,如一次從數(shù)據(jù)庫取出過多數(shù)據(jù)罐盔;
- 集合類中有對對象的引用但绕,使用完后未清空,使得 JVM 不能回收惶看;
- 代碼中存在死循環(huán)或循環(huán)產(chǎn)生過多重復(fù)的對象實(shí)體捏顺;
- 使用的第三方軟件中的 BUG;
- 啟動(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):
- 檢查對數(shù)據(jù)庫查詢中选脊,是否有一次獲得全部數(shù)據(jù)的查詢。一般來說脸甘,如果一次取十萬條記錄到內(nèi)存恳啥,就可能引起內(nèi)存溢出。這個(gè)問題比較隱蔽丹诀,在上線前钝的,數(shù)據(jù)庫中數(shù)據(jù)較少,不容易出問題铆遭,上線后硝桩,數(shù)據(jù)庫中數(shù)據(jù)多了,一次查詢就有可能引起內(nèi)存溢出枚荣。因此對于數(shù)據(jù)庫查詢盡量采用分頁的方式查詢碗脊。
- 檢查代碼中是否有死循環(huán)或遞歸調(diào)用。
- 檢查是否有大循環(huán)重復(fù)產(chǎn)生新對象實(shí)體橄妆。
- 檢查 List衙伶、MAP 等集合對象是否有使用完后,未清除的問題害碾。List矢劲、MAP 等集合對象會始終存有對對象的
引用,使得這些對象不能被 GC 回收慌随。
- 第四步芬沉,使用內(nèi)存查看工具動(dòng)態(tài)查看內(nèi)存使用情況。