眾所周知,在Java中內(nèi)存的分配和回收都是由java虛擬機(JVM)操作的,我們并不需要像C/C++那樣需要手動分配和釋放內(nèi)存,也正是因為有了JVM,才能使跨平臺成為了可能。說道垃圾回收,我們先要知道在java中內(nèi)存有哪些部分和作用。
一、java內(nèi)存組成
在JVM中,內(nèi)存主要分為一下幾個部分:程序計數(shù)器(PC)、方法棧、本地方法棧、堆、方法區(qū)
1、程序計數(shù)器:
操作系統(tǒng)在線程切換和恢復的時候,必須要記錄任務執(zhí)行的位置,這樣,當任務恢復的時候,才知道該從哪里繼續(xù)執(zhí)行,程序計數(shù)器就是記錄程序執(zhí)行的位置的,可以簡單理解成字節(jié)碼的行號指示器。
因為每個線程都是獨立執(zhí)行的,所以每個線程都有一個程序計數(shù)器,意味著,程序計數(shù)器是線程私有的。
對于java方法來說,程序計數(shù)器記錄的是字節(jié)碼的指令地址,對于native方法來說,它的值為空。
程序計數(shù)器是唯一一個不會發(fā)生OOM(out of memory)的地方。
2、方法棧
方法棧也叫作虛擬機棧,對每個即將執(zhí)行的方法,都會為其創(chuàng)建一個棧幀,對于java方法從開始執(zhí)行,到執(zhí)行結束,就是一個入棧到出棧的過程。
方法棧包含局部變量表,操作數(shù)棧,方法退出地址等。
方法棧是線程私有的。
當內(nèi)存不夠創(chuàng)建新的棧幀的時候,會拋出OutOfMemoryError;當方法棧調(diào)用深度超過棧的最大深度時,就會出現(xiàn)StackOverflowError,可以使用-Xss設置棧的大小。
3、本地方法棧
本地方法棧和方法棧很類似,但是只是執(zhí)行native方法時才會使用,JVM規(guī)范并沒有對本地方法棧的實現(xiàn)有強制性要求,所以會根據(jù)每個JVM的不同有不同的實現(xiàn)。本地方法棧也是線程私有的,同樣會拋出OutOfMemoryError和StackOverflowError。
4、方法區(qū)
JVM在java類在加載后,會將class文件代表的靜態(tài)存儲結構轉(zhuǎn)換為方法區(qū)的可運行結構,并將該結構存儲在方法區(qū)中。主要存儲的是類信息,常量池,類變量和JIT編譯后的數(shù)據(jù)。常量池主要包括字面量和符號引用。字面量比較接近java中常量的概念,比如字符串,final修飾的數(shù)據(jù)等,而符號引用只要是類和接口的全限定名,字段的名稱和描述符,方法的名稱和描述符。
方法區(qū)中的數(shù)據(jù)是共享的,對所有線程可見。
常量池在運行時也可以改變,比如調(diào)用字符串的intern()方法。
當內(nèi)存不夠分配新的常量空間的時候,也會拋出OutOfMemoryError。
5、堆
堆主要用來存儲新對象和數(shù)組,前面4部分的內(nèi)存都是由JVM進行管理和控制,對于開發(fā)人員來說,堆是可操作性最高的部分,當創(chuàng)建新的對象時,JVM會根據(jù)類信息在堆中分配一塊內(nèi)存給新的對象,當內(nèi)存不夠時,也會拋出OutOfMemoryError,同時,這部分區(qū)域也是垃圾收集器重要的收集對象。
二、垃圾收集算法
分配內(nèi)存很好理解,只要在需要分配內(nèi)存的地方申請相應的地址空間就可以,但是,內(nèi)存回收就沒這么簡單了,需要在程序運行過程中判斷對象是否還有用,對于沒有用的對象,需要對其占用的空間進行回收。這里我們簡單介紹下各種垃圾回收算法及其優(yōu)劣。
1、引用計數(shù)法
引用計數(shù)法就是記錄對象的引用,用來判斷對象是否還有用。對于一個對象,當有別的地方訪問它時,引用計數(shù)器數(shù)值+1,當不再引用它時,計數(shù)器-1,當對象的引用計數(shù)器數(shù)值為0的時候,說明該對象不再被使用,可以被回收。引用計數(shù)法實現(xiàn)很簡單,只需要對每個對象增加一個計數(shù)器即可,但是,對于對象間的循環(huán)引用就無能為力了。
2、標記清除算法
為了解決對象間循環(huán)引用的問題,有了標記-清除算法,就像它的名字一樣,等需要開始垃圾回收的時候,從GC Root開始,GC線程會把引用不到的對象標記出來(循環(huán)引用的對象自然就會被標記出來了),然后就把標記過的對象清除,這樣就可以釋放無用對象所占用的內(nèi)存了。但是,這里面有一個很嚴重問題-內(nèi)存碎片化嚴重,這樣會嚴重影響內(nèi)存的利用率。
舉個栗子,現(xiàn)在創(chuàng)建一個需要100MB內(nèi)存的對象,但是內(nèi)存只剩20MB,不夠,所以觸發(fā)了GC并且清理了120個對象騰出100MB的內(nèi)存,按說現(xiàn)在120MB的內(nèi)存已經(jīng)夠了,但多數(shù)情況下還是會拋出OOM異常,因為這些對象不是連續(xù)存儲的,所以清理出來的內(nèi)存也不是連續(xù)的,由于不存在連續(xù)的100MB的內(nèi)存,所以會拋異常。
3、復制算法
復制算法為了解決標記清除算法內(nèi)存碎片化的問題。復制算法主要工作流程為:
將內(nèi)存按照一定比例分為兩部分(通常為五五分),對象在只其中的一部分創(chuàng)建
在觸發(fā)GC后,從GC Root開始將能夠引用到的對象(有效對象)依次復制到另外一部分
完成所有有效對象的復制后,清除這部分內(nèi)存
下次GC又從另外一部分內(nèi)存復制到當前部分內(nèi)存,清除另一部分內(nèi)存
這樣,內(nèi)存碎片化的問題就解決了,但是,你會發(fā)現(xiàn),同一時間其實只有一部分內(nèi)存在使用,另外一部分都是在空閑的,這樣導致內(nèi)存利用率很低(五五分情況下內(nèi)存利用率最多到50%)
4、標記整理算法
標記整理算法是為了解決標記清除算法內(nèi)存碎片化和復制算法內(nèi)存利用率低的問題,它其實也是這兩種算法的結合。工作流程如下:
觸發(fā)GC后,從GC Root開始將有效對象標記出來
將有效對象依次移動到可用內(nèi)存開始處
將剩余部分內(nèi)存清
目前看來,標記整理算法解決了前面幾種方法的弊端,也沒有引入比較嚴重的新的問題,那么是不是就沒有任何問題,成為垃圾收集的銀彈了呢?答案是:當然不是。原因在于,前面所說的垃圾回收的過程中,除了GC線程外,jvm會停止所有java程序的運行,在jvm領域也稱為stop the world,這樣做的好處和壞處都顯而易見,好處就是減少了GC的復雜性,壞處就是java應用都被暫時停止,直到GC結束。在GC算法發(fā)展的過程中,細心的人會發(fā)現(xiàn),有些對象的生存周期很長,甚至和整個應用的生命周期相同,比如方法區(qū)中的類的字節(jié)碼所對應的結構,以及堆中代表類的Class對象;有的對象生命周期很短,比如方法中申明的變量所引用的對象,在方法結束后即失效。這種根據(jù)生命周期長短劃分對象,接近應用的生命周期的叫做永久代,生命周期長的叫做老年代,生命周期短的叫做新生代。
5、分代算法
其實分代算法不是一個具體的垃圾回收算法,它只是根據(jù)對象生命周期的長短,選擇不同的回收算法。比如說,復制算法,如果大多數(shù)都是新生代對象,每次復制的對象會很少,那么效率就會比較高;相反,如果大多數(shù)對象屬于老年代或者永久代,那么每次需要復制的對象就會特別多,效率就會很低。比如標記復制或者標記整理算法,如果是老年代居多,那么每次需要復制或者整理的對象就比較少,相反,如果是新生代居多,需要復制或者整理的內(nèi)存就會很多,效率自然就低很多。
所以分代算法主要就是根據(jù)生命周期的長短選擇合適的算法。一般來說,新生代居多合適用復制算法,老年代適合標記復制算法,永久代適合標記整理算法。
所以說在實際應用過程中,應該根據(jù)應用的特點選擇合適的垃圾收集算法。