GC垃圾回收機制


個人理解:

  因為在使用JAVA創建一個類或者對象后,難免會存在以后不使用的情況,為了減少其繼續再占用內存,必須建立一套清理垃圾的機制,但是怎么判斷什么樣的才算是不使用的垃圾呢,這里面進行了判斷並標記分類,然后根據不同的標記再進行不同的處理。不過世事無完美之說,其也是存在弊端的(開銷通常很大,而且它的運行具有不確定性),為了避免,我們還是在正常工作中,養成一個好的編程習慣。詳情參照https://www.cnblogs.com/jeffwongishandsome/p/talk-about-GC-and-how-to-use-GC-better.html(侵刪)和https://www.cnblogs.com/zhguang/p/3257367.html(侵刪)以及https://www.cnblogs.com/xiaoxi/p/6486852.html(侵刪)。其都解釋的比較詳細!

一、JAVA內存區域:

在Java運行時的數據區里,由JVM(JAVA虛擬機)管理的內存區域分為下圖幾個模塊:

其中:

1,程序計數器(Program Counter Register):程序計數器是一個比較小的內存區域,用於指示當前線程所執行的字節碼執行到了第幾行,可以理解為是當前線程的行號指示器。字節碼解釋器在工作時,會通過改變這個計數器的值來取下一條語句指令。

  每個程序計數器只用來記錄一個線程的行號,所以它是線程私有(一個線程就有一個程序計數器)的。

  如果程序執行的是一個Java方法,則計數器記錄的是正在執行的虛擬機字節碼指令地址;如果正在執行的是一個本地(native,由C語言編寫完成)方法,則計數器的值為Undefined,由於程序計數器只是記錄當前指令地址,所以不存在內存溢出的情況,因此,程序計數器也是所有JVM內存區域中唯一一個沒有定義OutOfMemoryError的區域。

2,虛擬機棧(JVM Stack):一個線程的每個方法在執行的同時,都會創建一個棧幀(Statck Frame),棧幀中存儲的有局部變量表、操作站、動態鏈接、方法出口等,當方法被調用時,棧幀在JVM棧中入棧,當方法執行完成時,棧幀出棧。

  局部變量表中存儲着方法的相關局部變量,包括各種基本數據類型,對象的引用,返回地址等。在局部變量表中,只有long和double類型會占用2個局部變量空間(Slot,對於32位機器,一個Slot就是32個bit),其它都是1個Slot。需要注意的是,局部變量表是在編譯時就已經確定好的,方法運行所需要分配的空間在棧幀中是完全確定的,在方法的生命周期內都不會改變。

  虛擬機棧中定義了兩種異常,如果線程調用的棧深度大於虛擬機允許的最大深度,則拋出StatckOverFlowError(棧溢出);不過多數Java虛擬機都允許動態擴展虛擬機棧的大小(有少部分是固定長度的),所以線程可以一直申請棧,直到內存不足,此時,會拋出OutOfMemoryError(內存溢出)。

  每個線程對應着一個虛擬機棧,因此虛擬機棧也是線程私有的。

3,本地方法棧(Native Method Statck):本地方法棧在作用,運行機制,異常類型等方面都與虛擬機棧相同,唯一的區別是:虛擬機棧是執行Java方法的,而本地方法棧是用來執行native方法的,在很多虛擬機中(如Sun的JDK默認的HotSpot虛擬機),會將本地方法棧與虛擬機棧放在一起使用。

  本地方法棧也是線程私有的。

4,堆區(Heap):堆區是理解Java GC機制最重要的區域,沒有之一。在JVM所管理的內存中,堆區是最大的一塊,堆區也是Java GC機制所管理的主要內存區域,堆區由所有線程共享,在虛擬機啟動時創建。堆區的存在是為了存儲對象實例,原則上講,所有的對象都在堆區上分配內存(不過現代技術里,也不是這么絕對的,也有棧上直接分配的)。

  一般的,根據Java虛擬機規范規定,堆內存需要在邏輯上是連續的(在物理上不需要),在實現時,可以是固定大小的,也可以是可擴展的,目前主流的虛擬機都是可擴展的。如果在執行垃圾回收之后,仍沒有足夠的內存分配,也不能再擴展,將會拋出OutOfMemoryError:Java heap space異常。

  關於堆區的內容還有很多,將在下節“Java內存分配機制”中詳細介紹。

