第2章 Java內(nèi)存區(qū)域與內(nèi)存溢出異常
運(yùn)行時(shí)數(shù)據(jù)區(qū)域
在虛擬機(jī)有棧、堆和方法區(qū)。

線程共享的:堆、方法區(qū)
不共享的:棧、程序計(jì)數(shù)器(代碼執(zhí)行的行號(hào))
程序計(jì)數(shù)器(Program?Counter?Register):
一小塊內(nèi)存空間,單前線程所執(zhí)行的字節(jié)碼行號(hào)指示器。字節(jié)碼解釋器工作時(shí),通過改變這個(gè)計(jì)數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個(gè)計(jì)數(shù)器來完成。
? ? ?
棧
- 通常我們定義一個(gè)對(duì)象的引用,還有就是函數(shù)調(diào)用的現(xiàn)場(chǎng)保存都使用內(nèi)存中的??臻g。
- 棧是一種線形集合,其添加和刪除元素的操作應(yīng)在同一段完成。棧按照后進(jìn)先出的方式進(jìn)行處理。
- 存在棧中的數(shù)據(jù)可以共享。假設(shè)我們同時(shí)定義: ?int?a?=?3; int?b?=?3; ?編譯器先處理int?a?=?3;首先它會(huì)在棧中創(chuàng)建一個(gè)變量為a的引用,然后查找有沒有字面值為3的地址,如果沒找到,就開辟一個(gè)存放3這個(gè)字面值的地址,然后將a指向3的地址。接著處理int?b?=?3;在創(chuàng)建完b的引用變量后,由于在棧中已經(jīng)有3這個(gè)字面值,便將b直接指向3的地址。這樣,就出現(xiàn)了a與b同時(shí)均指向3的情況。
- StackOverflowError
- 方法壓入和彈出
- 連續(xù)
1.引用類型總是被分配到“堆”上。不論是成員變量還是局部
2.基礎(chǔ)類型總是分配到它聲明的地方:成員變量在堆內(nèi)存里,局部變量在棧內(nèi)存里。
比如
void func(){
int a = 3;
}
這是存在棧里的。局部方法。
而
class Test{
int a = 3;
}
這是隨對(duì)象放到堆里的。
堆
- 用來存放對(duì)象的,幾乎所有對(duì)象都放在這里,被線程共享的,或者說是被棧共享的
- ??臻g操作起來最快但是棧很小,通常大量的對(duì)象都是放在堆空間,理論上整個(gè)內(nèi)存沒有被其他進(jìn)程使用的空間甚至硬盤上的虛擬內(nèi)存都可以被當(dāng)成堆空間來使用(Android中不適用)。
- 堆又可以分為新生代和老年代,實(shí)際還有一個(gè)區(qū)域叫永久代,但是jdk1.7已經(jīng)去永久代了,所以可以當(dāng)作沒有,永久代是當(dāng)jvm啟動(dòng)時(shí)就存放的JDK自身的類和接口數(shù)據(jù),關(guān)閉則釋放。
- oom
- 新生代:主要是用來存放新生的對(duì)象。一般占據(jù)堆的1/3空間。由于頻繁創(chuàng)建對(duì)象,所以新生代會(huì)頻繁觸發(fā)MinorGC進(jìn)行垃圾回收。
方法區(qū)
類信息、常量池、靜態(tài)變量,即類被編譯后的數(shù)據(jù)
類信息
類型全限定名
類型是類類型還是接口類型。
類型的訪問修飾符(public、abstract或final的某個(gè)子集)。
類型的常量池。
字段名和屬性
方法名和屬性
除了常量以外的所有類(靜態(tài))變量。
常量池的好處
常量池是為了避免頻繁的創(chuàng)建和銷毀對(duì)象而影響系統(tǒng)性能,其實(shí)現(xiàn)了對(duì)象的共享。例如字符串常量池,在編譯階段就把所有的字符串文字放到一個(gè)常量池中。
(1)節(jié)省內(nèi)存空間:常量池中所有相同的字符串常量被合并,只占用一個(gè)空間。
(2)節(jié)省運(yùn)行時(shí)間:比較字符串時(shí),==比equals()快。對(duì)于兩個(gè)引用變量,只用==判斷引用是否相等,也就可以判斷實(shí)際值是否相等。
String s0=”kvill”;
String s1=”kvill”;
String s2=”kv” “ill”;
System.out.println( s0==s1 );
System.out.println( s0==s2 );
結(jié)果為:
true true
s0和s1中的”kvill”都是字符串常量,它們?cè)诰幾g期就被確定了,存放在常量池
用new String() 創(chuàng)建的字符串不是常量,不能在編譯期就確定,所以new String() 創(chuàng)建的字符串不放入常量池中,它們有自己的地址空間。存放在堆
String s0=”kvill”;
String s1=new String(”kvill”);
String s2=”kv” new String(“ill”);
System.out.println( s0==s1 );
System.out.println( s0==s2 );
System.out.println( s1==s2 );
結(jié)果為: false false false
String str = new String("hello");
上面的語句中變量str放在棧上,用new創(chuàng)建出來的字符串對(duì)象放在堆上,而"hello"這個(gè)字面量放在靜態(tài)區(qū)。
對(duì)象
- 虛擬機(jī)遇到一條new指令時(shí),首先會(huì)檢查這個(gè)指令的參數(shù)是否能在常量池里定位到這個(gè)類符號(hào)的引用,并且檢查這個(gè)符號(hào)引用的類是否已被加載、解析和初始化過。如果沒有,那必須先執(zhí)行相應(yīng)的類加載過程。
對(duì)象的內(nèi)存布局
- 對(duì)象在內(nèi)存中存儲(chǔ)的布局可以分為3個(gè)部分:對(duì)象頭、實(shí)例數(shù)據(jù)、對(duì)齊填充。
- 對(duì)象頭包括兩部分信息,第一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼 (HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時(shí)間戳等,Mark Word
- 第三部分對(duì)齊填充并不是必然存在的,也沒有特別的含義,它僅僅起著占位符的作用。由于HotSpot VM的自動(dòng)內(nèi)存管理系統(tǒng)要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍,換句話說,就是對(duì)象的大小必須是8字節(jié)的整數(shù)倍。而對(duì)象頭部分正好是8字節(jié)的倍數(shù)(1倍或者2倍),因此,當(dāng)對(duì)象實(shí)例數(shù)據(jù)部分沒有對(duì)齊時(shí),就需要通過對(duì)齊填充來補(bǔ)全。
對(duì)象的訪問定位
建立對(duì)象是為了使用對(duì)象,我們的Java程序需要通過棧上的reference數(shù)據(jù)來操作堆上的具體對(duì)象。由于reference類型在Java虛擬機(jī)規(guī)范中只規(guī)定了一個(gè)指向?qū)ο蟮囊?,并沒有定義這個(gè)引用應(yīng)該通過何種方式去定位、訪問堆中的對(duì)象的具體位置,所以對(duì)象訪問方式也是取決于虛擬機(jī)實(shí)現(xiàn)而定的。目前主流的訪問方式有使用句柄和直接指針兩種。
如果使用句柄訪問的話,那么Java堆中將會(huì)劃分出一塊內(nèi)存來作為句柄池,reference中存儲(chǔ)的就是對(duì)象的句柄地址,而句柄中包含了對(duì)象實(shí)例數(shù)據(jù)與類型數(shù)據(jù)各自的具體地址信息

