關(guān)于Java內(nèi)存分配,很多問題都模模糊糊,不能全面貫通理解。今查閱資料,欲求深入挖掘,徹底理清java內(nèi)存分配脈絡(luò),只因水平有限,沒達到預(yù)期效果,僅以此文對所研究到之處作以記錄,為以后學(xué)習(xí)提供參考,避免重頭再來。
一、Java內(nèi)存分配
1、 Java有幾種存儲區(qū)域?
* 寄存器
-- 在CPU內(nèi)部,開發(fā)人員不能通過代碼來控制寄存器的分配,由編譯器來管理
* 棧
-- 在Windows下, 棧是向低地址擴展的數(shù)據(jù)結(jié)構(gòu),是一塊連續(xù)的內(nèi)存的區(qū)域,即棧頂?shù)牡刂泛蜅5淖畲笕萘渴窍到y(tǒng)預(yù)先規(guī)定好的。
-- 優(yōu)點:由系統(tǒng)自動分配,速度較快。
-- 缺點:不夠靈活,但程序員是無法控制的。
-- 存放基本數(shù)據(jù)類型、開發(fā)過程中就創(chuàng)建的對象(而不是運行過程中)
* 堆
-- 是向高地址擴展的數(shù)據(jù)結(jié)構(gòu),是不連續(xù)的內(nèi)存區(qū)域
-- 在堆中,沒有堆棧指針,為此也就無法直接從處理器那邊獲得支持
-- 堆的好處是有很大的靈活性。如Java編譯器不需要知道從堆里需要分配多少存儲區(qū)域,也不必知道存儲的數(shù)據(jù)在堆里會存活多長時間。
* 靜態(tài)存儲區(qū)域與常量存儲區(qū)域
-- 靜態(tài)存儲區(qū)用來存放static類型的變量
-- 常量存儲區(qū)用來存放常量類型(final)類型的值,一般在只讀存儲器中
* 非RAM存儲
-- 如流對象,是要發(fā)送到另外一臺機器上的
-- 持久化的對象,存放在磁盤上
2、 java內(nèi)存分配
-- 基礎(chǔ)數(shù)據(jù)類型直接在棧空間分配;
-- 方法的形式參數(shù),直接在??臻g分配,當方法調(diào)用完成后從棧空間回收;
-- 引用數(shù)據(jù)類型,需要用new來創(chuàng)建,既在棧空間分配一個地址空間,又在堆空間分配對象的類變量;
-- 方法的引用參數(shù),在棧空間分配一個地址空間,并指向堆空間的對象區(qū),當方法調(diào)用完后從??臻g回收;
-- 局部變量 new 出來時,在棧空間和堆空間中分配空間,當局部變量生命周期結(jié)束后,棧空間立刻被回收,堆空間區(qū)域等待GC回收;
-- 方法調(diào)用時傳入的 literal 參數(shù),先在??臻g分配,在方法調(diào)用完成后從??臻g釋放;
-- 字符串常量在 DATA 區(qū)域分配 ,this 在堆空間分配;
-- 數(shù)組既在??臻g分配數(shù)組名稱, 又在堆空間分配數(shù)組實際的大?。?div style="height:15px;">
3、Java內(nèi)存模型
* Java虛擬機將其管轄的內(nèi)存大致分三個邏輯部分:方法區(qū)(Method Area)、Java棧和Java堆。
-- 方法區(qū)是靜態(tài)分配的,編譯器將變量在綁定在某個存儲位置上,而且這些綁定不會在運行時改變。
常數(shù)池,源代碼中的命名常量、String常量和static 變量保存在方法區(qū)。
-- Java Stack是一個邏輯概念,特點是后進先出。一個棧的空間可能是連續(xù)的,也可能是不連續(xù)的。
最典型的Stack應(yīng)用是方法的調(diào)用,Java虛擬機每調(diào)用一次方法就創(chuàng)建一個方法幀(frame),退出該方法則對應(yīng)的 方法幀被彈出(pop)。棧中存儲的數(shù)據(jù)也是運行時確定的?
-- Java堆分配(heap allocation)意味著以隨意的順序,在運行時進行存儲空間分配和收回的內(nèi)存管理模型。
堆中存儲的數(shù)據(jù)常常是大小、數(shù)量和生命期在編譯時無法確定的。Java對象的內(nèi)存總是在heap中分配。
4、Java內(nèi)存分配實例解析
常量池(constant pool)指的是在編譯期被確定,并被保存在已編譯的.class文件中的一些數(shù)據(jù)。它包括了關(guān)于類、方法、接口等中的常量,也包括字符串常量。
常量池在運行期被JVM裝載,并且可以擴充。String的intern()方法就是擴充常量池的一個方法;當一個String實例str調(diào)用intern()方法時,Java查找常量池中是否有相同Unicode的字符串常量,如果有,則返回其引用,如果沒有,則在常量池中增加一個Unicode等于str的字符串并返回它的引用。
例:
String s1=new String("kvill");
String s2=s1.intern();
System.out.println( s1==s1.intern() );//false
System.out.println( s1+" "+s2 );// kvill kvill
System.out.println( s2==s1.intern() );//true
這個類中事先沒有聲名”kvill”常量,所以常量池中一開始是沒有”kvill”的,當調(diào)用s1.intern()后就在常量池中新添加了一個”kvill”常量,原來的不在常量池中的”kvill”仍然存在。s1==s1.intern()為false說明原來的“kvill”仍然存在;s2現(xiàn)在為常量池中“kvill”的地址,所以有s2==s1.intern()為true。
String 常量池問題
(1) 字符串常量的"+"號連接,在編譯期字符串常量的值就確定下來, 拿"a" + 1來說,編譯器優(yōu)化后在class中就已經(jīng)是a1。
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
(2) 對于含有字符串引用的"+"連接,無法被編譯器優(yōu)化。
String a = "ab";
String bb = "b";
String b = "a" + bb;
System.out.println((a == b)); //result = false
由于引用的值在程序編譯期是無法確定的,即"a" + bb,只有在運行期來動態(tài)分配并將連接后的新地址賦給b。
(3) 對于final修飾的變量,它在編譯時被解析為常量值的一個本地拷貝并存儲到自己的常量池中或嵌入到它的字節(jié)碼流中。所以此時的"a" + bb和"a" + "b"效果是一樣的。
String a = "ab";
final String bb = "b";
String b = "a" + bb;
System.out.println((a == b)); //result = true
(4) jvm對于字符串引用bb,它的值在編譯期無法確定,只有在程序運行期調(diào)用方法后,將方法的返回值和"a"來動態(tài)連接并分配地址為b。
String a = "ab";
final String bb = getbb();
String b = "a" + bb;
System.out.println((a == b)); //result = false
private static string getbb() {
return "b";
}
(5) String 變量采用連接運算符(+)效率低下。
String s = "a" + "b" + "c"; 就等價于String s = "abc";
String a = "a";
String b = "b";
String c = "c";
String s = a + b + c;
這個就不一樣了,最終結(jié)果等于:
Stringbuffer temp = new Stringbuffer();
temp.append(a).append(b).append(c);
String s = temp.toString();
(6) Integer、Double等包裝類和String有著同樣的特性:不變類。
String str = "abc"的內(nèi)部工作機制很有代表性,以Boolean為例,說明同樣的問題。
不變類的屬性一般定義為final,一旦構(gòu)造完畢就不能再改變了。
Boolean對象只有有限的兩種狀態(tài):true和false,將這兩個Boolean對象定義為命名常量:
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);
這兩個命名常量和字符串常量一樣,在常數(shù)池中分配空間。 Boolean.TRUE是一個引用,Boolean.FALSE是一個引用,而"abc"也是一個引用!由于Boolean.TRUE是類變量(static)將靜態(tài)地分配內(nèi)存,所以需要很多Boolean對象時,并不需要用new表達式創(chuàng)建各個實例,完全可以共享這兩個靜態(tài)變量。其JDK中源代碼是:
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
基本數(shù)據(jù)(Primitive)類型的自動裝箱(autoboxing)、拆箱(unboxing)是JSE 5.0提供的新功能。 Boolean b1 = 5>3; 等價于Boolean b1 = Boolean.valueOf(5>3); //優(yōu)于Boolean b1 = new Boolean (5>3);
static void foo(){
boolean isTrue = 5>3; //基本類型
Boolean b1 = Boolean.TRUE; //靜態(tài)變量創(chuàng)建的對象
Boolean b2 = Boolean.valueOf(isTrue);//靜態(tài)工廠
Boolean b3 = 5>3;//自動裝箱(autoboxing)
System.out.println("b1 == b2 ?" +(b1 == b2));
System.out.println("b1 == b3 ?" +(b1 == b3));
Boolean b4 = new Boolean(isTrue);////不宜使用
System.out.println("b1 == b4 ?" +(b1 == b4));//浪費內(nèi)存、有創(chuàng)建實例的時間開銷
} //這里b1、b2、b3指向同一個Boolean對象。
(7) 如果問你:String x ="abc";創(chuàng)建了幾個對象?
準確的答案是:0或者1個。如果存在"abc",則變量x持有"abc"這個引用,而不創(chuàng)建任何對象。
如果問你:String str1 = new String("abc"); 創(chuàng)建了幾個對象?
準確的答案是:1或者2個。(至少1個在heap中)
(8) 對于int a = 3; int b = 3;
編譯器先處理int a = 3;首先它會在棧中創(chuàng)建一個變量為a的引用,然后查找有沒有字面值為3的地址,沒找到,就開辟一個存放3這個字面值的地址,然后將a指向3的地址。接著處理int b = 3;在創(chuàng)建完b的引用變量后,由于在棧中已經(jīng)有3這個字面值,便將b直接指向3的地址。這樣,就出現(xiàn)了a與b同時均指向3的情況。
5、堆(Heap)和非堆(Non-heap)內(nèi)存
按照官方的說法:“Java 虛擬機具有一個堆,堆是運行時數(shù)據(jù)區(qū)域,所有類實例和數(shù)組的內(nèi)存均從此處分配。堆是在 Java 虛擬機啟動時創(chuàng)建的。”
可以看出JVM主要管理兩種類型的內(nèi)存:堆和非堆。
簡單來說堆就是Java代碼可及的內(nèi)存,是留給開發(fā)人員使用的;
非堆就是JVM留給自己用的,所以方法區(qū)、JVM內(nèi)部處理或優(yōu)化所需的內(nèi)存(如JIT編譯后的代碼緩存)、每個類結(jié)構(gòu)(如運行時常數(shù)池、字段和方法數(shù)據(jù))以及方法和構(gòu)造方法的代碼都在非堆內(nèi)存中。
堆內(nèi)存分配
JVM初始分配的內(nèi)存由-Xms指定,默認是物理內(nèi)存的1/64;
JVM最大分配的內(nèi)存由-Xmx指定,默認是物理內(nèi)存的1/4。
默認空余堆內(nèi)存小于40%時,JVM就會增大堆直到-Xmx的最大限制;空余堆內(nèi)存大于70%時,JVM會減少堆直到-Xms的最小限制。
因此服務(wù)器一般設(shè)置-Xms、-Xmx相等以避免在每次GC 后調(diào)整堆的大小。
非堆內(nèi)存分配
JVM使用-XX:PermSize設(shè)置非堆內(nèi)存初始值,默認是物理內(nèi)存的1/64;
由XX:MaxPermSize設(shè)置最大非堆內(nèi)存的大小,默認是物理內(nèi)存的1/4。
例子
-Xms256m
-Xmx1024m
-XX:PermSize=128M
-XX:MaxPermSize=256M
二、Java垃圾回收
1. JVM運行環(huán)境中垃圾對象的定義
一個對象創(chuàng)建后被放置在JVM的堆內(nèi)存中,當永遠不再引用這個對象時,它將被JVM在堆內(nèi)存中回收?;?nbsp; 當對象在JVM運行空間中無法通過根集合(root set)到達時,這個對象就被稱為垃圾對象。
2. 堆內(nèi)存
* 在JVM啟動時被創(chuàng)建;堆內(nèi)存中所存儲的對象可以被JVM自動回收,不能通過其他外部手段回收
* 堆內(nèi)存可分為兩個區(qū)域:新對象區(qū)和老對象區(qū)
-- 新對象區(qū)可分為三個小區(qū):Eden區(qū)、From區(qū)、To區(qū)
Eden區(qū)用來保存新創(chuàng)建的對象,當Eden區(qū)中的對象滿了之后,JVM將會做可達性測試,檢測有哪些對象由根集合出發(fā)是不可達的,不可達的對象就會被JVM回收,并將所有的活動對象從Eden區(qū)拷到To區(qū),此時一些對象將發(fā)生狀態(tài)交換,有的對象就從To區(qū)被轉(zhuǎn)移到From區(qū)。
3. JVM中對象的生命周期
* 創(chuàng)建階段(步驟)
-- 為對象分配存儲空間
-- 開始構(gòu)造對象
-- 遞歸調(diào)用其超類的構(gòu)造方法
-- 進行對象實例初始化與變量初始化
-- 執(zhí)行構(gòu)造方法體
* 應(yīng)用階段
-- 特征:系統(tǒng)至少維護著對象的一個強引用;所有對該對象引用強引用(除非顯示聲明為其它引用)
-- 強引用
指JVM內(nèi)存管理器從根引用集合出發(fā),遍尋堆中所有到達對象的路徑。當?shù)竭_某對象的任意路徑都不含有引用對象時,對這個對象的引用就被稱為強引用。
當內(nèi)存不足時,JVM寧愿拋出OutOfMemeryError錯誤使程序停止,也不會靠收回具有強引用的對象來釋放內(nèi)存空間
-- 軟引用
它能實現(xiàn)cache功能,防止最大限度的使用內(nèi)存時引起的OutOfMemory異常,在內(nèi)存不夠用的時候jvm會自動回收Soft Reference。
軟引用可以和一個引用隊列(ReferenceQueue)聯(lián)合使用,如果軟引用所引用的對象被垃圾回收,java虛擬機就會把這個軟引用加入到與之關(guān)聯(lián)的引用隊列中。
Java中提供軟引用的包:java.lang.ref.SoftReference(后續(xù)詳解)
軟引用
實現(xiàn)cache功能,防止最大限度的使用內(nèi)存時引起的OutOfMemory異常,在內(nèi)存不夠用的時候jvm會自動回收Soft Reference.軟引用可以和一個引用隊列(ReferenceQueue)聯(lián)合使用,如果軟引用所引用的對象被垃圾回收,Java虛擬機就會把這個軟引用加入到與之關(guān)聯(lián)的引用隊列中。
Java代碼
import java.lang.ref.SoftReference //實現(xiàn)cache功能,最大限度利用內(nèi)存 Test test = new Test(); SoftReference sr = new SoftRefence(test); test = null; if(sr.get() != null){ test = sr.get(); }else{ test = new Test(); sr = new SoftReference(test); test = null; }
Java代碼
//創(chuàng)建一個強引用 String str = new String("hello"); //創(chuàng)建引用隊列, <String>為范型標記,表明隊列中存放String對象的引用 ReferenceQueue<String> rq = new ReferenceQueue<String>(); //創(chuàng)建一個弱引用,它引用"hello"對象,并且與rq引用隊列關(guān)聯(lián) //<String>為范型標記,表明WeakReference會弱引用String對象 SoftReference<String> wf = new SoftReference<String>(str, rq); str=null; //取消"hello"對象的強引用 String str1=wf.get(); //假如"hello"對象沒有被回收,str1引用"hello"對象 //假如"hello"對象沒有被回收,rq.poll()返回null Reference<? extends String> ref=rq.poll();
-- 弱引用
只具有弱引用的對象有更短的生命周期,無論內(nèi)存是否緊張,被垃圾回收器發(fā)現(xiàn)立即回收。弱引用可以和一個引用隊列(ReferenceQueue)聯(lián)合使用。
可分為長弱引用和短弱引用,長弱引用在對象的Finalize方法被GC調(diào)用后依然追蹤對象
Java中提供弱引用的包:java.lang.ref.WeakReference
-- 虛引用
虛引用并不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。虛引用主要用來跟蹤對象被垃圾回收的活動。
Phantom對象指一些執(zhí)行完了finalize函數(shù),并且為不可達對象,但是還沒被GC回收的對象。這種對象可以輔助finalize進行一些后期的回收工作。
* 不可視階段
-- 如果一個對象已使用完,并且在其可視區(qū)域不再使用,應(yīng)該主動將其設(shè)置為null,即obj=null;這樣可以幫助JVM及時地發(fā)現(xiàn)這個垃圾對象,并且可以及時地揮手該對象所占用的系統(tǒng)資源。
Java代碼

