jVM內存模型
java和C/C++最大的一個區別就是自動的內存回收機制。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可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小