個人理解:
因為在使用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、使用更好的編程技巧,比如更好的算法、更優的數據結構、更佳的解決策略等等。