引言
Java的內存動態分配和垃圾收集的問題,都交給了JVM來處理。注意,將JVM運行數據區(虛擬機棧【棧幀】,程序計數器,堆內存)粗略的分爲棧和堆(所有線程共享),回收的是堆中的對象實例。不是棧中的引用類型。
那麼JVM是如何處理的?
從三個問題來分析:
1. 哪些內存需要回收?
2. 什麼時候進行回收?
3. 如何回收?
注:現代收集器基本採用分代收集算法,堆分爲:新生代和老年代。
1. 哪些內存需要回收?什麼時候回收?
1.1 瞭解下對象的創建:
- 通過new 關鍵字。
- JVM遇到new指令,檢查是否能在常量池中定位到一個類的符號引用。
- 檢查是否已被加載,解析,初始化過。
- 沒有,則執行相應的類加載過程。
- 類加載檢查通過後,爲新生對象分配內存。(類加載後確定對象所需內存大小)
- 從Java堆中劃分出一塊確定的內存。
1.2 若爲“死亡”的對象,則需要回收,如何判斷對象是否存活?
1.2.1 引用計數算法
- 給對象添加一個引用計數器
- 每當有一個地方引用它時,計數器指加1
- 當引用失效時,計數器指減1
- 任何時刻計數器爲0的對象就是需要回收的
Java虛擬機沒有采用這種方法來管理內存,主要原因是它難以解決對象之間相互循環引用的問題。(python中採用)
1.2.2 可達性分析算法
- 用一個“GC Roots”的對象(指一系列中的其中一個並非某一種)作爲起始點
- 從該節點向下搜索,搜索走過的路徑稱爲引用鏈
- 若一個對象沒有與任何引用鏈相連,即不可達
- 證明該對象是不可用,死亡
Java,C#採用此方法。
1.2.3 哪些可作爲GC Roots的對象?
- 虛擬機棧(棧幀中的本地變量表)中引用的對象
- 方法區中類靜態屬性引用的對象
- 方法區中常量引用的對象
- 本地方法棧中JNI引用的對象
1.2.4 無論哪種算法都是與“引用”有關,下面分析引用的4種類型,強度依次減弱。
- 強引用:Java中普遍存在的(如:Object obj = new Object()),只要強引用還在,垃圾收集器永遠不會回收掉被引用的對象實例。
- 軟引用:用於有用但非必需的對象。在系統要發生內存溢出異常之前,將會把這些對象列進回收範圍之中進行第二次回收。如果這次回收還沒有足夠的內存,纔會拋出內存溢出異常。[SoftReference]
- 弱引用:用於非必需對象。被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。[WeakReference]
- 虛引用:不影響生存時間,唯一目的就是能在這個對象被收集器回收時收到一個系統通知。[PhantomReference]
1.2.5 詳解,真正的”死亡”對象(兩次標記後進行回收)
事實上,不可達的對象,也並非“非死不可”,這時它們處於“緩刑”階段,真正死亡至少要經歷兩次標記過程:
1.2.5.1 第一次標記
- 對對象進行可達性分析後發現沒有與GC Roots相連接的引用鏈
- 篩選是否有必要執行finalize()方法[沒有覆蓋finalize()方法或finalize()方法已經被JVM調用過,視爲“無需執行”]
1.2.5.2 第二次標記【需要執行finalize(),通過篩選】
- 將該對象放置F-Queue的隊列中
- JVM自動建立一個低優先級的Finalizer線程去執行它(會觸發執行,但不一定等待它運行結束)
- GC對F-Queue中的對象進行標記(對象可通過finalize()拯救自己,重新關聯引用鏈等)
不建議使用finalize()
1.3 上面講的是需要回收的堆內存,關於回收方法區(或HotSpot虛擬機中的永久代),效率低
1.3.1 廢棄常量
- 與堆類似,沒有任何對象和其它地方引用的常量
- 隨內存回收,被系統清理出常量池
1.3.2 無用的類
- 該類所有的實例都已經被回收(堆中無該類的實例)
- 加載該類的ClassLoader已經被回收
- 該類對應的java.lang.Class對象沒有在任何地方被引用,無法通過反射訪問該類的方法。
2. 如何回收?【垃圾收集算法】
2.1 標記-清除[Mark-Sweep]算法
- 標記出所有需要回收的對象
- 統一回收(清除)所有被標記的對象
缺點:效率不高;標記清除後會產生大量不連續的內存碎片。
2.2 複製[Copying]算法
- 將可用內存按容量劃分爲大小相等的兩塊
- 每次只使用其中的一塊
- 當使用的這塊內存用完了,則將還存活的對象複製到另一塊上面
- 把使用過的那塊內存一次性清理掉
優點:實現簡單,運行高效,可按順序分配內存。
缺點:內存直接縮小爲原來一半,代價太大;對象存活率較高時,效率變低。
2.3 標記-整理[Mark-Compact]算法
- 標記出所有需要回收的對象
- 讓所有存活的對象都向一端移動(整理)
- 清理掉存活對象端以外的內存
適合堆中的老年代的垃圾收集
2.4 分代收集[Generational Collection]算法(商業虛擬機主要採用方法)
- 根據對象存活週期的不同將內存劃分爲幾塊
- 一般分爲新生代和老年代
- 不同年代採用最適當的收集算法
- 新生代,一般複製算法
- 老年代,一般“標記-清理”或“標記-整理”
具體如何回收,需依據具體的垃圾收集器實現
關於內存泄露
- OutOfMemoryError異常,java堆溢出:-Xms ,-Xmx
對象不斷被創建,並且保證GC Roots到對象之間有可達路徑來避免垃圾回收機制清除這些對象,那麼在對象數量到達最大堆的容量限制後就會產生內存溢出異常。 - VM Stack溢出。-Xss
- 常量池溢出。-XX:PermSize,-XX:MaxPermSize