如果使用直接指針訪問,那么Java堆對(duì)象的布局中就必須考慮如何放置訪問類型數(shù)據(jù)的相關(guān)信息,而reference中存儲(chǔ)的直接就是對(duì)象地址

使用直接指針訪問方式的最大好處就是速度更快,它節(jié)省了一次指針定位的時(shí)間開銷,由于對(duì)象的訪問在Java中非常頻繁,因此這類開銷積少成多后也是一項(xiàng)非??捎^的執(zhí)行成本。就本書討論的主要虛擬機(jī)Sun HotSpot而言,它是使用第二種方式進(jìn)行對(duì)象訪問的
模擬溢出
堆溢出
當(dāng)出現(xiàn)Java堆內(nèi)存溢出時(shí),異常堆
棧信息“java.lang.OutOfMemoryError”會(huì)跟著進(jìn)一步提示“Java heap space”
java.lang.OutOfMemoryError:Java heap space
Dumping heap to java_pid3404.hprof……
Heap dump file created[22045981 bytes in 0.663 secs
Eclipse Memory Analyzer
如果是內(nèi)存泄露,可進(jìn)一步通過工具查看泄露對(duì)象到GC Roots的引用鏈。于是就能找到泄露對(duì)象是
通過怎樣的路徑與GC Roots相關(guān)聯(lián)并導(dǎo)致垃圾收集器無法自動(dòng)回收它們的。掌握了泄露對(duì)象的類型信息及GC Roots引用鏈的信息,就可以比較準(zhǔn)確地定位出泄露代碼的位置。
StackOverflowError
在單個(gè)線程下,無論是由于棧幀太大還是虛擬機(jī)棧容量太小,當(dāng)內(nèi)存無法分配的時(shí)候,虛擬機(jī)拋出的都是StackOverflowError異常。
public class JavaVMStackSOF{
private int stackLength=1;
public void stackLeak(){
stackLength ;
stackLeak();
}
public static void main(String[]args)throws Throwable{
JavaVMStackSOF oom=new JavaVMStackSOF();
try{
oom.stackLeak();
}catch(Throwable e){
System.out.println("stack length:" oom.stackLength);
throw e;
}
}
}
方法區(qū)和運(yùn)行時(shí)常量池溢出
運(yùn)行時(shí)常量池溢出,在OutOfMemoryError后面跟隨的提示信息是“PermGen space”,說明運(yùn)行時(shí)常量池屬于方法區(qū)(HotSpot虛擬機(jī)中的永久代)的一部分。
public class RuntimeConstantPoolOOM{
public static void main(String[]args){
public static void main(String[]args){
String str1=new StringBuilder("計(jì)算機(jī)").append("軟件").toString();
System.out.println(str1.intern()==str1);
String str2=new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern()==str2);
}
}
}
這段代碼在JDK 1.6中運(yùn)行,會(huì)得到兩個(gè)false,而在JDK 1.7中運(yùn)行,會(huì)得到一個(gè)true和一個(gè)false
JDK 1.7(以及部分其他虛擬機(jī),例如JRockit)的intern()實(shí)現(xiàn)不會(huì)再?gòu)?fù)制實(shí)例,只是在常量池中記錄首次出現(xiàn)的實(shí)例引用,因此intern()返回的引用和由StringBuilder創(chuàng)建的那個(gè)字符串實(shí)例是同一個(gè)。
對(duì)str2比較返回false是因?yàn)椤癹ava”這個(gè)字符串在執(zhí)行StringBuilder.toString()之前已經(jīng)出現(xiàn)過,字符串常量池中已經(jīng)有它的引用了,不符合“首次出現(xiàn)”的原則,而“計(jì)算機(jī)軟件”這個(gè)字符串則是首次出現(xiàn)的,因此返回true。
理解GC日志
33.125:[GC[DefNew:3324K->152K(3712K),0.0025925 secs]3324K->152K(11904K),0.0031680 secs]
1 0 0.6 6 7:[F u l l G C[T e n u r e d:0 K->2 1 0 K(1 0 2 4 0 K),0.0 1 4 9 1 4 2 s e c s]4603K->
210K(19456K),[Perm:2999K->2999K(21248K)],0.0150007 secs][Times:user=0.01 sys=0.00,real=0.02 secs]
最前面的數(shù)字“33.125:”和“100.667:”代表了GC發(fā)生的時(shí)間,這個(gè)數(shù)字的含義是從Java虛擬機(jī)啟動(dòng)以來經(jīng)過的秒數(shù)。
接下來的“[DefNew”、“[Tenured”、“[Perm”表示GC發(fā)生的區(qū)域,這里顯示的區(qū)域名稱與使用的GC收集器是密切相關(guān)的,例如上面樣例所使用的Serial收集器中的新生代名為“Default New Generation”,所以顯示的是“[DefNew”。如果是ParNew收集器,新生代名稱就會(huì)變?yōu)椤癧ParNew”,意為“Parallel NewGeneration”。如果采用Parallel Scavenge收集器,那它配套的新生代稱為“PSYoungGen”,老年代和永久代同理,名稱也是由收集器決定的。
后面方括號(hào)內(nèi)部的“3324K->152K(3712K)”含義是“GC前該內(nèi)存區(qū)域已使用容量->GC后該內(nèi)存區(qū)域已使用容量(該內(nèi)存區(qū)域總?cè)萘浚?。而在方括?hào)之外的“3324K->152K(11904K)”表示“GC前
Java堆已使用容量->GC后Java堆已使用容量(Java堆總?cè)萘浚薄?br>
再往后,“0.0025925 secs”表示該內(nèi)存區(qū)域GC所占用的時(shí)間,單位是秒。
第3章 垃圾收集器與內(nèi)存分配策略
垃圾收集器與內(nèi)存分配策略
哪些內(nèi)存需要回收?
什么時(shí)候回收?
如何回收?
- Java內(nèi)存運(yùn)行時(shí)區(qū)域的各個(gè)部分,其中程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧3個(gè)區(qū)域隨線程而生,隨線程而滅;棧中的棧幀隨著方法的進(jìn)入和退出而有條不紊地執(zhí)行著出棧和入棧操作。每一個(gè)棧幀中分配多少內(nèi)存基本上是在類結(jié)構(gòu)確定下來時(shí)就已知的(盡管在運(yùn)行期會(huì)由JIT編譯器進(jìn)行一些優(yōu)化,但在本章基于概念模型的討論中,大體上可以認(rèn)為是編譯期可知的),因此這幾個(gè)區(qū)域的內(nèi)存分配和回收都具備確定性,在這幾個(gè)區(qū)
域內(nèi)就不需要過多考慮回收的問題,因?yàn)榉椒ńY(jié)束或者線程結(jié)束時(shí),內(nèi)存自然就跟隨著回收了。
- 而Java堆和方法區(qū)則不一樣,一個(gè)接口中的多個(gè)實(shí)現(xiàn)類需要的內(nèi)存可能不一樣,一個(gè)方法中的多個(gè)分支需要的內(nèi)存也可能不一樣,我們只有在程序處于運(yùn)行期間時(shí)才能知道會(huì)創(chuàng)建哪些對(duì)象,這部分內(nèi)存的分配和回收都是動(dòng)態(tài)的,垃圾收集器所關(guān)注的是這部分內(nèi)存。
對(duì)象存活判定算法
引用計(jì)數(shù)算法
- 給對(duì)象中添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí),計(jì)數(shù)器值就加1;當(dāng)引用失效時(shí),計(jì)數(shù)器值就減1;任何時(shí)刻計(jì)數(shù)器為0的對(duì)象就是不可能再被使用的。
- 但是,至少主流的Java虛擬機(jī)里面沒有選用引用計(jì)數(shù)算法來管理內(nèi)存,其中最主要的原因是它很難解決對(duì)象之間相互循環(huán)引用的問題。
- 舉個(gè)簡(jiǎn)單的例子,請(qǐng)看代碼清單3-1中的testGC()方法:對(duì)象objA和objB都有字段instance,賦值令objA.instance=objB及objB.instance=objA,除此之外,這兩個(gè)對(duì)象再無任何引用,實(shí)際上這兩個(gè)對(duì)象已經(jīng)不可能再被訪問,但是它們因?yàn)榛ハ嘁弥鴮?duì)方,導(dǎo)致它們的引用計(jì)數(shù)都不為0,于是引用計(jì)數(shù)算法無法通知GC收集器回收它們。
虛擬機(jī)并不是通過引用計(jì)數(shù)算法來判斷對(duì)象是否存活的。
可達(dá)性分析算法
基本思路就是通過一系列的稱為“GC Roots”的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜索所走過的路徑稱為引用鏈(ReferenceChain),當(dāng)一個(gè)對(duì)象到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個(gè)對(duì)象不可達(dá))時(shí),則證明此對(duì)象是不可用的