5,方法區(Method Area):在Java虛擬機規范中,將方法區作為堆的一個邏輯部分來對待,但事實上,方法區並不是堆(Non-Heap);另外,不少人的博客中,將Java GC的分代收集機制分為3個代:青年代,老年代,永久代,這些作者將方法區定義為“永久代”,這是因為,對於之前的HotSpot Java虛擬機的實現方式中,將分代收集的思想擴展到了方法區,並將方法區設計成了永久代。不過,除HotSpot之外的多數虛擬機,並不將方法區當做永久代,HotSpot本身,也計划取消永久代。本文中,由於筆者主要使用Oracle JDK6.0,因此仍將使用永久代一詞。

  方法區是各個線程共享的區域,用於存儲已經被虛擬機加載的類信息(即加載類時需要加載的信息,包括版本、field、方法、接口等信息)、final常量、靜態變量、編譯器即時編譯的代碼等。

  方法區在物理上也不需要是連續的,可以選擇固定大小或可擴展大小,並且方法區比堆還多了一個限制:可以選擇是否執行垃圾收集。一般的,方法區上執行的垃圾收集是很少的,這也是方法區被稱為永久代的原因之一(HotSpot),但這也不代表着在方法區上完全沒有垃圾收集,其上的垃圾收集主要是針對常量池的內存回收和對已加載類的卸載。

  在方法區上進行垃圾收集,條件苛刻而且相當困難,效果也不令人滿意,所以一般不做太多考慮,可以留作以后進一步深入研究時使用。

  在方法區上定義了OutOfMemoryError:PermGen space異常,在內存不足時拋出。

  運行時常量池(Runtime Constant Pool)是方法區的一部分,用於存儲編譯期就生成的字面常量、符號引用、翻譯出來的直接引用(符號引用就是編碼是用字符串表示某個變量、接口的位置,直接引用就是根據符號引用翻譯出來的地址,將在類鏈接階段完成翻譯);運行時常量池除了存儲編譯期常量外,也可以存儲在運行時間產生的常量(比如String類的intern()方法,作用是String維護了一個常量池,如果調用的字符“abc”已經在常量池中,則返回池中的字符串地址,否則,新建一個常量加入池中,並返回地址)。

6,直接內存(Direct Memory):直接內存並不是JVM管理的內存,可以這樣理解,直接內存,就是JVM以外的機器內存,比如,你有4G的內存,JVM占用了1G,則其余的3G就是直接內存,JDK中有一種基於通道(Channel)和緩沖區(Buffer)的內存分配方式,將由C語言實現的native函數庫分配在直接內存中,用存儲在JVM堆中的DirectByteBuffer來引用。由於直接內存收到本機器內存的限制,所以也可能出現OutOfMemoryError的異常。

Java對象的訪問方式

一般來說,一個Java的引用訪問涉及到3個內存區域:JVM棧,堆,方法區。

 以最簡單的本地變量引用:Object obj = new Object()為例:

①、Object obj表示一個本地引用,存儲在JVM棧的本地變量表中,表示一個reference類型數據;

②、new Object()作為實例對象數據存儲在堆中;

③、堆中還記錄了Object類的類型信息(接口、方法、field、對象類型等)的地址,這些地址所執行的數據存儲在方法區中;

二、需要GC的原因:

應用程序對資源操作,通常簡單分為以下幾個步驟:

1、為對應的資源分配內存

2、初始化內存

3、使用資源

4、清理資源

5、釋放內存

應用程序對資源(內存使用)管理的方式,常見的一般有如下幾種:

1、手動管理:C,C++

2、計數管理:COM

3、自動管理:.NET,Java,PHP,GO…

但是,手動管理和計數管理的復雜性很容易產生以下典型問題:

1.程序員忘記去釋放內存

2.應用程序訪問已經釋放的內存

產生的后果很嚴重,常見的如內存泄露、數據內容亂碼,而且大部分時候,程序的行為會變得怪異而不可預測,還有Access Violation等。

.NET、Java等給出的解決方案,就是通過自動垃圾回收機制GC進行內存管理。這樣,問題1自然得到解決,問題2也沒有存在的基礎。

總結:無法自動化的內存管理方式極容易產生bug,影響系統穩定性,尤其是線上多服務器的集群環境,程序出現執行時bug必須定位到某台服務器然后dump內存再分析bug所在,極其打擊開發人員編程積極性,而且源源不斷的類似bug讓人厭惡。

三、GC如何工作:

GC的工作流程主要分為如下幾個步驟:

1、標記(Mark)---GC的根節點也即GC Root

2、計划(Plan)

3、清理(Sweep)

4、引用更新(Relocate)

5、壓縮(Compact)

 

在Java中,可以當做GC Root的對象有以下幾種:

1、虛擬機(JVM)棧中的引用的對象

2、方法區中的類靜態屬性引用的對象

3、方法區中的常量引用的對象(主要指聲明為final的常量值)

4、本地方法棧中JNI的引用的對象

 四、什么時候發生GC;

1、當應用程序分配新的對象,GC的代的預算大小已經達到閾值,比如GC的第0代已滿

2、代碼主動顯式調用System.GC.Collect()

3、其他特殊情況,比如,windows報告內存不足、CLR卸載AppDomain、CLR關閉,甚至某些極端情況下系統參數設置改變也可能導致GC回收

五、建議:

由於GC的代價很大,平時開發中注意一些良好的編程習慣有可能對GC有積極正面的影響,否則有可能產生不良效果。

1、盡量不要new很大的object,大對象(>=85000Byte)直接歸為G2代,GC回收算法從來不對大對象堆(LOH)進行內存壓縮整理,因為在堆中下移85000字節或更大的內存塊會浪費太多CPU時間。

2、不要頻繁的new生命周期很短object,這樣頻繁垃圾回收頻繁壓縮有可能會導致很多內存碎片,可以使用設計良好穩定運行的對象池(ObjectPool)技術來規避這種問題。

3、使用更好的編程技巧,比如更好的算法、更優的數據結構、更佳的解決策略等等。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM