原文連接:a870439570.github.io/interview-d… #思惟導圖 java
1. 運行時數據區域
Java虛擬機在執行Java程序的過程當中會把它所管理的內存劃分爲若干個不一樣的數據區域。這些區域有各自的用途,以及建立和銷燬的時間,有的區域隨着虛擬機進程的啓動而存在,有些區域則是依賴用戶線程的啓動和結束而創建和銷燬。
線程私有的:虛擬機棧,本地方法棧,程序計數器
線程共享的 方法區,堆
程序計數器
程序計數器是一塊較小的內存空間,它的做用能夠看做是當前線程所執行的字節碼行號指示器,在虛擬機的概念模型裏,字節碼解釋器工做時 就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令,分支,循環,跳轉,異常處理,線程恢復等基礎功能都須要這個計數器來完成。(若是正在執行的是本地方法則計數器爲空)。
Java虛擬機棧
虛擬機棧描述的是Java方法執行的內存模型:每一個方法被執行的時候都會建立一個棧幀用於存儲局部變量表,操做棧,動態連接,方法出口等信息。每個方法被調用直至執行完成的過程,就對應着一個棧幀在虛擬機中從入棧到出棧的過程。
本地方法棧
本地方法棧與 Java 虛擬機棧相似,它們之間的區別只不過是本地方法棧爲本地方法服務。git
Java 堆
Java
堆是整個虛擬機所管理的最大內存區域,全部的對象建立都是在這個區域進行內存分配。
這塊區域也是垃圾回收器重點管理的區域,因爲大多數垃圾回收器都採用分代回收算法
,全部堆內存也分爲 新生代
、老年代
,能夠方便垃圾的準確回收。
方法區
方法區主要用於存放已經被虛擬機加載的類信息,如常量,靜態變量
,即時編譯器編譯後的代碼等。和Java堆同樣不須要連續的內存,而且能夠動態擴展。
對這塊區域進行垃圾回收的主要目標是對常量池的回收和對類的卸載,可是通常比較難實現。
運行時常量池
運行時常量池是方法區的一部分。class文件除了有類的版本,字段,方法,接口等描述信息外,還有一項信息是常量池,用於存放編譯期生成的各類字面量和符號引用,會在類加載後放入這個區域。
直接內存
直接內存並非虛擬機運行時數據區域的一部分。
在 JDK 1.4 中新加入了 NIO 類,它可使用 Native 函數庫直接分配堆外內存,而後經過 Java堆裏的 DirectByteBuffer 對象做爲這塊內存的引用進行操做。這樣能在一些場景中顯著提升性能,由於避免了在堆內存和堆外內存來回拷貝數據。
2. Minor GC和Full GC
Minor GC:指發生在新生代的垃圾收集動做,由於 Java 對象大多都具 備朝生夕滅的特性,因此Minor GC
很是頻繁,通常回收速度也比較快。
Major GC或Full GC:指發生在老年代的 GC,出現了 Major GC,常常 會伴隨至少一次的 Minor GC(但非絕對的,在 ParallelScavenge 收集器的收集策略裏 就有直接進行 Major GC
的策略選擇過程) 。MajorGC
的速度通常會比 Minor GC 慢 10 倍以上。
Minor GC觸發機制
當年輕代滿時就會觸發Minor GC,這裏的年輕代滿指的是Eden代滿,Survivor滿不會引起GCgithub
Full GC觸發機制:
當年老代滿時會引起Full GC,Full GC將會同時回收年輕代、年老代,
當永久代滿時也會引起Full GC,會致使Class、Method元信息的卸載
3. Java中的四種引用
強引用,軟引用,弱引用,虛引用算法
強引用
就是指在程序代碼中廣泛存在的,相似Object obj=new Object()這類的引用,只要強引用還存在,垃圾回收期永遠不會回收掉被引用的對象數據庫
軟引用
用來描述一些還有用,但並不是必須的對象。對於軟引用關聯着的對象,在系統將要發生內存溢出前,將會把這些對象列進回收範圍以內並進行第二次回收,若是這這次回收仍是沒有足夠的內存,纔會拋出內存溢出。數組
弱引用
用來描述非必須的對象,可是它的強度比軟引用更弱一下,被弱引用關聯的對象,只能生存到下一次垃圾收集發生以前。當垃圾收集器工做時,不管當前內存是否足夠,只會回收被弱引用關聯的對象安全
虛引用
被稱爲幽靈引用或幻引用,是最弱的一種引用關係,一個對象是否有虛引用的存在,徹底不會對其它生存時間構成影響,也沒法經過虛引用來取得一個實列。爲一個對象設置虛引用的目的就是在對象被回收時收到一個系統通知。bash
4. 垃圾收集算法
1.Serial收集器
一個單線程的收集器,只會使用一個CPU或一條收集線程去完成垃圾收集工做。在進行垃圾收集時必須暫停其它全部的工做線程,直接到結束。(Stop The Word)這項工做是虛擬機在後臺自動發起和完成的。
JDK1.3以前是新生代收集的惟一選擇。
它依然是虛擬機運行在Client模式下的默認新手代收集器,簡單而高效。
2. ParNew收集器
Serial收集器的多線程版本,使用多條線程收集。其他的和Serial同樣,是許多運行在Server模式下的虛擬機首選新生代收集器。且目前除了Serial收集器,只有它能夠與CMS收集器配合工做微信
3.Parallel Scavenge收集器
它是一款新生代收集器。使用複製算法收集,又是並行的多線程收集器
特色是達到一個可控制的吞吐量,也被稱爲「吞吐量優先」收集器。
4.Serial Old收集器
它是Serial收集器的老年代版本,是一個單線程收集器,使用標記-整理算法收集。
主要意義是給Client模式下虛擬機使用。若是是Server模式,則有兩種用途,一是在JDK1.5以前與Parallel Scavenge收集器搭配使用。二是做爲CMS收集器的後背預案
5.Parallel Old收集器
它是Parallel Scavenge收集器的老年代版本,使用多線程和標記-整理算法。JDK1.6纔開始提供。網絡
6.CMS收集器
是一種以獲取最短回收停頓時間的爲目標的收集器。基於標記-清楚算法實現。
運做過程分爲四個階段。初始標記,併發標記,從新標記,併發清除。
初始標記和併發標記仍然須要"Stop The Word".初始標記只是記錄下GC Roots能直接關聯到對象,速度快。併發標記就是進行GC Roots Tracing過程。從新標記修正併發標記期間因程序繼續運做致使標記產生變更的一部分對象的標記記錄。整個過程耗時最長是併發標記和併發清除過程。
優勢是併發收集,低停頓。缺點是:對CPU資源很是敏感,沒法處理浮動垃圾。收集結束時會產生大量空間碎片
7.G1收集器
當前收集器技術最前沿成果之一。將整個Java堆分爲多個大小相等的獨立區域。雖然保留新生代和老年代,但它們再也不是物理隔離,都是一部分不須要連續的集合。
特色是並行與併發充分利用CPU縮短停頓時間。分代收集,空間整合不會產生內存空間碎片,可預測的停頓。有計劃的避免回收整個Java堆。
運行大體分爲:初始標記,併發標記,最終標記,篩選回收。
標記-清除算法
算法分爲標記和清除兩個階段。首先先標記全部要被回收的對象,標記完成後再統一清除被標記的對象。
主要缺點有兩個,
一是效率問題,標記和清除的過程效率都不高。二是空間問題,標記清除後會產生大量不連續的內存碎片,空間碎片太多,可能會致使,當程序在之後的運行過程當中須要分配較大的對象時沒法找到足夠的連續內存,而不得不提早出發另外一次垃圾收集動做
複製算法
爲了解決效率問題,一種複製收集的算法出現了。它將可用內存按容量劃分爲大小相等的兩塊,每次只用其中的一塊。當這一塊內存用完,就將還存活着的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。這樣使得每次都是對其中一塊進行內存回收,內存分配時也就不用內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可,實現簡單,運行高效。只是這種算法的代價是將內存縮小爲原來的一半未免過高了一點。
標記-整理算法
複製手機算法在對象存活率較高的時要執行多的複製操做,效率將會變低。更關鍵的是,若是不想浪費50%的空間,就須要額外的空間進行分配擔保,以應對被使用的內存中對象都100%存貨的極端狀況,因此在老年代通常不能直接選用這種算法。根據老年代的特色,有人提出了另外一種 標記-整理的算法,標記過程仍然與 標記-清楚算法同樣。但後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象向一端移動,而後直接清理掉端邊界之外的內存
分代收集算法
根據對象的存活週期的不一樣將內存劃分爲幾塊。通常是把Java堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少許存活,那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。而老年代中由於對象存活率高,沒有額外空間對它進行分配擔保,就必須使用標記-清理或標記-整理算法來進行回收
5. 內存分配與回收策略
對象的內存分配,往大方向講,就是在堆上分配,對象主要分配在新生代的Eden區上,若是啓動本地線程分配緩衝,將按線程的優先級在TLAB上分配。少數狀況也可能分配在老年代中,分配的規則並非百分之白固定,其細節取決於當前使用的是哪種垃圾回收期組合,還有虛擬機中於內存相關的參數設置。
對象優先在Eden區分配
對象一般在新生代的Eden區進行分配,當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC,與Minor GC對應的是Major GC、Full GC。
Minor GC:指發生在新生代的垃圾收集動做,很是頻繁,速度較快。
Major GC:指發生在老年代的GC,出現Major GC,常常會伴隨一次Minor GC,同時Minor GC也會引發Major GC,通常在GC日誌中統稱爲GC,不頻繁。
Full GC:指發生在老年代和新生代的GC,速度很慢,須要Stop The World。
大對象直接進入老年代
須要大量連續內存空間的Java對象稱爲大對象,大對象的出現會致使提早觸發垃圾收集以獲取更大的連續的空間來進行大對象的分配。虛擬機提供了-XX:PretenureSizeThreadshold參數來設置大對象的閾值,超過閾值的對象直接分配到老年代。
長期存活的對象進入老年代
每一個對象有一個對象年齡計數器,與前面的對象的存儲佈局中的GC分代年齡對應。對象出生在Eden區、通過一次Minor GC後仍然存活,並可以被Survivor容納,設置年齡爲1,對象在Survivor區每次通過一次Minor GC,年齡就加1,當年齡達到必定程度(默認15),就晉升到老年代,虛擬機提供了-XX:MaxTenuringThreshold來進行設置。
動態對象年齡判斷
對象的年齡到達了MaxTenuringThreshold能夠進入老年代,同時,若是在survivor區中相同年齡全部對象大小的總和大於survivor區的一半,年齡大於等於該年齡的對象就能夠直接進入老年代。無需等到MaxTenuringThreshold中要求的年齡。
具體代碼以下:
public class AllocationTest {
private static final int _1MB = 1024 * 1024;
/*
* -Xms20M -Xmx20M -Xmn10M
-XX:SurvivorRatio=8
-XX:+PrintGCDetails
-XX:+UseSerialGC
-XX:MaxTenuringThreshold=15
-XX:+PrintTenuringDistribution
* */
public static void testTenuringThreshold2 () {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[_1MB / 4];
allocation2 = new byte[_1MB / 4];
allocation3 = new byte[4 * _1MB];
allocation4 = new byte[4 * _1MB];
allocation4 = null;
allocation4 = new byte[4 * _1MB];
}
public static void main(String[] args) {
test PretenureSizeThreshold2();
}
}
複製代碼
空間分配擔保
發生Minor GC時,虛擬機會檢查老年代連續的空閒區域是否大於新生代全部對象的總和,若成立,則說明Minor GC是安全的,不然,虛擬機須要查看HandlePromotionFailure的值,看是否運行擔保失敗,若容許,則虛擬機繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,若大於,將嘗試進行一次Minor GC;若小於或者HandlePromotionFailure設置不運行冒險,那麼此時將改爲一次Full GC,以上是JDK Update 24以前的策略,以後的策略改變了,只要老年代的連續空間大於新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,不然將進行Full GC。
冒險是指通過一次Minor GC後有大量對象存活,而新生代的survivor區很小,放不下這些大量存活的對象,因此須要老年代進行分配擔保,把survivor區沒法容納的對象直接進入老年代。
回收方法區
不少人任務方法區是沒有垃圾回收的,Java虛擬機規範中確實說過能夠不要求虛擬機在方法區實現垃圾收集,而在方法去進行垃圾收集的性價比通常比較低,在堆中,由其是在新生代中,常規應用進行一次垃圾收集通常能夠回收70%~96%的空間,而永久代的垃圾收集效率遠低於此。
永久代的垃圾主要回收兩部份內容:廢棄常量和無用的類。
回收廢棄常量於回收Java堆
中的對象很是類似。以常量池中字面量的回收爲列,假如一個字符串「abc
"已經進入常量池中,可是當前系統沒有任何一個String對象叫作」abc
"的,換句話就是沒有任何Sting對象引用常量池中的"abc",也沒有其它地方引用了這個字面變量,若是這時候發生內存回收,並且必要的話,這個「abc
"常量就會被系統請出常量池,常量池中的其它類,接口,方法,字段的符號引用也與此相似。
image.png
Java中對象訪問是如何進行的
對象訪問在Java中無處不在,即時是最簡單的訪問也會涉及到Java棧,Java堆,方法區這三個最重要的內存區域之間的關係。
Object obj=new Object();
複製代碼
假設這段代碼出如今方法體中, 那嗎「Object obj
」這部分的語義將會反應到Java棧
的本地變量中,做爲一個reference
類型數據出現。而「new Object()
」這部分的語義將會反應到Java堆
中,造成一塊存儲了Object類型全部實例數據值的結構化內存,根據具體類型以及虛擬機實現的對象內存佈局的不一樣,這塊內存的長度是不固定的。
另外,在Java堆中還必須包含能查找到此對象類型數據(如對象類型,父親,實現的接口,方法等)的地址消息,這些類型數據則存儲在方法區中。
怎樣判斷對象是否存活
是否使用引用計數法?不少判斷對象存活的算法是這樣的,給對象添加一個引用計數器,每當有一個地方引用它時,計數器值就加1,當引用失效時,計數器減1;
任什麼時候刻計數器都爲0的對象就是不可能再被使用的。客觀的來講,引用計數法的實現簡單,斷定效率也很高,在大部分狀況下是一個不錯的算法,也有一些著名的案例,列如微軟的COM技術,可是,在Java語言中沒有選用引用技術發來管理內存,其中最主要的緣由是由於它很難解決對象之間的互循環引用問題。
摘抄自<<深刻理解Java虛擬機>>一書中的原話
根搜索算法:Java是使用根搜索算法判斷對象是否存活的。
這個算法的思路就是經過一系列的名爲「GC roots"的對象做爲起點,從這些節點開始向下搜索,搜索走過的路徑稱爲引用鏈,當一個對象的GC roots沒有任何引用鏈相連時,則證實此對象是不可用的。以下圖所示,對象object5,object6,object7雖然相互關聯,可是他們的GC roots是不可達到的,因此它們將會被斷定是可回收的對象。
image.png
做爲GC roots的幾種對象
虛擬機棧(棧中的本地變量表)中的引用對象。
方法區中的類靜態屬性引用對象。
方法區中的常量引用的對象。
本地方法中JNI(即通常說的native方法)的引用的對象。
6. 虛擬機類加載機制
類加載的時機
類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。其中準備、驗證、解析3個部分統稱爲鏈接(Linking)
在這裏輸入圖片標題
加載、驗證、準備、初始化和卸載這5個階段的順序是肯定的,類的加載過程必須按照這種順序循序漸進地開始,而解析階段則不必定:它在某些狀況下能夠在初始化階段以後再開始,這是爲了支持Java語言的運行時綁定(也稱爲動態綁定或晚期綁定)。
什麼時候開始類加載的第一個階段
java虛擬機規範中並無進行強制約束,這點能夠交給虛擬機的具體實現來自由把握。可是對於初始階段,虛擬機規範則是嚴格規定了有且只有5種狀況必須當即對類進行初始化(而加載,驗證,準備天然須要再次以前開始)
遇到new,getstatic,pustatic
或invokestatic
這4條字節碼指令時,若是類沒有進行過初始化,則須要先觸發其初始化。生成這4條指令的最多見Java代碼場景是:使用new關鍵字實例化對象,讀取或設置一個類的靜態字段(被final修飾,已在編譯器把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
對類進行反射調用時,若是類沒有進行過初始化,則須要先觸發其初始化。
當初始化一個類時,若是發現父類尚未初始化,則須要先觸發父類初始化。
當虛擬機啓動時,用戶指定一個執行的主類,虛擬機會先初始化這個主類。
當使用jdk1.7動態語言支持時,若是一個實例最後解析結果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,而且這個方法句柄所對應的類沒有進行過初始化,則須要先觸發其初始化。
類的加載過程
1. 加載
在加載階段(能夠參考java.lang.ClassLoader的loadClass()方法),虛擬機須要完成如下3件事情:
經過一個類的全限定名來獲取定義此類的二進制字節流(並無指明要從一個Class文件中獲取,能夠從其餘渠道,譬如:網絡、動態生成、數據庫等);
將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構;
在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口;
加載階段和鏈接階段(Linking)的部份內容(如一部分字節碼文件格式驗證動做)是交叉進行的,加載階段還沒有完成,鏈接階段可能已經開始,但這些夾在加載階段之中進行的動做,仍然屬於鏈接階段的內容,這兩個階段的開始時間仍然保持着固定的前後順序。
2. 驗證
驗證是鏈接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。
驗證階段大體會完成4個階段的檢驗動做:
文件格式驗證:驗證字節流是否符合Class文件格式的規範;例如:是否以魔術0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理範圍以內、常量池中的常量是否有不被支持的類型。
元數據驗證:對字節碼描述的信息進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規範的要求;例如:這個類是否有父類,除了java.lang.Object以外。
字節碼驗證:經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的
符號引用驗證:確保解析動做能正確執行。
驗證階段是很是重要的,但不是必須的,它對程序運行期沒有影響,若是所引用的類通過反覆驗證,那麼能夠考慮採用-Xverifynone參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。
3. 準備
準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。這時候進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一塊兒分配在堆中。其次,這裏所說的初始值「一般狀況」下是數據類型的零值,假設一個類變量的定義爲:
public static int value=123;
複製代碼
那變量value在準備階段事後的初始值爲0而不是123.由於這時候還沒有開始執行任何java方法,而把value賦值爲123的putstatic指令是程序被編譯後,存放於類構造器()方法之中,因此把value賦值爲123的動做將在初始化階段纔會執行。
至於「特殊狀況」是指:public static final int value=123,即當類字段的字段屬性是ConstantValue時,會在準備階段初始化爲指定的值,因此標註爲final以後,value的值在準備階段初始化爲123而非0.
4. 解析
解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。
5. 初始化
若是一個類被主動引用,就會觸發類的初始化。
在java中,直接引用的狀況有,經過new關鍵字實例化對象、讀取或設置類的靜態變量、調用類的靜態方法。經過反射方式執行以上三種行爲。初始化子類的時候,會觸發父類的初始化。做爲程序入口直接運行時(也就是直接調用main方法)。除了以上四種狀況,其餘使用類的方式叫作被動引用,而被動引用不會觸發類的初始化
6. 使用
類的使用包括主動引用和被動引用
被動引用:引用父類的靜態字段,只會引發父類的初始化,而不會引發子類的初始化。定義類數組,不會引發類的初始化。引用類的常量,不會引發類的初始化。
7. 卸載
知足下面的狀況,類就會被卸載:該類全部的實例都已經被回收,也就是java堆中不存在該類的任何實例。加載該類的ClassLoader已經被回收。該類對應的java.lang.Class對象沒有任何地方被引用,沒法在任何地方經過反射訪問該類的方法。
若是以上三個條件所有知足,jvm就會在方法區垃圾回收的時候對類進行卸載,類的卸載過程其實就是在方法區中清空類信息,java類的整個生命週期就結束了。
總結
對象基本上都是在jvm的堆區中建立,在建立對象以前,會觸發類加載(加載、鏈接、初始化),當類初始化完成後,根據類信息在堆區中實例化類對象,初始化非靜態變量、非靜態代碼以及默認構造方法,當對象使用完以後會在合適的時候被jvm垃圾收集器回收。
對象的生命週期只是類的生命週期中使用階段的主動引用的一種狀況(即實例化類對象)。而類的整個生命週期則要比對象的生命週期長的多。
類的生命週期
jvm(java虛擬機)中的幾個比較重要的內存區域,這幾個區域在java類的生命週期中扮演着比較重要的角色:
方法區:在java的虛擬機中有一塊專門用來存放已經加載的類信息、常量、靜態變量以及方法代碼的內存區域,叫作方法區。
常量池:常量池是方法區的一部分,主要用來存放常量和類中的符號引用等信息。
堆區:用於存放類的對象實例。
棧區:也叫java虛擬機棧,是由一個一個的棧幀組成的後進先出的棧式結構,棧楨中存放方法運行時產生的局部變量、方法出口等信息。當調用一個方法時,虛擬機棧中就會建立一個棧幀存放這些數據,當方法調用完成時,棧幀消失,若是方法中調用了其餘方法,則繼續在棧頂建立新的棧楨。
微信圖片_20180704125805.png
類加載器
經過一個類的全限定名來獲取描述此類的二進制字節流,這個動做放到java虛擬機外部去實現。以便讓應用程序本身決定如何去獲取所須要的類。實現各動做的代碼模塊稱爲「類加載器」。
比較兩個類是否相等,只有這兩個類是由同一個類加載器加載的前提下才有意義,不然即便這兩個;誒是來源同一個class文件,但類加載器不一樣,他們也不相等。
啓動類加載器
這個類加載器負責放在<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath參數所指定的路徑中的,而且是虛擬機識別的類庫。用戶沒法直接使用。
擴展類加載器
這個類加載器由sun.misc.Launcher$AppClassLoader實現。它負責<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的全部類庫。用戶能夠直接使用。
應用程序類加載器
這個類由sun.misc.Launcher$AppClassLoader實現。是ClassLoader中getSystemClassLoader()方法的返回值。它負責用戶路徑(ClassPath)所指定的類庫。用戶能夠直接使用。若是用戶沒有本身定義類加載器,默認使用這個
自定義加載器
用戶本身定義的類加載器。
雙親委派模型
若是一個類加載器收到類加載的請求,它首先不會本身去嘗試加載這個類,而是把這個請求委派給父類加載器完成。每一個類加載器都是如此,只有當父加載器在本身的搜索範圍內找不到指定的類時(即ClassNotFoundException),子加載器纔會嘗試本身去加載。
優勢
Java類隨着它的類加載器一塊兒具有了一種帶有優先級的層次關係。例如類java.lang.Object,它存在在rt.jar中,不管哪個類加載器要加載這個類,最終都是委派給處於模型最頂端的Bootstrap ClassLoader進行加載,所以Object類在程序的各類類加載器環境中都是同一個類。
相反,若是沒有雙親委派模型而是由各個類加載器自行加載的話,若是用戶編寫了一個java.lang.Object的同名類並放在ClassPath中,那系統中將會出現多個不一樣的Object類,程序將混亂。所以,若是開發者嘗試編寫一個與rt.jar類庫中重名的Java類,能夠正常編譯,可是永遠沒法被加載運行。
7. happens-before原則
概述
咱們沒法就全部場景來規定某個線程修改的變量什麼時候對其餘線程可見,可是咱們能夠指定某些規則,這規則就是happens-before,從JDK 5 開始,JMM就使用happens-before的概念來闡述多線程之間的內存可見性。
在JMM中,若是一個操做執行的結果須要對另外一個操做可見,那麼這兩個操做之間必須存在happens-before關係。 happens-before原則很是重要,它是判斷數據是否存在競爭、線程是否安全的主要依據,依靠這個原則,咱們解決在併發環境下兩操做之間是否可能存在衝突的全部問題。下面咱們就一個簡單的例子稍微瞭解下happens-before ;
i = 1; //線程A執行
j = i ; //線程B執行
複製代碼
j 是否等於1呢?假定線程A的操做(i = 1)happens-before線程B的操做(j = i),那麼能夠肯定線程B執行後j = 1 必定成立,若是他們不存在happens-before原則,那麼j = 1 不必定成立。這就是happens-before原則的威力。
原則定義
若是一個操做happens-before另外一個操做,那麼第一個操做的執行結果將對第二個操做可見,並且第一個操做的執行順序排在第二個操做以前。
兩個操做之間存在happens-before關係,並不意味着必定要按照happens-before原則制定的順序來執行。若是重排序以後的執行結果與按照happens-before關係來執行的結果一致,那麼這種重排序並不非法。
規則以下
程序次序規則
一個線程內,按照代碼順序,書寫在前面的操做先行發生於書寫在後面的操做;
鎖定規則
一個unLock操做先行發生於後面對同一個鎖額lock操做;
volatile變量規則
對一個變量的寫操做先行發生於後面對這個變量的讀操做;
傳遞規則
若是操做A先行發生於操做B,而操做B又先行發生於操做C,則能夠得出操做A先行發生於操做C;
線程啓動規則
Thread對象的start()方法先行發生於此線程的每一個一個動做;
程中斷規則
對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生;
線程終結規則
線程中全部的操做都先行發生於線程的終止檢測,咱們能夠經過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行;
對象終結規則
一個對象的初始化完成先行發生於他的finalize()方法的開始;
8. 對象
Java中建立對象的5種方式
使用new關鍵字 → 調用了構造函數
Employee emp1 = new Employee();
複製代碼
使用Class類的newInstance方法→ 調用了構造函數
<!--使用Class類的newInstance方法建立對象。這個newInstance方法調用無參的構造函數建立對象。-->
Employee emp2 = (Employee) Class.forName("org.programming.mitra.exercises.Employee" ).newInstance();
複製代碼
使用Constructor類的newInstance方法 → 調用了構造函數
<!--和Class類的newInstance方法很像, java.lang.reflect.Constructor類裏也有一個newInstance方法能夠建立對象-->
Constructor<Employee> constructor = Employee.class.getConstructor();
Employee emp3 = constructor.newInstance();
複製代碼
使用clone方法→ 沒有調用構造函數
<!--不管什麼時候咱們調用一個對象的clone 方法,jvm就會建立一個新的對象,將前面對象的內容所有拷貝進去。用clone 方法建立對象並不會調用任何構造函數。-->
<!--要使用clone 方法,咱們須要先實現Cloneable接口並實現其定義的clone 方法-->
Employee emp4 = (Employee) emp3.clone();
複製代碼
使用反序列化→ 沒有調用構造函數
ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.obj" ));
Employee emp5 = (Employee) in.readObject();
複製代碼
Java 對象生命週期
對象的整個生命週期大體能夠分爲7個階段:
建立階段(Creation)
在建立階段系統經過下面的幾個步驟來完成對象的建立過程
1,爲對象分配存儲空間
2,開始構造對象
3,從超類到子類對static成員進行初始化
4,超類成員變量按順序初始化,遞歸調用超類的構造方法
5,子類成員變量按順序初始化,子類構造方法調用
一旦對象被建立,並被分派給某些變量賦值,這個對象的狀態就切換到了應用階段
複製代碼
應用階段(In Use)
對象至少被一個強引用持有着
複製代碼
不可視階段(Invisible)
當一個對象處於不可見階段時,說明程序自己再也不持有該對象的任何強引用,雖然該這些引用仍然是存在着的。
簡單說就是程序的執行已經超出了該對象的做用域了。
複製代碼
不可到達階段(Unreachable)
對象處於不可達階段是指該對象再也不被任何強引用所持有
與「不可見階段」相比,「不可見階段」是指程序再也不持有該對象的任何強引用,這種狀況下,該對象仍可能被JVM等系統下的某些已裝載的靜態變量或線程或JNI等強引用持有着,這些特殊的強引用被稱爲」GC root」。存在着這些GC root會致使對象的內存泄露狀況,沒法被回收。
複製代碼
可收集階段(Collected)
當垃圾回收器發現該對象已經處於「不可達階段」而且垃圾回收器已經對該對象的內存空間從新分配作好準備時,則對象進入了「收集階段」。
若是該對象已經重寫了finalize()方法,則會去執行該方法的終端操做。
複製代碼
終結階段(Finalized)
當對象執行完finalize()方法後仍然處於不可達狀態時,則該對象進入終結階段。在該階段是等待垃圾回收器對該對象空間進行回收。
複製代碼
對象空間從新分配階段
垃圾回收器對該對象的所佔用的內存空間進行回收或者再分配了,則該對象完全消失了,稱之爲「對象空間從新分配階段」。
複製代碼
對象內存分配
類加載檢查經過後,虛擬機將爲新生對象分配內存,對象所需內存大小在類加載完成後能夠徹底肯定,對象內存分配任務就是把一塊肯定大小的內存從堆中劃分出來。
指針碰撞法
若是堆中內存是絕對規整的。用過的內存放一邊,空閒的放一邊,中間放着一個指針做爲分界點的指示器,那所分配內存就是把指針向空閒一邊移動一段與對象大小相等的距離,即爲「指針碰撞」
空閒列表法
若是堆中內存不規整,已使用內存和未使用內存相互交錯,虛擬機就必須一個列表,記錄哪些內存塊可用,在分配時從列表中找到一塊足夠大空間劃分給對象,並更新列表上記錄,即爲「空閒列表」
總結
選擇何種分配方式,由堆是否規整決定,而堆是否規整由採用的垃圾收集器是否有壓縮整理功能決定。
使用Serial,ParNew等帶Compactg過程的收集器時,系統採用指針碰撞法
使用CMS這種基於Mark-Sweep算法的收集器時,系統採用空閒列表法
對象的訪問定位
Java程序須要經過棧上的references數據來操做堆上的具體對象。由於referencesz只是指向對象的一個引用,並無定義這個引用經過何種方式去方位堆中對象的具體位置。因此對象訪問方式取決於虛擬機實現而定的。
目前主流的訪問方式有使用句柄和直接指針兩種。
句柄定位
使用句柄訪問時,Java堆中會劃分出一塊內存來做爲句柄池,references中存儲的就是對象的句柄地址。句柄中包含對象實列數據與類型數據各組的具體地址信息 references->句柄池->java堆
屏幕截圖.png
直接指針定位
若是是直接指針訪問,Java堆的佈局就必須考慮如何放置訪問類型數據相關。
屏幕截圖.png
各自優勢
句柄訪問最大好處就是references中存儲的是穩定的句柄地址,在對象移動(垃圾收集時移動對象是廣泛行爲)時只會改變句柄中的實列數據指針,references自己不須要修改。
直接指針訪問的最大好處是速度快,節省了一次定位的實時間開銷。
9. 常量池總結
全局字符串池
string pool也有叫作string literal pool
全局字符串池裏的內容是在類加載完成,通過驗證,準備階段以後在堆中生成字符串對象實例,而後將該字符串對象實例的引用值存到string pool中(記住:string pool中存的是引用值而不是具體的實例對象,具體的實例對象是在堆中開闢的一塊空間存放的。)。
在HotSpot VM裏實現的string pool功能的是一個StringTable類,它是一個哈希表,裏面存的是駐留字符串(也就是咱們常說的用雙引號括起來的)的引用(而不是駐留字符串實例自己),也就是說在堆中的某些字符串實例被這個StringTable引用以後就等同被賦予了」駐留字符串」的身份。這個StringTable在每一個HotSpot VM的實例只有一份,被全部的類共享。
class文件常量池
class constant pool
咱們都知道,class文件中除了包含類的版本、字段、方法、接口等描述信息外,還有一項信息就是常量池(constant pool table),用於存放編譯器生成的各類字面量(Literal)和符號引用(Symbolic References)。
字面量就是咱們所說的常量概念,如文本字符串、被聲明爲final的常量值等。
符號引用是一組符號來描述所引用的目標,符號能夠是任何形式的字面量,只要使用時能無歧義地定位到目標便可(它與直接引用區分一下,直接引用通常是指向方法區的本地指針,相對偏移量或是一個能間接定位到目標的句柄)。通常包括下面三類常量。類和接口的全限定名,字段的名稱和描述符,方法的名稱和描述符。
常量池的每一項常量都是一個表,一共有以下表所示的11種各不相同的表結構數據,這每一個表開始的第一位都是一個字節的標誌位(取值1-12),表明當前這個常量屬於哪一種常量類型。
屏幕截圖.png
運行時常量池(runtime constant pool)
當java文件被編譯成class文件以後,也就是會生成我上面所說的class常量池,那麼運行時常量池又是何時產生的呢?
jvm在執行某個類的時候,必須通過加載、鏈接、初始化,而鏈接又包括驗證、準備、解析三個階段。而當類加載到內存中後,jvm就會將class常量池中的內容存放到運行時常量池中,由此可知,運行時常量池也是每一個類都有一個。在上面我也說了,class常量池中存的是字面量和符號引用,也就是說他們存的並非對象的實例,而是對象的符號引用值。而通過解析(resolve)以後,也就是把符號引用替換爲直接引用,解析的過程會去查詢全局字符串池,也就是咱們上面所說的StringTable,以保證運行時常量池所引用的字符串與全局字符串池中所引用的是一致的。
舉個實例來講明一下:
public class HelloWorld {
public static void main(String []args) {
String str1 = "abc" ;
String str2 = new String("def" );
String str3 = "abc" ;
String str4 = str2.intern();
String str5 = "def" ;
System.out.println(str1 == str3);//true
System.out.println(str2 == str4);//false
System.out.println(str4 == str5);//true
}
}
複製代碼
回到上面的那個程序,如今就很容易解釋整個程序的內存分配過程了,首先,在堆中會有一個」abc」實例,全局StringTable中存放着」abc」的一個引用值
而後在運行第二句的時候會生成兩個實例,一個是」def」的實例對象,而且StringTable中存儲一個」def」的引用值,還有一個是new出來的一個」def」的實例對象 與上面那個是不一樣的實例
當在解析str3的時候查找StringTable,裏面有」abc」的全局駐留字符串引用,因此str3的引用地址與以前的那個已存在的相同
str4是在運行的時候調用intern()函數,返回StringTable中」def」的引用值,若是沒有就將str2的引用值添加進去,在這裏,StringTable中已經有了」def」的引用值了,因此返回上面在new str2的時候添加到StringTable中的 「def」引用值
上面程序的首先通過編譯以後,在該類的class常量池中存放一些符號引用,而後類加載以後,將class常量池中存放的符號引用轉存到運行時常量池中,而後通過驗證,準備階段以後,在堆中生成駐留字符串的實例對象(也就是上例中str1所指向的」abc」實例對象),而後將這個對象的引用存到全局String Pool中,也就是StringTable中,最後在解析階段,要把運行時常量池中的符號引用替換成直接引用,那麼就直接查詢StringTable,保證StringTable裏的引用值與運行時常量池中的引用值一致,大概整個過程就是這樣了。
總結
1.全局常量池在每一個VM中只有一份,存放的是字符串常量的引用值。
2.class常量池是在編譯的時候每一個class都有的,在編譯階段,存放的是常量的符號引用。
3.運行時常量池是在類加載完成以後,將每一個class常量池中的符號引用值轉存到運行時常量池中,也就是說,每一個class都有一個運行時常量池,類在解析以後,將符號引用替換成直接引用,與全局常量池中的引用值保持一致。
class文件常量池和運行時常量池
最近一直被方法區裏面存着什麼東西困擾着?
1.方法區裏存class文件信息和class文件常量池是個什麼關係。
2.class文件常量池和運行時常量池是什麼關係。
複製代碼
方法區存着類的信息,常量和靜態變量,即類被編譯後的數據。這個說法實際上是沒問題的,只是太籠統了。更加詳細一點的說法是方法區裏存放着類的版本,字段,方法,接口和常量池。常量池裏存儲着字面量和符號引用。
符號引用包括:1.類的全限定名,2.字段名和屬性,3.方法名和屬性。
屏幕截圖.png
屏幕截圖.png
能夠看到在方法區裏的class文件信息包括:魔數,版本號,常量池,類,父類和接口數組,字段,方法等信息,其實類裏面又包括字段和方法的信息。
屏幕截圖.png
屏幕截圖.png
class文件常量池和運行時常量池的關係以及區別
class文件常量池存儲的是當class文件被java虛擬機加載進來後存放在方法區的一些字面量和符號引用,字面量包括字符串,基本類型的常量。
運行時常量池是當class文件被加載完成後,java虛擬機會將class文件常量池裏的內容轉移到運行時常量池裏,在class文件常量池的符號引用有一部分是會被轉變爲直接引用的,好比說類的靜態方法或私有方法,實例構造方法,父類方法,這是由於這些方法不能被重寫其餘版本,因此能在加載的時候就能夠將符號引用轉變爲直接引用,而其餘的一些方法是在這個方法被第一次調用的時候纔會將符號引用轉變爲直接引用的。
總結:
方法區裏存儲着class文件的信息和運行時常量池,class文件的信息包括類信息和class文件常量池。
運行時常量池裏的內容除了是class文件常量池裏的內容外,還將class文件常量池裏的符號引用轉變爲直接引用,並且運行時常量池裏的內容是能動態添加的。例如調用String的intern方法就能將string的值添加到String常量池中,這裏String常量池是包含在運行時常量池裏的,但在jdk1.8後,將String常量池放到了堆中。
10. 類文件結構
1.class類文件結構
class 文件結構是一組以8位字節爲基礎單位的二進制流。存儲的內容幾乎所有是程序運行的必要數據,無空隙。
若是須要佔用8位字節以上空間的數據,則按照高位在前的方式分割成若干個8位字節進行存儲。
class文件結構採用一種相似C語言體系的僞結構體系,這種僞結構只有無符號數和表兩種數據類型。
魔數與Class文件的版本
class文件的頭4個字節稱爲魔數,惟一做用是肯定這個文件是否爲一個能被虛擬機接受的文件。
魔數值能夠自由選擇,只要未被普遍使用同事不會引發混淆。
緊接着魔數的4個字節是class文件版本號,第5和第6個字節是次版本你好,7和8個字節是class文件版本號(java版本號從45開始。jdk7是51.0)
常量池
主次版本號以後的是常量池,常量池能夠理解爲class文件中的資源倉庫。
class文件結構中只有常量的容量技術是從1開始
常量池主要存放兩大類常量:字面量(如文本字符串,finald常量)和符號引用(類和接口的全限定名,字段的名稱和描述符,方法的名稱和描述符)。
虛擬機運行時,需從常量池獲取對應的符號引用,再在類建立時或運行將誒系會,翻譯到哪具體的內存地址中。
訪問標誌
常量池以後的兩個字節表明訪問標誌,用於識別class是類仍是接口,是否爲public類型或abstract類型等等。
類索引,父類縮影與接口索引集合
這三項按順序排列在訪問標誌以後,class文件中由這三項來肯定整個類的繼承關係。
類索引用於肯定類的全限定名,父類索引用於肯定類的父類權限定名。接口索引集合描述類實現了哪些接口
字段表集合
用於描述接口或類中聲明的變量。字段包裹類級別的變量和實列變量。不包括方法內部聲明的局部變量。
方法表集合
方法表結構依次包括訪問標誌,名稱索引,描述索引,屬性集合.
2.字節碼指令簡介