Java的GC機制

jVM內存模型

java和C/C++最大的一個區別就是自動的內存回收機制。Java的內存模型經典圖例如下:
Java內存機制
這裏重點注意三個地方:堆內存、方法區、與棧內存。其中堆內存與方法區又合稱爲堆內存。在程序未實例化之前,類的模型以及全局變量都是保存在方法區中。類的加載方法分爲兩種:

顯式加載:Class.forName()利用Class的靜態方法對類進行實例化。
隱式加載:通過new關鍵字加載。

當類實例化之後,實例就按照方法區類的模板加載到了堆內存。此時棧內存存放基本的數據類型(int,float,short,long,char等)以及指向堆對象的引用變量。總之,堆是用來存放對象的,棧是用來執行程序的,相較於堆,棧的執行速度較快。
這裏需要重點注意的是Java中的常量池。由於版本原因,運行時常量池在JDK1.6及之前版本的JVM中是方法區的一部分,而在HotSpot虛擬機中方法區放在了”永久代(Permanent Generation)”。所以運行時常量池也是在永久代的。 但是JDK1.7及之後版本的JVM已經將運行時常量池從方法區中移了出來,在Java 堆(Heap)中開闢了一塊區域存放運行時常量池。
這裏拿int變量與string變量來作爲例子。int與string變量最大的區別就是int存在它自身的基本數據類型,在int,integer,new integer()之間存在相對複雜的變化。
int:
基本數據類型與對象間比較,在JDK1.5以後存在自動拆箱機制,不論後面其它哪個相比較,都會被向下轉型到int,然後比較其數值。
Integer與Integer間的比較,從jdk1.5開始,有“自動裝箱”這麼一個機制,在byte-128到127範圍內,如果存在了一個值,再創建相同值的時候就不會重新創建,而是引用原來那個,但是超過byte範圍還是會新建的對象。
關於new integer() 這裏涉及到和int比較不再贅述,都自動拆箱比較數值。但和integer進行比較的時候,當integer在integer cache中時候,顯然不等,當超出integer cache範圍時候,此時integer相當於new integer()兩個new比較都在堆中創建了對象,對應不同的地址,顯然不相等。

public class test {
    public static void main(String[] args) {
        int i0=1000;
        Integer i1=1000;
        Integer i2=1000;
        Integer i3=2000;
        Integer i11=new Integer(1000);
        Integer i22=new Integer(1000);
        Integer i33=new Integer(2000);
        System.out.println(i0==i11);//true
        System.out.println(i0==i1);//true
        System.out.println(i1==i2);//false不會有自動拆箱
        System.out.println(i1==i11);//false
        System.out.println(i11==i22);//false
        System.out.println(i3==(i1+i2));//true後面的這些加加減減全部都拆箱爲基本數據類型。
        System.out.println(i3==(i11+i22));//true
        System.out.println(i33==(i1+i2));//true
        System.out.println(i33==(i11+i22));//true
    }
}

string:

問:String s = new String(“xyz”); 產生幾個對象?

答:一個或兩個。如果常量池中原來沒有 ”xyz”, 就是兩個。如果原來的常量池中存在“xyz”時,就是一個。

字符串加法:JVM對於字符串常量的”+”號連接,將程序編譯期,JVM就將常量字符串的”+”連接優化爲連接後的值。由final修飾和無final修飾:有final修飾的字符串能在編譯前確定,無final修飾的字符串是無法在編譯前確定的。

//字符串相加JVM優化
String a = "a1";  
String b = "a" + 1;  
System.out.println((a == b)); //result = true  
String a = "atrue";  
String b = "a" + "true";  
System.out.println((a == b)); //result = true  
String a = "a3.4";  
String b = "a" + 3.4;  
System.out.println((a == b)); //result = true  
//字符串相加無final關鍵字
String a = "ab";  
String bb = "b";  
String b = "a" + bb;  
System.out.println((a == b)); //result = false   
//字符串相加有final關鍵字
String a = "ab";  
final String bb = "b";  
String b = "a" + bb;  
System.out.println((a == b)); //result = true   

Java中的內存溢出

首先我們來區別兩個概念:

內存溢出:程序在申請內存空間時,沒有足夠的內存空間供其使用。
內存泄漏:程序在申請內存後,無法釋放已經申請的內存空間。

沿襲上述所述,Java中的堆的溢出,主要原因就是對象實例化過多。棧的溢出主要是因爲線程需要棧深度大於虛擬機本身。這裏尤其注意在多線程的環境之中,可以通過減小最大堆和減小棧容量來創建更多的線程。
關於內存泄漏,常見原因是對象在使用後沒有斷開引用。例如(1)各類連接如SQL、IO。(2)變量不合理的作用域。(3)面試常考:單例設計模式也會導致內存泄漏。

//核心:單例模式中存在對於指向其它實例的指針
class Bigclass{
    //class body
}
class Singleton{
    private Bigclass bc;
    private static Singleton instance=new Singleton(new Bigclass());
    private Singleton(Bigclass bc){this.bc=bc}
    public Singleton getinstance{
        return instance;
    }
}

關於GC機制

判斷方法

引用計數法:略。無法解決互相引用的問題。
可達性分析法:通過一系列的 gc root(棧指針,方法區中static相關已經常量引用) 進行樹搜索。

回收算法

標記-清除法:效率問題以及內存碎片。
複製算法:僅僅使用1/2內存。內存開銷較大。但實際上98%的對象是朝生夕死的,所以不需要按照1:1的比例劃分,而是以8:1的比例(hotspot默認)劃分爲Eden和survivor。
分代收集法:jvm中的GC算法。新生代採用複製算法,因爲有大量對象死去。老年代採用標記整理或者標記清除。

回收內存模型:
回收內存模型

由分代收集再談幾類引用:

強引用:怎麼都不會被GC機制回收。寧願報錯都不會回收該區域。
軟引用:只有在內存不足時,纔會被回收。JDK1.2以後提供softreference類。
弱引用:weak爲關鍵字(weakhashmap),一旦運行system.gc()則立馬被回收
虛引用:就和沒有引用一樣。

system.gc()與finalize

system.gc()調用的是runtime中的gc方法。對象回收前會調用finalize方法,但也只會調用一次,此時可以通過finalize 完成引用對自我進行最後一次拯救(參見《深入理解JVM》)。不同於C++中的析構函數,C++調用析構函數對象一定會被銷燬。
此外調用GC時會STOP THE WORLD現象,因爲不能存在在垃圾回收時,引用關係還在不斷變化的情況。這裏又引入了安全點的概念。所謂安全點,是以是否具有讓程序具有長時間執行的特徵來選定的。如一些循環操作。在隨後運行GC時又存在搶先式中斷主動式中斷。注意其中概念與區別。目前的hotspot主要採取主動式中斷輪詢各個線程是否到達安全點。

fullgc與minor 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可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小

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