JVM垃圾收集針對的是主要是堆中的垃圾,因為執行緒啟動時在堆疊中分配空間,執行緒結束,自動釋放空間,不需要即時監控;方法區主要儲存類別資訊以及靜態變數與常數,通常在整個程式運行期間都有效,不存在需要回收的對象。
垃圾指的是無法被線程訪問的對象,一個對像只有對線程可見,可被線程訪問,才可用,也可以簡單理解為沒有任何引用的對象。嚴格來說,沒有任何引用對象的表述縮小了垃圾的範疇,比如在循環引用中,一個對象A引用了另一個對象B,對象B又引用了A,A與B的引用關係只存在於兩者之間,沒有任何一個外界對象引用A或B,那麼A與B就無法被納入程序運行之中,即不能被任何一個線程訪問,就成了無用對象,按照定義垃圾的動機,不再被使用的對象即為垃圾,A與B自然就屬於垃圾。
有時也將垃圾稱為無用對象,非垃圾稱作存活對象。
無用物件持續佔有記憶體導致記憶體浪費的現象叫做記憶體外洩。記憶體外洩的根本原因是長生命週期的物件使用完畢後依然持有短生命週期物件的引用,例如下面的程式碼:
##public void test02(){ Object obj = new Object(); obj.doSome();//调用了对象中的doSome方法以后,就不再使用该对象,但是依然持有对象的引用obj ...................... }
在方法中創建了一個對象,創建對象的目的只是為了訪問對像中的方法doSome,方法訪問完畢,就不再訪問對象,而此時引用變量obj依然指向該對象,既然對象存在引用,該物件就不會被視為垃圾,但物件已經變成無用對象,應該解除引用,以便垃圾回收器回收該物件佔用的記憶體空間,「obj=null;」解除引用。
垃圾回收器回收的不是對象,而是無用物件佔用的記憶體空間,以使空間可以被再次使用。
在記憶體分配與回收過程中產生的較小的、不連續的可用空間,如圖所示:
由於等待回收的空間分散排列,回收完成以後,可用空間被已用空間分割成一個個獨立的小單元,由於這些單元體積很小,無法存放大的數據,需要為大的數據另外開闢空間,就造成了內存浪費。
物件建立時給物件加入一個引用計數器,每當有變數引用該對象,計數器就加1,變數釋放了引用,計數器減1,任何時刻計數器為0,就表示沒有物件引用該對象,物件成了垃圾。
引用計數演算法的天生缺陷是,無法解決循環引用問題,即循環引用的兩個物件即使不再被使用,依然被視為可用對象,不會被垃圾回收器回收。 JVM垃圾回收機制採用的不是該演算法,而是下面的可達性分析演算法。
可達性分析演算法採用追蹤方式判斷物件是否存活,任何可被追蹤即可到達的對像都是存活對象,不可追蹤即不可到達的對像都是無用對象。追蹤的起點是目前正在被訪問的任何一個對象,從該對像出發,找到該對象引用的對象,依次循環,形成一個引用鏈,不在引用鏈上的對象就是不被線程使用的對象,就是垃圾。如下圖,左側形成了一條引用鏈,鏈上的所有物件都是存活對象,右側物件雖然彼此之間存在著引用關係,脫離線程的引用鏈,因此是無用物件。
這種分析是動態的,而非靜態的,隨著物件引用關係的變化而改變。
不同物件的生命週期不同,為了及時回收內存,對生命週期短的物件頻繁執行垃圾收集操作,而生命週期長的物件比較穩定,可以長期存在,垃圾收集掃描次數少,因此為了節省開銷,降低垃圾收集次數,將不同生命週期的物件分別儲存於記憶體的不同區域,以便採用不同的垃圾收集策略。
JVM將物件的儲存空間分為三個區:新生代、舊年代、永久代。
設立新生代是為了快速回收生命週期短的對象,所有新生成的對象首先放在新生代中。新生代分為3部分:eden、from survivor、to survivor,空間比例為8:1:1。
新建立的物件首先放在eden,執行多次gc(垃圾收集),eden區滿了以後,那些還存活的對象會被轉移到from survivor區,from survivor滿了以後,存活的對像被轉移到to survivor,to surviror區滿了以後,存活的對象就被提升到老年代。
JVM對年輕代中的物件頻繁執行gc,絕大多數物件會在年輕代被回收,很少一部分進入老年代。
老年代中的對像都是經過多次gc存活下來的對象,生命週期較長,比較穩定,因此執行gc操作次數較少。
永久代也就是方法區,儲存的是類別資訊以及靜態變數與常數,生命週期與應用程式相同,一般不需要垃圾收集器處理。
垃圾收集演算法是垃圾確認以後實際收集時採用的演算法,常見的演算法有3種:
先標記無用對象,然後回收物件佔用的記憶體空間,會導致記憶體碎片,基本上不採用該演算法。
將記憶體空間分成多個區域,垃圾收集時將一個區域內的存活物件全部複製到另一個區域,然後清空該區域,這樣就不會產生記憶體碎片。新生代垃圾收集時採用此演算法,從eden的區複製到from survivor區,再複製到to survivor區,因為存活物件較少,複製時佔用空間較少。
首先標記需要清除的對象,將所有存活對象移動到一端,然後清除所有無用對象。老年代不像新生代,每次gc之後存活對象較多,採用複製算法佔用內存較大,採用“標記-整理”算法既節省了內存,又不會導致內存碎片。
不同的世代有不同的gc機制:Scavenge GC與Full GC。
當新生代eden區域已滿,新生成的物件申請空間失敗,會觸發Scavenge GC,對eden區域進行gc操縱,清除無用對象,騰出空間。 Scavenge GC只對新生代起作用。
當老年代已滿或持久代已滿,或明確呼叫了System.gc()方法,會觸發Full GC,對所有物件儲存區域進行gc,資源耗費較大,應該較少Full GC次數。
告訴JVM啟動垃圾回收器,垃圾回收器採用守護線程,不一定立即啟動,具體何時啟動無法控制。另外資源消耗大,一般情況下不要明確呼叫。
物件範圍的方法。在JVM確認一個物件無法被存取後調用,只能被調用一次,通常用來釋放連線資源。此方法被呼叫後,垃圾回收器不會立即回收物件佔用的空間,因為該方法執行過程中,物件可能會重新被訪問,而是在下一次gc時,確認物件依然無法被訪問,才回收物件佔用的空間。
#避免使用靜態變量,因為靜態變數的生命週期與應用程式相同,即使長期不被使用,仍佔據記憶體空間。
資源連接,如OutputStream\InputStream\Connection\Socket,使用完畢,應立即關閉,及時釋放資源。
盡量不要明確呼叫System.gc(),因為方法可能觸發Full GC,開銷較大。
減少臨時變數的使用,因為方法執行完畢後,臨時變數變成垃圾,大量的臨時變數會提高gc次數,增加系統消耗。
物件引用完畢後,及時釋放引用,以便及時回收記憶體空間。
盡量使用可變對象,降低不可變對象的使用次數。
盡量使用基本型別變數來取代對應的包裝類,因為基本型別變數所佔用的資源比對應的包裝類少很多。
分散創建於刪除對象,因為集中創建對象,需要大量的空間,很可能觸發Full GC,瞬間增大系統消耗。集中刪除對象,瞬間出現大量無用對象,也有可能觸發Full GC。
參考:
##
#
#
#
#
以上是java--垃圾回收機制詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!