JVM虛擬機器筆記之垃圾回收及記憶體分配策略
程式計數器.虛擬機器棧.本地方法棧隨執行緒而生隨執行緒而滅,棧幀分配多少記憶體在類結構確定後就確定了。垃圾回收針對的是Java堆和方法區。
一:物件已死嗎
- 在垃圾收集器進行回收前,第一件事就是確定這些物件哪些還存活,哪些已經死去。
1.引用計數演算法
- 在物件中新增一個引用計數器,每當有一個地方引用它時,計數器就加1;當引用失效時,計數器減1;其中計數器為0的物件是不可能再被使用的已死物件。
- 當兩個物件相互引用時,這兩個物件就不會被回收 引用計數演算法,不被主流虛擬機器採用,主要原因是它很難解決物件之間相互迴圈引用的問題。
2 可達性分析演算法
通過一系列的稱為GC Roots的物件作為起始點,從這些節點開始向下搜尋,搜尋所經過
的路徑稱為引用鏈(Reference Chain),當一個物件到GC Roots沒有任何引用鏈相連(在圖論中稱為物件不可達)時,這個物件就是不可用的。
如圖object5,6,7雖然有關聯,但是到gc roots是不可達的所以是可回收物件。
在java中,可作為GC Roots的物件包括:
- 虛擬機器棧(棧幀中的本地變量表)中引用的物件
- 方法區中類靜態屬性引用的物件
- 方法區中常量引用的物件
- 本地方法棧中JNI引用的物件
3 引用的分類
- 強引用:是指在程式程式碼中直接存在的引用,譬如引用new操作符建立的物件。只要強引用還存在,垃圾收集器就永遠不會回收掉被引用的物件。
- 軟引用:還有用但是並非必需的引用,早系統將要發生記憶體溢位異常之前會把這些物件列進回收範圍中進行二次回收,若還是沒有足夠的記憶體,才會丟擲記憶體溢位異常。
- 弱引用:非必需的物件,只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論記憶體是否夠用都將回收這些物件。
- 虛引用:一個物件是否有虛引用的存在完全不會對他的生存時間構成影響,也無法通過虛引用來取得一個物件例項。
4 宣告一個物件死亡的過程
要真正宣告一個物件死亡,至少要經歷兩次標記過程:
- 若物件在進行可達性分析後發現沒有與GC Roots相連線的引用鏈,會被 第一次標記
並且進行一次篩選。篩選的條件是此物件是否有必要執行finalize()方法(如當物件沒有重寫finalize()方法或者finalize()方法已經被虛擬機器呼叫過則認為沒有必要執行)。 - 如果有必要執行則將該物件放置在F-Queue佇列中,並在稍後由一個由虛擬機器自己建立的、低優先順序的Finalizer執行緒去執行它;稍後GC將對F-Queue中的物件進行第二次標記,如果物件還是沒有被引用,則會被回收。
但是作者不建議通過finalize()方法“拯救”物件,因為它執行代價高、不確定性大、無法保證各個物件的呼叫順序。
5 回收方法區
永久代(方法區)的垃圾收集主要回收兩部分內容:廢棄常量和無用的類
- 廢棄常量:假如一個字串“abc”已經進入了常量池中,但是當前系統沒有任何一個String物件是叫做“abc”的,換句話說,就是沒有任何String物件引用常量池中的“abc”常量,也沒有其他 地方引用了這個字面量,如果這時發生記憶體回收,而且必要的話,這個“abc”常量就會被系統清理出常量池。
- 無用的類:同時滿足下面3個條件的類(例項、類載入器被回收,java.lang.Class物件沒有被引用)。①該類所有的例項都已經被回收,也就是Java堆中不存在該類的任何例項。
②載入該類的ClassLoader已經被回收。
③該類對應的java.lang.Class物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
二:垃圾收集演算法
1.標記清除演算法
分為兩個階段:標記和清除
- 標記:首先標記所有需要回收的物件(標記過程在上文宣告一個物件死亡過程中提及)
- 清除:在標記完成後統一回收所有被標記的物件
不足地方:①效率問題,標記和清除過程效率都不高;②空間問題,垃圾回收後較多不不連續的記憶體碎片,導致分配較大物件時找不到足夠的連續記憶體而不得不提前觸發又一次垃圾回收動作.
2.複製演算法(新生代)
- 將可用記憶體按容量分為兩個塊,每次只用其中之一。當這一塊記憶體用完之後,將還存活的物件複製到另一邊去,然後清除所有已經使用過的部分。
- 優點 :每次都是對整個半區進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況,只要移動堆頂指標,按順序分配記憶體即可,實現簡單,執行高效。
- 缺點 :代價是將記憶體縮小為了原來的一半,未免太高了一點。
解決方法:新生代中的物件98%是“朝生夕死”的,所以並不需要按照1:1的比例來劃分記憶體空間,而是將記憶體分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。
在HotSpot裡,考慮到大部分物件存活時間很短將記憶體分為Eden和兩塊Survivor,預設比例為8:1:1。代價是存在部分記憶體空間浪費,適合在新生代使用。
備註:不保證每次不多於10%的物件存活,survivor空間不夠時,依賴其他記憶體分配擔保(老年代)
3 標記-整理演算法(老年代演算法)(Mark-Compact)
物件存活率太高情況下,進行較多複製操作效率將會變低,應對諸如所以物件都存活情況下,所有老年代不採用複製演算法.
- 標記過程仍然與“標記-清除”演算法一樣,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。
4.分代收集演算法
- 當前商用虛擬機器都採用了這種演算法,根據物件的存活週期將記憶體劃分為幾塊,一般是把Java堆分為新生代和老生代,根據各個年代採用適當的收集演算法。
- 新生代一般採用複製演算法(Copying)。
- 老生代一搬採用 標記-清理(Mark-Sweep) 或者標記-整理(Mark-Compact) 進行回收。