即使在可達(dá)性分析算法中不可達(dá)的對(duì)象,也并非是“非死不可”的,這時(shí)候它們暫時(shí)處于“緩刑”階段,要真正宣告一個(gè)對(duì)象死亡,至少要經(jīng)歷兩次標(biāo)記過程:
如果對(duì)象在進(jìn)行可達(dá)性分析后發(fā)現(xiàn)沒有與GCRoots相連接的引用鏈,那它將會(huì)被第一次標(biāo)記并且進(jìn)行一次篩選,篩選的條件是對(duì)象是否有必要執(zhí)行finalize()方法。
當(dāng)對(duì)象沒有覆蓋finalize()方法,或者finalize()方法已經(jīng)被虛擬機(jī)調(diào)用過,虛擬機(jī)將這兩種情況都視為“沒有必要執(zhí)行”。
代碼中有兩段完全一樣的代碼片段,執(zhí)行結(jié)果卻是一次逃脫成功,一次失敗,這是因?yàn)槿魏我粋€(gè)對(duì)象的finalize()方法都只會(huì)被系統(tǒng)自動(dòng)調(diào)用一次,如果對(duì)象面臨下一次回收,它的finalize()方法不會(huì)被再次執(zhí)行,因此第二段代碼的自救行動(dòng)失敗了。
finalize()能做的所有工作,使用try-finally或者其他方式都可以做得更好、更及時(shí),所以筆者建議大家完全可以忘掉Java語言中有這個(gè)方法的存在。
在Java語言中,可作為GC Roots的對(duì)象包括下面幾種:
虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象。
局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型(boolean、byte、char、short、int、float、long、double)、對(duì)象引用(reference類型,它不等同于對(duì)象本身,可能是一個(gè)指向?qū)ο笃鹗嫉刂返囊弥羔槪?br>
也可能是指向一個(gè)代表對(duì)象的句柄或其他與此對(duì)象相關(guān)的位置)和returnAddress類型(指向了一條字節(jié)碼指令的地址)。
- 方法區(qū)中類靜態(tài)屬性引用的對(duì)象。
- 方法區(qū)中常量引用的對(duì)象。
本地方法棧中JNI(即一般說的Native方法)引用的對(duì)象
再談引用
在JDK 1.2以前,Java中的引用的定義很傳統(tǒng):如果reference類型的數(shù)據(jù)中存儲(chǔ)的數(shù)值代表的是另外一塊內(nèi)存的起始地址,就稱這塊內(nèi)存代表著一個(gè)引用。
在JDK 1.2之后,Java對(duì)引用的概念進(jìn)行了擴(kuò)充,將引用分為強(qiáng)引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4種,這4種引用強(qiáng)度依次
逐漸減弱。
- 強(qiáng)引用就是指在程序代碼之中普遍存在的,類似“Object obj=new Object()”這類的引用,只要強(qiáng)引用還存在,垃圾收集器永遠(yuǎn)不會(huì)回收掉被引用的對(duì)象。
- 軟引用是用來描述一些還有用但并非必需的對(duì)象。對(duì)于軟引用關(guān)聯(lián)著的對(duì)象,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會(huì)把這些對(duì)象列進(jìn)回收范圍之中進(jìn)行第二次回收。如果這次回收還沒有足夠的內(nèi)存,才會(huì)拋出內(nèi)存溢出異常。
- 弱引用也是用來描述非必需對(duì)象的,但是它的強(qiáng)度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對(duì)象只能生存到下一次垃圾收集發(fā)生之前。當(dāng)垃圾收集器工作時(shí),無論當(dāng)前內(nèi)存是否足夠,都會(huì)回收掉只被弱引用關(guān)聯(lián)的對(duì)象。
垃圾收集算法
標(biāo)記-清除算法
算法分為“標(biāo)記”和“清除”兩個(gè)階段:首先標(biāo)記出所有需要回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對(duì)象。
這樣做的好處:
數(shù)組 a[10] 中存儲(chǔ)了 8 個(gè)元素:a,b,c,d,e,f,g,h?,F(xiàn)在,我們要依次刪除 a,b,c 三個(gè)元素。
為了避免 d,e,f,g,h 這幾個(gè)數(shù)據(jù)會(huì)被搬移三次,我們可以先記錄下已經(jīng)刪除的數(shù)據(jù)。每次的刪除操作并不是真正地搬移數(shù)據(jù),只是記錄數(shù)據(jù)已經(jīng)被刪除。當(dāng)數(shù)組沒有更多空間存儲(chǔ)數(shù)據(jù)時(shí),我們?cè)儆|發(fā)執(zhí)行一次真正的刪除操作,這樣就大大減少了刪除操作導(dǎo)致的數(shù)據(jù)搬移。
它的主要不足有兩個(gè):一個(gè)是效率問題,標(biāo)記和清除兩個(gè)過程的效率都不高;另一個(gè)是空間問題,標(biāo)記清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會(huì)導(dǎo)致以后在程序運(yùn)行過程中需要分配較大對(duì)象時(shí),無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動(dòng)作。