package reference; /* WeakHashMap, 在這種Map中存放了鍵對象的弱引用,當一個鍵對象被垃圾回收,那么相應(yīng)的值對象的引用會從Map中刪除。WeakHashMap能夠節(jié)約存儲空間,可用 來緩存那些非必須存在的數(shù)據(jù)。 */ import java.util.*; import java.lang.ref.*; class Key { String id; public Key(String id) { this.id = id; } public String toString() { return id; } public int hashCode() { return id.hashCode(); } public boolean equals(Object r) { return (r instanceof Key) && id.equals(((Key) r).id); } public void finalize() { System.out.println("Finalizing Key " + id); } } class Value { String id; public Value(String id) { this.id = id; } public String toString() { return id; } public void finalize() { System.out.println("Finalizing Value " + id); } } public class MapCache { public static void main(String[] args) throws Exception { int size = 1000; // 或者從命令行獲得size的大小 if (args.length > 0) size = Integer.parseInt(args[0]); Key[] keys = new Key[size]; // 存放鍵對象的強引用 WeakHashMap<Key, Value> whm = new WeakHashMap<Key, Value>(); for (int i = 0; i < size; i++) { Key k = new Key(Integer.toString(i)); Value v = new Value(Integer.toString(i)); if (i % 3 == 0) keys[i] = k; // 使Key對象持有強引用 whm.put(k, v); // 使Key對象持有弱引用 } // 催促垃圾回收器工作 System.gc(); // 把CPU讓給垃圾回收器線程 Thread.sleep(8000); } }
4. Java中的析構(gòu)方法finalize
finalize()方法常稱之為終止器
protected void finalize(){
// finalization code here
}
對象即將被銷毀時,有時需要做一些善后工作。可以把這些操作寫在finalize()方法里。
Java終止器卻是在對象被銷毀時調(diào)用。一旦垃圾收集器準備好釋放無用對象占用的存儲空間,它首先調(diào)用那些對象的finalize()方法,然后才真正回收對象的內(nèi)存。而被丟棄的對象何時被銷毀,應(yīng)用是無法獲知的。大多數(shù)場合,被丟棄對象在應(yīng)用終止后仍未銷毀。到程序結(jié)束的時候,并非所有收尾模塊都會得到調(diào)用。
5. 應(yīng)用能干預(yù)垃圾回收嗎?
在應(yīng)用代碼里控制JVM的垃圾回收運作是不可能的事。
對垃圾回收有兩個途徑。第一個就是將指向某對象的所有引用變量全部移走。這就相當于向JVM發(fā)了一個消息:這個對象不要了。第二個是調(diào)用庫方法System.gc()。第一個是一個告知,而調(diào)用System.gc()也僅僅是一個請求。JVM接受這個消息后,并不是立即做垃圾回收,而只是對幾個垃圾回收算法做了加權(quán),使垃圾回收操作容易發(fā)生,或提早發(fā)生,或回收較多而已。
希望JVM及時回收垃圾,是一種需求。其實,還有相反的一種需要:在某段時間內(nèi)最好不要回收垃圾。要求運行速度最快的實時系統(tǒng),特別是嵌入式系統(tǒng),往往希望如此。
Java的垃圾回收機制是為所有Java應(yīng)用進程服務(wù)的,而不是為某個特定的進程服務(wù)的。因此,任何一個進程都不能命令垃圾回收機制做什么、怎么做或做多少。
6. 垃圾回收算法
* 引用計數(shù)
該算法在java虛擬機沒被使用過,主要是循環(huán)引用問題,因為計數(shù)并不記錄誰指向他,無法發(fā)現(xiàn)這些交互自引用對象。
-- 怎么計數(shù)?
當引用連接到對象時,對象計數(shù)加1
當引用離開作用域或被置為null時減1
-- 怎么回收?
遍歷對象列表,計數(shù)為0就釋放
-- 有什么問題?
循環(huán)引用問題。
* 標記算法
標記算法的思想是從堆棧和靜態(tài)存儲區(qū)的對象開始,遍歷所有引用,標記活得對象。
對于標記后有兩種處理方式:
(1) 停止-復(fù)制
-- 所謂停止,就是停止在運行的程序,進行垃圾回收
-- 所謂復(fù)制,就是將活得對象復(fù)制到另外一個堆上,以使內(nèi)存更緊湊
-- 優(yōu)點在于,當大塊內(nèi)存釋放時,有利于整個內(nèi)存的重分配
-- 有什么問題?
一、停止,干擾程序的正常運行,二,復(fù)制,明顯耗費大量時間,三,如果程序比較穩(wěn)定,垃圾比較少,那么每次重新復(fù)制量是非常大的,非常不合算
-- 什么時候啟動停止-復(fù)制?
內(nèi)存數(shù)量較低時,具體多低我也不知道
(2) 清除 也稱標記-清除算法
-- 也就是將標記為非活得對象釋放,也必須暫停程序運行
-- 優(yōu)點就是在程序比較穩(wěn)定,垃圾比較少的時候,速度比較快
-- 有什么問題?
很顯然停止程序運行是一個問題,只清除也會造成很對內(nèi)存碎片。
-- 為什么這2個算法都要暫停程序運行?
這是因為,如果不暫停,剛才的標記會被運行的程序弄亂