Java內存管理與垃圾回收

1.Java內存管理


1.1內存分配與對象初始化

當我們使用new關鍵字創建一個Java對象時,JVM首先會檢查這個new指令的參數是否能在常量池中定位到一個類的符號引用,然後檢查與這個符號引用相對應的類是否已經成功經歷過加載,解析和初始化等步驟(涉及類加載機制),當類完成裝載步驟之後,就可以完全確定創建對象實例所需要的內存空間大小,接下來JVM將會對其進行內存分配,以存儲所生成的對象實例。

如果內存空間中已用和未用的內存各自一邊,彼此之間維繫一個記錄下一次分配起始點的標記指針,當爲新對象分配內存時,只需要通過修改指針的偏移量將新對象分配在第一個空閒內存位置上就行,這種分配方式就叫指針碰撞,反之則只能用空閒列表(Free List)執行內存分配。

1.1.1內存分配方式之-----快速分配策略

因爲堆區是線程共享的,因此在併發環境下,從堆區劃分內存空間是非線程安全的,由於要保證數據操作的原子性,基於線程安全的考慮,如果一個類在分配內存之前就已經成功完成類裝載步驟,JVM就會優先選擇在TLAB(Thread Local Allocation,本地線程分配緩衝區)中爲對象實例分配內存空間,TLAB在Java堆區中是一塊線程私有區域的區域,它包含在Eden空間內。除了可以避免一系列非線程安全問題,還能夠提升內存分配的吞吐量,這種內存分配方式稱爲快速分配策略


1.1.2完成內存分配後,初始化對象實例

JVM首先會對分配後的內存空間進行零值初始化,確保對象的實例字段在Java代碼中可以不用賦初始值就能夠直接使用。接着,JVM就會初始化對象頭和實例數據,最後將對象的引用入棧後再更新程序計數器中的字節碼指令地址。經過內存分配和初始化,一個Java對象實例纔算創建成功。


1.2逃逸分析與棧上分配

Java堆區不是對象內存分配的唯一選擇,對象內存分配還可以在堆外進行。

堆外存儲好處:降低GC的回收率和提升GC的回收效率。

常見的堆外存儲技術:利用逃逸分析技術篩選出未發生逃逸的對象,然後避開堆區直接選擇在棧幀中分析內存空間。


1.2.1概念

JVM在執行性能優化之前的一種分析技術,具體目標就是分析出對象的作用域。
簡單來說,當一個對象被定義在方法體內部之後,它的受訪權限就僅限於方法體內,一旦其引用被外部成員引用後,這個對象就發生了逃逸,反之如果定義在方法體內的對象並沒有被任何外部成員引用,JVM就會將其分配在棧幀中。

1.2.2優點

如果沒有發生逃逸,那麼對象就會在棧上分配內存,因此GC就無需執行內存回收,因爲棧幀會伴隨方法的調用而創建,伴隨方法的執行結束而銷燬。所以,棧上分配的對象所佔用的內存空間會隨棧幀的出棧而釋放。

注意:JDK6後,已經默認開啓逃逸分析了。

1.2.3舉例

/**
 * 逃逸分析與棧上分配
 */
public class StackAllocation {
    
    public StackAllocation obj;
    
    public StackAllocation getObj() {
        /*方法返回StackAllocation對象實例,發生逃逸,分配在堆 */
        return null == obj ? new StackAllocation() : obj;
    }
    
    public void setObj() {
        /*爲成員變量賦值,發生逃逸,分配在堆*/
        //方法體內的對象被外部成員obj引用,所以發生逃逸
        obj = new StackAllocation();
    }
    
    public void useStackAllocation1() {
        /*引用成員變量的值,發生逃逸,分配在堆*/
        //方法體內的obj對象的引用實際上爲外部成員obj對象的引用,因此發生逃逸
        StackAllocation obj = getObj();
    }
    
    public void useStackAllocation2() {
        /*對象的作用域僅限於方法體內,未發生逃逸,分配在棧上*/
        //方法體內的對象並沒有被任何外部成員引用
        StackAllocation obj = new StackAllocation();
    }
}


2.垃圾標記

2.1垃圾標記