復(fù)制算法
它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。
這樣使得每次都是對(duì)整個(gè)半?yún)^(qū)進(jìn)行內(nèi)存回收,內(nèi)存分配時(shí)也就不用考慮內(nèi)存碎片等復(fù)雜情況,只要移動(dòng)堆頂指針,按順序分配內(nèi)存即可,實(shí)現(xiàn)簡(jiǎn)單,運(yùn)行高效。只是這種算法的代價(jià)是將內(nèi)存縮小為了原來的一半,未免太高了一點(diǎn)。

- 新生代中的對(duì)象98%是“朝生夕死”的,所以并不需要按照1:1的比例來劃分內(nèi)存空間,而是將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor [1] 。
- 當(dāng)回收時(shí),將Eden和Survivor中還存活著的對(duì)象一次性地復(fù)制到另外一塊Survivor空間上,最后清理掉Eden和剛才用過的Survivor空間。
- HotSpot虛擬機(jī)默認(rèn)Eden和Survivor的大小比例是8:1,也就是每次新生代中可用內(nèi)存空間為整個(gè)新生代容量的90%(80% 10%),只有10%的內(nèi)存會(huì)被“浪費(fèi)”。
- 當(dāng)然,98%的對(duì)象可回收只是一般場(chǎng)景下的數(shù)據(jù),我們沒有辦法保證每次回收都只有不多于10%的對(duì)象存活,當(dāng)Survivor空間不夠用時(shí),需要依賴其他內(nèi)存(這里指老年代)進(jìn)行分配擔(dān)保(Handle Promotion)。
標(biāo)記-整理算法
- 復(fù)制收集算法在對(duì)象存活率較高時(shí)就要進(jìn)行較多的復(fù)制操作,效率將會(huì)變低。更關(guān)鍵的是,如果不想浪費(fèi)50%的空間,就需要有額外的空間進(jìn)行分配擔(dān)保,以應(yīng)對(duì)被使用的內(nèi)存中所有對(duì)象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。
- 標(biāo)記過程仍然與“標(biāo)記-清除”算法一樣,但后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向一端移動(dòng),然后直接清理掉端邊界以外的內(nèi)存。

