1. 既然有 GC 機(jī)制,為什么還會(huì)有內(nèi)存泄露的情況
理論上 Java 因?yàn)橛欣厥諜C(jī)制(GC)不會(huì)存在內(nèi)存泄露問(wèn)題(這也是 Java 被廣泛使用于服務(wù)器端編程的一個(gè)重要原因)。然而在實(shí)際開(kāi)發(fā)中,可能會(huì)存在無(wú)用但可達(dá)的對(duì)象,這些對(duì)象不能被 GC 回收,因此也會(huì)導(dǎo)致內(nèi)存泄露的發(fā)生。
例如 hibernate 的 Session(一級(jí)緩存)中的對(duì)象屬于持久態(tài),垃圾回收器是不會(huì)回收這些對(duì)象的,然而這些對(duì)象中可能存在無(wú)用的垃圾對(duì)象,如果不及時(shí)關(guān)閉(close)或清空(flush)一級(jí)緩存就可能導(dǎo)致內(nèi)存泄露。
下面例子中的代碼也會(huì)導(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),乍看之下似乎沒(méi)有什么明顯的問(wèn)題,它甚至可以通過(guò)你編寫(xiě)的各種單元測(cè)試。然而其中的 pop 方法卻存在內(nèi)存泄露的問(wèn)題,當(dāng)我們用 pop 方法彈出棧中的對(duì)象時(shí),該對(duì)象不會(huì)被當(dāng)作垃圾回收,即使使用棧的程序不再引用這些對(duì)象,因?yàn)闂?nèi)部維護(hù)著對(duì)這些對(duì)象的過(guò)期引用(obsoletereference)。在支持垃圾回收的語(yǔ)言中,內(nèi)存泄露是很隱蔽的,這種內(nèi)存泄露其實(shí)就是無(wú)意識(shí)的對(duì)象保持。如果一個(gè)對(duì)象引用被無(wú)意識(shí)的保留起來(lái)了,那么垃圾回收器不會(huì)處理這個(gè)對(duì)象,也不會(huì)處理該對(duì)象引用的其他對(duì)象,即使這樣的對(duì)象只有少數(shù)幾個(gè),也可能會(huì)導(dǎo)致很多的對(duì)象被排除在垃圾回收之外,從而對(duì)性能造成重大影響,極端情況下會(huì)引發(fā) Disk Paging (物理內(nèi)存與硬盤(pán)的虛擬內(nèi)存交換數(shù)據(jù)),甚至造成 OutOfMemoryError。
2. Java 中為什么會(huì)有 GC 機(jī)制呢?
Java 中為什么會(huì)有 GC 機(jī)制呢?
? 安全性考慮;-- for security.
? 減少內(nèi)存泄露;-- erase memory leak in some degree.
? 減少程序員工作量。-- Programmers don't worry about memory releasing.
3. 對(duì)于 Java 的 GC 哪些內(nèi)存需要回收
內(nèi)存運(yùn)行時(shí) JVM 會(huì)有一個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū)來(lái)管理內(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)存基本上在類(lèi)結(jié)構(gòu)確定是哪個(gè)時(shí)就已知了,因此這 3 個(gè)區(qū)域的內(nèi)存分配和回收都是確定的,無(wú)需考慮內(nèi)存回收的問(wèn)題。
但方法區(qū)和堆就不同了,一個(gè)接口的多個(gè)實(shí)現(xiàn)類(lèi)需要的內(nèi)存可能不一樣,我們只有在程序運(yùn)行期間才會(huì)知道會(huì)創(chuàng)建哪些對(duì)象,這部分內(nèi)存的分配和回收都是動(dòng)態(tài)的,GC 主要關(guān)注的是這部分內(nèi)存。
總而言之,GC 主要進(jìn)行回收的內(nèi)存是 JVM 中的方法區(qū)和堆;
3. Java 的 GC 什么時(shí)候回收垃圾?
在面試中經(jīng)常會(huì)碰到這樣一個(gè)問(wèn)題(事實(shí)上筆者也碰到過(guò)):如何判斷一個(gè)對(duì)象已經(jīng)死去?
很容易想到的一個(gè)答案是:對(duì)一個(gè)對(duì)象添加引用計(jì)數(shù)器。每當(dāng)有地方引用它時(shí),計(jì)數(shù)器值加 1;當(dāng)引用失效時(shí),計(jì)數(shù)器值減 1.而當(dāng)計(jì)數(shù)器的值為 0 時(shí)這個(gè)對(duì)象就不會(huì)再被使用,判斷為已死。是不是簡(jiǎn)單又直觀。然而,很遺憾。這種做法是錯(cuò)誤的!為什么是錯(cuò)的呢?事實(shí)上,用引用計(jì)數(shù)法確實(shí)在大部分情況下是一個(gè)不錯(cuò)的解決方案,而在實(shí)際的應(yīng)用中也有不少案例,但它卻無(wú)法解決對(duì)象之間的循環(huán)引用問(wèn)題。比如對(duì)象 A 中有一個(gè)字段指向了對(duì)象 B,而對(duì)象 B 中也有一個(gè)字段指向了對(duì)象 A,而事實(shí)上他們倆都不再使用,但計(jì)數(shù)器的值永遠(yuǎn)都不可能為 0,也就不會(huì)被回收,然后就發(fā)生了內(nèi)存泄露。
所以,正確的做法應(yīng)該是怎樣呢?
在 Java,C#等語(yǔ)言中,比較主流的判定一個(gè)對(duì)象已死的方法是:可達(dá)性分析(Reachability Analysis).
所有生成的對(duì)象都是一個(gè)稱(chēng)為"GC Roots"的根的子樹(shù)。從 GC Roots 開(kāi)始向下搜索,搜索所經(jīng)過(guò)的路徑稱(chēng)為引用鏈(Reference Chain),當(dāng)一個(gè)對(duì)象到 GC Roots 沒(méi)有任何引用鏈可以到達(dá)時(shí),就稱(chēng)這個(gè)對(duì)象是不可達(dá)的(不可引用的),也就是可以被 GC 回收了。無(wú)論是引用計(jì)數(shù)器還是可達(dá)性分析,判定對(duì)象是否存活都與引用有關(guān)!那么,如何定義對(duì)象的引用呢?
我們希望給出這樣一類(lèi)描述:當(dāng)內(nèi)存空間還夠時(shí),能夠保存在內(nèi)存中;如果進(jìn)行了垃圾回收之后內(nèi)存空間仍舊非常緊張,則可以拋棄這些對(duì)象。所以根據(jù)不同的需求,給出如下四種引用,根據(jù)引用類(lèi)型的不同,GC 回收時(shí)也會(huì)有不同的操作:
- 強(qiáng)引用(Strong Reference):Object obj = new Object();只要強(qiáng)引用還存在,GC 永遠(yuǎn)不會(huì)回收掉被引用的對(duì)象。
- 軟引用(Soft Reference):描述一些還有用但非必需的對(duì)象。在系統(tǒng)將會(huì)發(fā)生內(nèi)存溢出之前,會(huì)把這些對(duì)象列入回收范圍進(jìn)行二次回收(即系統(tǒng)將會(huì)發(fā)生內(nèi)存溢出了,才會(huì)對(duì)他們進(jìn)行回收。)
- 弱引用(Weak Reference):程度比軟引用還要弱一些。這些對(duì)象只能生存到下次 GC 之前。當(dāng) GC 工作時(shí),無(wú)論內(nèi)存是否足夠都會(huì)將其回收(即只要進(jìn)行 GC,就會(huì)對(duì)他們進(jìn)行回收。)
- 虛引用(Phantom Reference):一個(gè)對(duì)象是否存在虛引用,完全不會(huì)對(duì)其生存時(shí)間構(gòu)成影響。
關(guān)于方法區(qū)中需要回收的是一些廢棄的常量和無(wú)用的類(lèi)。
1.廢棄的常量的回收。這里看引用計(jì)數(shù)就可以了。沒(méi)有對(duì)象引用該常量就可以放心的回收了。
2.無(wú)用的類(lèi)的回收。什么是無(wú)用的類(lèi)呢?
A.該類(lèi)所有的實(shí)例都已經(jīng)被回收。也就是 Java 堆中不存在該類(lèi)的任何實(shí)例;
B.加載該類(lèi)的 ClassLoader 已經(jīng)被回收;
C.該類(lèi)對(duì)應(yīng)的 java.lang.Class 對(duì)象沒(méi)有任何地方被引用,無(wú)法在任何地方通過(guò)反射訪問(wèn)該類(lèi)的方法。
總而言之:
- 對(duì)于堆中的對(duì)象,主要用可達(dá)性分析判斷一個(gè)對(duì)象是否還存在引用,如果該對(duì)象沒(méi)有任何引用就應(yīng)該被回收。而根據(jù)我們實(shí)際對(duì)引用的不同需求,又分成了 4 種引用,每種引用的回收機(jī)制也是不同的。
- 對(duì)于方法區(qū)中的常量和類(lèi),當(dāng)一個(gè)常量沒(méi)有任何對(duì)象引用它,它就可以被回收了。而對(duì)于類(lèi),如果可以判定它為無(wú)用類(lèi),就可以被回收了。
4.在開(kāi)發(fā)中遇到過(guò)內(nèi)存溢出么?原因有哪些?解決方法有哪些?
引起內(nèi)存溢出的原因有很多種,常見(jiàn)的有以下幾種:
- 內(nèi)存中加載的數(shù)據(jù)量過(guò)于龐大,如一次從數(shù)據(jù)庫(kù)取出過(guò)多數(shù)據(jù);
- 集合類(lèi)中有對(duì)對(duì)象的引用,使用完后未清空,使得 JVM 不能回收;
- 代碼中存在死循環(huán)或循環(huán)產(chǎn)生過(guò)多重復(fù)的對(duì)象實(shí)體;
- 使用的第三方軟件中的 BUG;
- 啟動(dòng)參數(shù)內(nèi)存值設(shè)定的過(guò)??;
內(nèi)存溢出的解決方案:
- 第一步,修改 JVM 啟動(dòng)參數(shù),直接增加內(nèi)存。(-Xms,-Xmx 參數(shù)一定不要忘記加。)
- 第二步,檢查錯(cuò)誤日志,查看“OutOfMemory”錯(cuò)誤前是否有其它異?;蝈e(cuò)誤。
- 第三步,對(duì)代碼進(jìn)行走查和分析,找出可能發(fā)生內(nèi)存溢出的位置。
重點(diǎn)排查以下幾點(diǎn):
- 檢查對(duì)數(shù)據(jù)庫(kù)查詢(xún)中,是否有一次獲得全部數(shù)據(jù)的查詢(xún)。一般來(lái)說(shuō),如果一次取十萬(wàn)條記錄到內(nèi)存,就可能引起內(nèi)存溢出。這個(gè)問(wèn)題比較隱蔽,在上線前,數(shù)據(jù)庫(kù)中數(shù)據(jù)較少,不容易出問(wèn)題,上線后,數(shù)據(jù)庫(kù)中數(shù)據(jù)多了,一次查詢(xún)就有可能引起內(nèi)存溢出。因此對(duì)于數(shù)據(jù)庫(kù)查詢(xún)盡量采用分頁(yè)的方式查詢(xún)。
- 檢查代碼中是否有死循環(huán)或遞歸調(diào)用。
- 檢查是否有大循環(huán)重復(fù)產(chǎn)生新對(duì)象實(shí)體。
- 檢查 List、MAP 等集合對(duì)象是否有使用完后,未清除的問(wèn)題。List、MAP 等集合對(duì)象會(huì)始終存有對(duì)對(duì)象的
引用,使得這些對(duì)象不能被 GC 回收。
- 第四步,使用內(nèi)存查看工具動(dòng)態(tài)查看內(nèi)存使用情況。