轉自https://blog.csdn.net/weixin_39788856/article/details/80388002
1、Java垃圾回收機制
GC,即就是Java垃圾回收機制。目前主流的JVM(HotSpot)采用的是分代收集算法。作為Java開發者,一般不需要專門編寫內存回收和垃圾清理代碼,對內存泄露和溢出的問題。與C++不同的是,Java采用的是類似於樹形結構的可達性分析法來判斷對象是否還存在引用。即:從gcroot開始,把所有可以搜索得到的對象標記為存活對象。缺點就是:1. 有可能不知不覺浪費了很多內存。2. JVM花費過多時間來進行內存回收。3. 內存泄露
理解Java的垃圾回收機制,就要從:“什么時候”,“對什么東西”,“做了什么”三個方面來具體分析。
第一:“什么時候”即就是GC觸發的條件。GC觸發的條件有兩種。(1)程序調用System.gc時可以觸發;(2)系統自身來決定GC觸發的時機。系統判斷GC觸發的依據:根據Eden區和From Space區的內存大小來決定。當內存大小不足時,則會啟動GC線程並停止應用線程。
第二:“對什么東西”籠統的認為是Java對象。但是准確來講,GC操作的對象分為:通過可達性分析法無法搜索到的對象和可以搜索到的對象。對於搜索不到的方法進行標記。
第三:“做了什么”最淺顯的理解為釋放對象。但是從GC的底層機制可以看出,對於可以搜索到的對象進行復制操作,對於搜索不到的對象,調用finalize()方法進行釋放。
具體過程:當GC線程啟動時,會通過可達性分析法把Eden區和From Space區的存活對象復制到To Space區,然后把Eden Space和From Space區的對象釋放掉。當GC輪訓掃描To Space區一定次數后,把依然存活的對象復制到老年代,然后釋放To Space區的對象。
對於用可達性分析法搜索不到的對象,GC並不一定會回收該對象。要完全回收一個對象,至少需要經過兩次標記的過程:
第一次標記:對於一個沒有其他引用的對象,篩選該對象是否有必要執行finalize()方法,如果沒有執行必要,則意味可直接回收。(篩選依據:是否復寫或執行過finalize()方法;因為finalize方法只能被執行一次)。
第二次標記:如果被篩選判定位有必要執行,則會放入FQueue隊列,並自動創建一個低優先級的finalize線程來執行釋放操作。如果在一個對象釋放前被其他對象引用,則該對象會被移除FQueue隊列。
2、JVM內存管理
根據JVM規范,JVM把內存划分了如下幾個區域:. 方法區、堆區、 本地方法棧、虛擬機棧、程序計數器 。其中,方法區和堆是所有線程共享的。
2.1 方法區
方法區存放了要加載的類的信息(如類名,修飾符)、類中的靜態變量、final定義的常量、類中的field、方法信息,當開發人員調用類對象中的getName、isInterface等方法來獲取信息時,這些數據都來源於方法區。方法區是全局共享的,在一定條件下它也會被GC。當方法區使用的內存超過它允許的大小時,就會拋出OutOfMemory:PermGen Space異常。
在Hotspot虛擬機中,這塊區域對應的是Permanent Generation(持久代),一般的,方法區上執行的垃圾收集是很少的,因此方法區又被稱為持久代的原因之一,但這也不代表着在方法區上完全沒有垃圾收集,其上的垃圾收集主要是針對常量池的內存回收和對已加載類的卸載。在方法區上進行垃圾收集,條件苛刻而且相當困難。
運行時常量池(Runtime Constant Pool)是方法區的一部分,用於存儲編譯期就生成的字面常量、符號引用、翻譯出來的直接引用(符號引用就是編碼是用字符串表示某個變量、接口的位置,直接引用就是根據符號引用翻譯出來的地址,將在類鏈接階段完成翻譯);運行時常量池除了存儲編譯期常量外,也可以存儲在運行時間產生的常量,比如String類的intern()方法,作用是String維護了一個常量池,如果調用的字符“abc”已經在常量池中,則返回池中的字符串地址,否則,新建一個常量加入池中,並返回地址。
2.2 堆
堆區是理解Java GC機制最重要的區域。在JVM所管理的內存中,堆區是最大的一塊,堆區也是JavaGC機制所管理的主要內存區域,堆區由所有線程共享,在虛擬機啟動時創建。堆區用來存儲對象實例及數組值,可以認為java中所有通過new創建的對象都在此分配。
對於堆區大小,可以通過參數-Xms和-Xmx來控制,-Xms為JVM啟動時申請的最新heap內存,默認為物理內存的1/64但小於1GB;-Xmx為JVM可申請的最大Heap內存,默認為物理內存的1/4但小於1GB,默認當剩余堆空間小於40%時,JVM會增大Heap到-Xmx大小,可通過-XX:MinHeapFreeRadio參數來控制這個比例;當空余堆內存大於70%時,JVM會減小Heap大小到-Xms指定大小,可通過-XX:MaxHeapFreeRatio來指定這個比例。對於系統而言,為了避免在運行期間頻繁的調整Heap大小,我們通常將-Xms和-Xmx設置成一樣。為了讓內存回收更加高效,從Sun JDK 1.2開始對堆采用了分代管理方式,如下圖所示:
年輕代(Young Generation)
對象在被創建時,內存首先是在年輕代進行分配(注意,大對象可以直接在老年代分配)。當年輕代需要回收時會觸發Minor GC(也稱作Young GC)。
年輕代由Eden Space和兩塊相同大小的Survivor Space(又稱From Space和To Space)構成,Eden區和Servior區的內存比為8:1:1,可通過-Xmn參數來調整新生代大小,也可通過-XX:SurvivorRadio來調整Eden Space和Survivor Space大小。不同的GC方式會按不同的方式來按此值划分Eden Space和Survivor Space,有些GC方式還會根據運行狀況來動態調整Eden、From Space、To Space的大小。
年輕代的Eden區內存是連續的,所以其分配會非常快;同樣Eden區的回收也非常快(因為大部分情況下Eden區對象存活時間非常短,而Eden區采用的復制回收算法,此算法在存活對象比例很少的情況下非常高效)。如果在執行垃圾回收之后,仍沒有足夠的內存分配,也不能再擴展,將會拋出OutOfMemoryError:Java Heap Space異常。
老年代(Old Generation)
老年代用於存放在年輕代中經多次垃圾回收仍然存活的對象,可以理解為比較老一點的對象,例如緩存對象;新建的對象也有可能在老年代上直接分配內存,這主要有兩種情況:一種為大對象,可以通過啟動參數設置-XX:PretenureSizeThreshold=1024,表示超過多大時就不在年輕代分配,而是直接在老年代分配。此參數在年輕代采用Parallel Scavenge GC時無效,因為其會根據運行情況自己決定什么對象直接在老年代上分配內存;另一種為大的數組對象,且數組對象中無引用外部對象。
當老年代滿了的時候就需要對老年代進行垃圾回收,老年代的垃圾回收稱作Full GC。老年代所占用的內存大小為-Xmx對應的值減去-Xmn對應的值。
2.3 本地方法棧(Native Method Stack)
本地方法棧用於支持native方法的執行,存儲了每個native方法調用的狀態。本地方法棧和虛擬機方法棧運行機制一致,它們唯一的區別就是,虛擬機棧是執行Java方法的,而本地方法棧是用來執行native方法的,在很多虛擬機中(如Sun的JDK默認的HotSpot虛擬機),會將本地方法棧與虛擬機棧放在一起使用。
2.4 程序計數器(Program Counter Register)
程序計數器是一個比較小的內存區域,可能是CPU寄存器或者操作系統內存,其主要用於指示當前線程所執行的字節碼執行到了第幾行,可以理解為是當前線程的行號指示器。字節碼解釋器在工作時,會通過改變這個計數器的值來取下一條語句指令。 每個程序計數器只用來記錄一個線程的行號,所以它是線程私有(一個線程就有一個程序計數器)的。
如果程序執行的是一個Java方法,則計數器記錄的是正在執行的虛擬機字節碼指令地址;如果正在執行的是一個本地(native,由C語言編寫完成)方法,則計數器的值為Undefined,由於程序計數器只是記錄當前指令地址,所以不存在內存溢出的情況,因此,程序計數器也是所有JVM內存區域中唯一一個沒有定義OutOfMemoryError的區域。
2.5 虛擬機棧(JVM Stack)
虛擬機棧占用的是操作系統內存,每個線程都對應着一個虛擬機棧,它是線程私有的,而且分配非常高效。一個線程的每個方法在執行的同時,都會創建一個棧幀(Statck Frame),棧幀中存儲的有局部變量表、操作站、動態鏈接、方法出口等,當方法被調用時,棧幀在JVM棧中入棧,當方法執行完成時,棧幀出棧。
局部變量表中存儲着方法的相關局部變量,包括各種基本數據類型,對象的引用,返回地址等。在局部變量表中,只有long和double類型會占用2個局部變量空間(Slot,對於32位機器,一個Slot就是32個bit),其它都是1個Slot。需要注意的是,局部變量表是在編譯時就已經確定好的,方法運行所需要分配的空間在棧幀中是完全確定的,在方法的生命周期內都不會改變。
虛擬機棧中定義了兩種異常,如果線程調用的棧深度大於虛擬機允許的最大深度,則拋出StatckOverFlowError(棧溢出);不過多數Java虛擬機都允許動態擴展虛擬機棧的大小(有少部分是固定長度的),所以線程可以一直申請棧,直到內存不足,此時,會拋出OutOfMemoryError(內存溢出)。
3、虛擬機中GC的過程
1,在初始階段,新創建的對象被分配到Eden區,survivor的兩塊空間都為空。
2,當Eden區滿了的時候,minor garbage 被觸發 。
3,經過掃描與標記,存活的對象被復制到S0,不存活的對象被回收
4,在下一次的Minor GC中,Eden區的情況和上面一致,沒有引用的對象被回收,存活的對象被復制到survivor區。然而在survivor區,S0的所有的數據都被復制到S1,需要注意的是,在上次minor GC過程中移動到S0中的兩個對象在復制到S1后其年齡要加1。此時Eden區S0區被清空,所有存活的數據都復制到了S1區,並且S1區存在着年齡不一樣的對象,過程如下圖所示:
5,再下一次MinorGC則重復這個過程,這一次survivor的兩個區對換,存活的對象被復制到S0,存活的對象年齡加1,Eden區和另一個survivor區被清空。
6,再經過幾次Minor GC之后,當存活對象的年齡達到一個閾值之后(可通過參數配置,默認是8),就會被從年輕代Promotion到老年代。
7,隨着MinorGC一次又一次的進行,不斷會有新的對象被promote到老年代。
8,上面基本上覆蓋了整個年輕代所有的回收過程。最終,MajorGC將會在老年代發生,老年代的空間將會被清除和壓縮。
從上面的過程可以看出,Eden區是連續的空間,且Survivor總有一個為空。經過一次GC和復制,一個Survivor中保存着當前還活着的對象,而Eden區和另一個Survivor區的內容都不再需要了,可以直接清空,到下一次GC時,兩個Survivor的角色再互換。因此,這種方式分配內存和清理內存的效率都極高,這種垃圾回收的方式就是著名的“停止-復制(Stop-and-copy)”清理法(將Eden區和一個Survivor中仍然存活的對象拷貝到另一個Survivor中),這不代表着停止復制清理法很高效,其實,它也只在這種情況下(基於大部分對象存活周期很短的事實)高效,如果在老年代采用停止復制,則是非常不合適的。
老年代存儲的對象比年輕代多得多,而且不乏大對象,對老年代進行內存清理時,如果使用停止-復制算法,則相當低效。一般,老年代用的算法是標記-壓縮算法,即:標記出仍然存活的對象(存在引用的),將所有存活的對象向一端移動,以保證內存的連續。在發生Minor GC時,虛擬機會檢查每次晉升進入老年代的大小是否大於老年代的剩余空間大小,如果大於,則直接觸發一次Full GC,否則,就查看是否設置了-XX:+HandlePromotionFailure(允許擔保失敗),如果允許,則只會進行MinorGC,此時可以容忍內存分配失敗;如果不允許,則仍然進行Full GC(這代表着如果設置-XX:+Handle PromotionFailure,則觸發MinorGC就會同時觸發Full GC,哪怕老年代還有很多內存,所以,最好不要這樣做)。
關於方法區即永久代的回收,永久代的回收有兩種:常量池中的常量,無用的類信息,常量的回收很簡單,沒有引用了就可以被回收。對於無用的類進行回收,必須保證3點:
1. 類的所有實例都已經被回收。2. 加載類的ClassLoader已經被回收。3. 類對象的Class對象沒有被引用(即沒有通過反射引用該類的地方)。
永久代的回收並不是必須的,可以通過參數來設置是否對類進行回收。
4、Minor GC ,Full GC 觸發條件
Minor GC觸發條件:當Eden區滿時,觸發Minor GC。
Full GC觸發條件:
(1)調用System.gc時,系統建議執行Full GC,但是不必然執行
(2)老年代空間不足
(3)方法區空間不足
(4)通過Minor GC后進入老年代的平均大小大於老年代的可用內存
(5)由Eden區、From Space區向To Space區復制時,對象大小大於To Space可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小。