分代收集算法
一般是把Java堆分為新生代和老年代,這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴ā?/p>
標(biāo)記-清除算法 (會(huì)產(chǎn)生空閑內(nèi)存碎片)
標(biāo)記-整理算法(防止產(chǎn)生內(nèi)存碎片)
復(fù)制算法(效率最高,但是內(nèi)存利用率低)
JVM 中新生代使用復(fù)制算法,老年代使用標(biāo)記整理算法。
第7章:虛擬機(jī)類加載機(jī)制
/**
*被動(dòng)使用類字段演示一:
*通過子類引用父類的靜態(tài)字段,不會(huì)導(dǎo)致子類初始化
**/
public class SuperClass{
static{
System.out.println("SuperClass init!");
}
public static int value=123;
}
public class SubClass extends SuperClass{
static{
System.out.println("SubClass init!");
}
}
/**
*非主動(dòng)使用類字段演示
**/
public class NotInitialization{
public static void main(String[]args){
System.out.println(SubClass.value);
}
}
上述代碼運(yùn)行之后,只會(huì)輸出“SuperClass init!”,而不會(huì)輸出“SubClass init!”。對(duì)于靜態(tài)字段,只有直接定義這個(gè)字段的類才會(huì)被初始化,因此通過其子類來引用父類中定義的靜態(tài)字段,只會(huì)觸發(fā)父類
的初始化而不會(huì)觸發(fā)子類的初始化。
/**
*被動(dòng)使用類字段演示三:
*常量在編譯階段會(huì)存入調(diào)用類的常量池中,本質(zhì)上并沒有直接引用到定義常量的類,因此不會(huì)觸發(fā)定義常量的類的初始化。
**/
public class ConstClass{
static{
System.out.println("ConstClass init!");
}
public static final String HELLOWORLD="hello world";
}
/**
*非主動(dòng)使用類字段演示
**/
public class NotInitialization{
public static void main(String[]args){
System.out.println(ConstClass.HELLOWORLD);
}
}
上述代碼運(yùn)行之后,也沒有輸出“ConstClass init!”
虛擬機(jī)把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的Java類型,這就是虛擬機(jī)的類加載機(jī)制(運(yùn)行時(shí))。
類從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個(gè)生命周期包括:加載(Loading)、驗(yàn)證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個(gè)階段。其中驗(yàn)證、準(zhǔn)備、解析3個(gè)部分統(tǒng)稱為連接(Linking)