在GC執行垃圾回收之前,需要區分區內存中哪些是存活對象,哪些是死亡對象,只有被標記爲已經死亡的對象,GC纔會在執行垃圾回收時釋放其佔用的內存空間。

如何標記?

2.1.1標記算法——引用計數算法

爲程序中的每一個對象創建一個私有的引用計數器,當目標對象被其他存活對象引用時,引用計數器中的值加1,不再引用就減1,當引用計數器中的值爲0的時候,就意味該對象已經不再被任何存活對象引用,可以標記爲垃圾對象。

缺陷: 一些明顯死亡的對象之間存在相互引用時,引用計數器中的值永遠不會爲0,這樣會導致GC在執行內存回收時永遠無法釋放掉無用內存所佔用的內存空間,極有可能引發內存泄漏。

2.1.2標記算法——根搜索算法(基本在用這個)

以根對象集合作爲起始點,按照從上至下的方式搜索被根對象集合所連接的目標對象是否可達(使用根搜索算法後,內存中的存活對象都會被根對象集合直接或者間接連接着),如果目標對象不可達,就意味着該對象已經死亡。

根對象集合存儲的內容:
1.Java棧中的對象引用
2.本地方法棧中的對象引用
3.運行時常量池中的對象引用
4.方法區中類靜態屬性的對象引用
5.與一個類對應的唯一數據類型的Class對象



3.垃圾回收——分代收集算法

成功區分出內存中存活對象和死亡對象後,接下來就是執行垃圾回收釋放無用對象所佔用的內存空間了。

3.1回收算法之——標記-清除

標記後,清除。不執行其他操作。
缺點: 執行效率低下,並且由於被執行內存回收的無用對象所佔用的內存空間有可能是一些不連續的內存塊,不可避免地會產生一些內存碎片,從而導致後續沒有足夠的可用內存空間分配給較大的對象。

3.2回收算法之——複製

Java堆區分爲新生代(又分爲Eden空間,From Survivor空間,To Survivor空間)和老年代。
空間比例: Eden:From Survivor+To Survivor = 8:1

當執行一次Minor GC(新生代的垃圾回收)時,Eden空間中存活對象會被複制到To Survivor空間,並且之前已經經歷過一次Minor GC且在From Survivor空間中存活下來的對象如果還年輕的話同樣也會被複制到To Survivor空間內。

複製方向:(Eden->To Survivor,From Survivor->To Survivor)

不會複製到To Survivor中的特殊情況:

1.存活對象的分代年齡超過指定的閾值時,將會直接晉升到老年代中
2. To Survivor空間的容量達到閾值時,存活對象直接晉升到老年代中

當所有存活對象被複制到To Survivor空間或晉升到老年代後,剩下的均爲垃圾對象,這就意味GC可以對這些死亡對象執行一次Minor GC,釋放其佔用的空間。

執行完Minor GC後,Eden空間和From Survivor空間將會被清空,存活下來的對象全部存儲在To Survivor空間內,接下來From Survivor和To Survivor空間互換位置。其實複製算法無非就是使用To Survivor空間作爲一個臨時空間交換的角色,務必保證兩塊Survivor空間中一塊必須時空的,着就是複製算法。

應用場景:多應用於新生代中。

缺點:

不適用於老年代中的內存回收:因爲老年代中的對象生命週期比較長,極端情況下能與JVM生命週期保持一致,如果老年代也採用複製算法,那麼內存回收需要額外的空間和時間,還會導致較多的複製操作從而影響GC執行效率。


3.3回收算法之——標記-壓縮

成功標記垃圾對象後,該算法會將所有的存活對象都移動到一個規整且連續的內存空間中,然後執行Full GC(老年代的垃圾回收,或者稱之爲Major GC)回收無用對象所佔用的內存空間。

成功執行壓縮後,已用和未用的內存都各自一邊,彼此之間維繫着一個記錄下一次分配起始點的標記指針,當爲新對象分配內存時,則可以使用 指針碰撞 技術修改指針的偏移量將新對象分配在第一個空閒內存位置上,爲新對象分配內存帶來便捷。

應用場景:多應用於老年代中。

3.4圖例

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章