new一個(gè)對(duì)象發(fā)生了什么
java在new一個(gè)對(duì)象的時(shí)候,會(huì)先查看對(duì)象所屬的類有沒有被加載到內(nèi)存,如果沒有的話,就會(huì)先通過類的全限定名來加載。加載并初始化類完成后,再進(jìn)行對(duì)象的創(chuàng)建工作。我們先假設(shè)是第一次使用該類,這樣的話new一個(gè)對(duì)象就可以分為兩個(gè)過程:加載并初始化類?和創(chuàng)建對(duì)象。
一、類加載過程(第一次使用該類)
java是使用雙親委派模型?來進(jìn)行類的加載的,所以在描述類加載過程前,我們先看一下它的工作過程:
1、加載
2、驗(yàn)證
3、準(zhǔn)備
4、解析
5、初始化(先父后子)
為靜態(tài)變量賦值
執(zhí)行static代碼塊
二、創(chuàng)建對(duì)象
1、在堆區(qū)分配對(duì)象需要的內(nèi)存分配的內(nèi)存包括本類和父類的所有實(shí)例變量,但不包括任何靜態(tài)變量
2、對(duì)所有實(shí)例變量賦默認(rèn)值將方法區(qū)內(nèi)對(duì)實(shí)例變量的定義拷貝一份到堆區(qū),然后賦默認(rèn)值
3、執(zhí)行實(shí)例初始化代碼初始化順序是先初始化父類再初始化子類,初始化時(shí)先執(zhí)行實(shí)例代碼塊然后是構(gòu)造方法
4、如果有類似于Child c = new Child()形式的c引用的話,在棧區(qū)定義Child類型引用變量c,然后將堆區(qū)對(duì)象的地址賦值給它需要注意的是,每個(gè)子類對(duì)象持有父類對(duì)象的引用?,可在內(nèi)部通過super關(guān)鍵字來調(diào)用父類對(duì)象,但在外部不可訪問
來源:https://www./content-4-629051.html
|