java虛擬機主要分爲如下幾個區:java
方法區:
1. 有時候也成爲永久代,在該區內不多發生垃圾回收,可是並不表明不發生GC,在這裏進行的GC主要是對方法區裏的常量池和對類型的卸載
2. 方法區主要用來存儲已被虛擬機加載的類的信息、常量、靜態變量和即時編譯器編譯後的代碼等數據。
3. 該區域是被線程共享的。
4. 方法區裏有一個運行時常量池,用於存放靜態編譯產生的字面量和符號引用。該常量池具備動態性,也就是說常量並不必定是編譯時肯定,運行時生成的常量也會存在這個常量池中。程序員虛擬機棧:
1. 虛擬機棧也就是咱們日常所稱的棧內存,它爲java方法服務,每一個方法在執行的時候都會建立一個棧幀,用於存儲局部變量表、操做數棧、動態連接和方法出口等信息。
2. 虛擬機棧是線程私有的,它的生命週期與線程相同。
3. 局部變量表裏存儲的是基本數據類型、returnAddress類型(指向一條字節碼指令的地址)和對象引用,這個對象引用有多是指向對象起始地址的一個指針,也有多是表明對象的句柄或者與對象相關聯的位置。局部變量所需的內存空間在編譯器間肯定
4.操做數棧的做用主要用來存儲運算結果以及運算的操做數,它不一樣於局部變量表經過索引來訪問,而是壓棧和出棧的方式
5.每一個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程當中的動態鏈接.動態連接就是將常量池中的符號引用在運行期轉化爲直接引用。算法本地方法棧
本地方法棧和虛擬機棧相似,只不過本地方法棧爲Native方法服務。數組堆
java堆是全部線程所共享的一塊內存,在虛擬機啓動時建立,幾乎全部的對象實例都在這裏建立,所以該區域常常發生垃圾回收操做。緩存程序計數器
內存空間小,字節碼解釋器工做時經過改變這個計數值能夠選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理和線程恢復等功能都須要依賴這個計數器完成。該內存區域是惟一一個java虛擬機規範沒有規定任何OOM狀況的區域。數據結構
判斷一個對象是否存活有兩種方法:
1. 引用計數法
所謂引用計數法就是給每個對象設置一個引用計數器,每當有一個地方引用這個對象時,就將計數器加一,引用失效時,計數器就減一。當一個對象的引用計數器爲零時,說明此對象沒有被引用,也就是「死對象」,將會被垃圾回收.
引用計數法有一個缺陷就是沒法解決循環引用問題,也就是說當對象A引用對象B,對象B又引用者對象A,那麼此時A,B對象的引用計數器都不爲零,也就形成沒法完成垃圾回收,因此主流的虛擬機都沒有采用這種算法。多線程2.可達性算法(引用鏈法)
該算法的思想是:從一個被稱爲GC Roots的對象開始向下搜索,若是一個對象到GC Roots沒有任何引用鏈相連時,則說明此對象不可用。
在java中能夠做爲GC Roots的對象有如下幾種:jvm
- 虛擬機棧中引用的對象
- 方法區類靜態屬性引用的對象
- 方法區常量池引用的對象
- 本地方法棧JNI引用的對象
雖然這些算法能夠斷定一個對象是否能被回收,可是當知足上述條件時,一個對象比不必定會被回收。當一個對象不可達GC Root時,這個對象並 不會立馬被回收,而是出於一個死緩的階段,若要被真正的回收須要經歷兩次標記。
若是對象在可達性分析中沒有與GC Root的引用鏈,那麼此時就會被第一次標記而且進行一次篩選,篩選的條件是是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法或者已被虛擬機調用過,那麼就認爲是不必的。
若是該對象有必要執行finalize()方法,那麼這個對象將會放在一個稱爲F-Queue的對隊列中,虛擬機會觸發一個Finalize()線程去執行,此線程是低優先級的,而且虛擬機不會承諾一直等待它運行完,這是由於若是finalize()執行緩慢或者發生了死鎖,那麼就會形成F-Queue隊列一直等待,形成了內存回收系統的崩潰。GC對處於F-Queue中的對象進行第二次被標記,這時,該對象將被移除」即將回收」集合,等待回收。函數
在系統運行過程當中,會產生一些無用的對象,這些對象佔據着必定的內存,若是不對這些對象清理回收無用對象的內存,可能會致使內存的耗盡,因此垃圾回收機制回收的是內存。同時GC回收的是堆區和方法區的內存。學習
在java中,程序員是不須要顯示的去釋放一個對象的內存的,而是由虛擬機自行執行。在JVM中,有一個垃圾回收線程,它是低優先級的,在正常狀況下是不會執行的,只有在虛擬機空閒或者當前堆內存不足時,纔會觸發執行,掃面那些沒有被任何引用的對象,並將它們添加到要回收的集合中,進行回收。
一、標記-清除:
這是垃圾收集算法中最基礎的,根據名字就能夠知道,它的思想就是標記哪些要被回收的對象,而後統一回收。這種方法很簡單,可是會有兩個主要問題:1.效率不高,標記和清除的效率都很低;2.會產生大量不連續的內存碎片,致使之後程序在分配較大的對象時,因爲沒有充足的連續內存而提早觸發一次GC動做。二、複製算法:
爲了解決效率問題,複製算法將可用內存按容量劃分爲相等的兩部分,而後每次只使用其中的一塊,當一塊內存用完時,就將還存活的對象複製到第二塊內存上,而後一次性清楚完第一塊內存,再將第二塊上的對象複製到第一塊。可是這種方式,內存的代價過高,每次基本上都要浪費通常的內存。
因而將該算法進行了改進,內存區域再也不是按照1:1去劃分,而是將內存劃分爲8:1:1三部分,較大那分內存交Eden區,其他是兩塊較小的內存區叫Survior區。每次都會優先使用Eden區,若Eden區滿,就將對象複製到第二塊內存區上,而後清除Eden區,若是此時存活的對象太多,以致於Survivor不夠時,會將這些對象經過分配擔保機制複製到老年代中。(java堆又分爲新生代和老年代)三、標記-整理
該算法主要是爲了解決標記-清除,產生大量內存碎片的問題;當對象存活率較高時,也解決了複製算法的效率問題。它的不一樣之處就是在清除對象的時候現將可回收對象移動到一端,而後清除掉端邊界之外的對象,這樣就不會產生內存碎片了。四、分代收集
如今的虛擬機垃圾收集大多采用這種方式,它根據對象的生存週期,將堆分爲新生代和老年代。在新生代中,因爲對象生存期短,每次回收都會有大量對象死去,那麼這時就採用複製算法。老年代裏的對象存活率較高,沒有額外的空間進行分配擔保,因此可使用標記-整理 或者 標記-清除。
java內存模型(JMM)是線程間通訊的控制機制.JMM定義了主內存和線程之間抽象關係。線程之間的共享變量存儲在主內存(main memory)中,每一個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存,寫緩衝區,寄存器以及其餘的硬件和編譯器優化。Java內存模型的抽象示意圖以下:
從上圖來看,線程A與線程B之間如要通訊的話,必需要經歷下面2個步驟:
1. 首先,線程A把本地內存A中更新過的共享變量刷新到主內存中去。
2. 而後,線程B到主內存中去讀取線程A以前已更新過的共享變量。具體參考博客:http://blog.csdn.net/suifeng3051/article/details/52611310
類加載過程
類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括:加載、驗證、準備、解析、初始化、使用和卸載七個階段。它們開始的順序以下圖所示:
其中類加載的過程包括了加載、驗證、準備、解析、初始化五個階段。在這五個階段中,加載、驗證、準備和初始化這四個階段發生的順序是肯定的,而解析階段則不必定,它在某些狀況下能夠在初始化階段以後開始,這是爲了支持 Java 語言的運行時綁定(也成爲動態綁定或晚期綁定)。另外注意這裏的幾個階段是按順序開始,而不是按順序進行或完成,由於這些階段一般都是互相交叉地混合進行的,一般在一個階段執行的過程當中調用或激活另外一個階段。
簡要說明下 Java 中的綁定:綁定指的是把一個方法的調用與方法所在的類(方法主體)關聯起來,對 Java 來講,綁定分爲靜態綁定和動態綁定:
- 靜態綁定:即前期綁定。在程序執行前方法已經被綁定,此時由編譯器或其它鏈接程序實現。針對 Java,簡單的能夠理解爲程序編譯期的綁定。Java 當中的方法只有 final,static,private 和構造方法是前期綁定的。
- 動態綁定:即晚期綁定,也叫運行時綁定。在運行時根據具體對象的類型進行綁定。在 Java 中,幾乎全部的方法都是後期綁定的。
講述類加載過程當中每一個階段所作的工做:
加載:
加載時類加載過程的第一個階段,在加載階段,虛擬機須要完成如下三件事情:
- 經過一個類的全限定名來獲取其定義的二進制字節流。
- 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。
- 在 Java 堆中生成一個表明這個類的 java.lang.Class 對象,做爲對方法區中這些數據的訪問入口。
驗證:
驗證的目的是爲了確保Class文件的字節流中的信息不回危害到虛擬機.在該階段主要完成如下四鍾驗證:1. 文件格式驗證:驗證字節流是否符合Class文件的規範,如主次版本號是否在當前虛擬機範圍內,常量池中的常量是否有不被支持的類型.
2. 元數據驗證:對字節碼描述的信息進行語義分析,如這個類是否有父類,是否集成了不被繼承的類等。
3. 字節碼驗證:是整個驗證過程當中最複雜的一個階段,經過驗證數據流和控制流的分析,肯定程序語義是否正確,主要針對方法體的驗證。如:方法中的類型轉換是否正確,跳轉指令是否正確等。
4. 符號引用驗證:這個動做在後面的解析過程當中發生,主要是爲了確保解析動做能正確執行。準備
準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中分配。對於該階段有如下幾點須要注意:
- 這時候進行內存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨着對象一塊分配在 Java 堆中。
- 這裏所設置的初始值一般狀況下是數據類型默認的零值(如 0、0L、null、false 等),而不是被在 Java 代碼中被顯式地賦予的值。
假設一個類變量的定義爲:
public static int value = 3;
那麼變量 value 在準備階段事後的初始值爲 0,而不是 3,由於這時候還沒有開始執行任何 Java 方法,而把 value 賦值爲 3 的 putstatic 指令是在程序編譯後,存放於類構造器 ()方法之中的,因此把 value 賦值爲 3 的動做將在初始化階段纔會執行。
解析階段是虛擬機將常量池中的符號引用轉化爲直接引用的過程。在 Class 類文件結構一文中已經比較過了符號引用和直接引用的區別和關聯,這裏再也不贅述。前面說解析階段可能開始於初始化以前,也可能在初始化以後開始,虛擬機會根據須要來判斷,究竟是在類被加載器加載時就對常量池中的符號引用進行解析(初始化以前),仍是等到一個符號引用將要被使用前纔去解析它(初始化以後)。
對同一個符號引用進行屢次解析請求時很常見的事情,虛擬機實現可能會對第一次解析的結果進行緩存(在運行時常量池中記錄直接引用,並把常量標示爲已解析狀態),從而避免解析動做重複進行。
解析動做主要針對類或接口、字段、類方法、接口方法四類符號引用進行,分別對應於常量池中的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info 四種常量類型。
一、類或接口的解析:判斷所要轉化成的直接引用是對數組類型,仍是普通的對象類型的引用,從而進行不一樣的解析。
二、字段解析:對字段進行解析時,會先在本類中查找是否包含有簡單名稱和字段描述符都與目標相匹配的字段,若是有,則查找結束;若是沒有,則會按照繼承關係從上往下遞歸搜索該類所實現的各個接口和它們的父接口,尚未,則按照繼承關係從上往下遞歸搜索其父類,直至查找結束,查找流程以下圖所示:
·
三、類方法解析:對類方法的解析與對字段解析的搜索步驟差很少,只是多了判斷該方法所處的是類仍是接口的步驟,並且對類方法的匹配搜索,是先搜索父類,再搜索接口。
四、接口方法解析:與類方法解析步驟相似,知識接口不會有父類,所以,只遞歸向上搜索父接口就好了。
初始化
初始化是類加載過程的最後一步,到了此階段,才真正開始執行類中定義的 Java 程序代碼。在準備階段,類變量已經被賦過一次系統要求的初始值,而在初始化階段,則是根據程序員經過程序指定的主觀計劃去初始化類變量和其餘資源,或者能夠從另外一個角度來表達:初始化階段是執行類構造器()方法的過程。
這裏簡單說明下()方法的執行規則:
一、()方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊以前的變量,定義在它以後的變量,在前面的靜態語句中能夠賦值,可是不能訪問。
二、()方法與實例構造器()方法(類的構造函數)不一樣,它不須要顯式地調用父類構造器,虛擬機會保證在子類的()方法執行以前,父類的()方法已經執行完畢。所以,在虛擬機中第一個被執行的()方法的類確定是java.lang.Object。
三、()方法對於類或接口來講並非必須的,若是一個類中沒有靜態語句塊,也沒有對類變量的賦值操做,那麼編譯器能夠不爲這個類生成()方法。
四、接口中不能使用靜態語句塊,但仍然有類變量(final static)初始化的賦值操做,所以接口與類同樣會生成()方法。可是接口魚類不一樣的是:執行接口的()方法不須要先執行父接口的()方法,只有當父接口中定義的變量被使用時,父接口才會被初始化。另外,接口的實現類在初始化時也同樣不會執行接口的()方法。
五、虛擬機會保證一個類的()方法在多線程環境中被正確地加鎖和同步,若是多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的()方法,其餘線程都須要阻塞等待,直到活動線程執行()方法完畢。若是在一個類的()方法中有耗時很長的操做,那就可能形成多個線程阻塞,在實際應用中這種阻塞每每是很隱蔽的。
總結
整個類加載過程當中,除了在加載階段用戶應用程序能夠自定義類加載器參與以外,其他全部的動做徹底由虛擬機主導和控制。到了初始化纔開始執行類中定義的 Java 程序代碼(亦及字節碼),但這裏的執行代碼只是個開端,它僅限於()方法。類加載過程當中主要是將 Class 文件(準確地講,應該是類的二進制字節流)加載到虛擬機內存中,真正執行字節碼的操做,在加載完成後才真正開始。
實現經過類的權限定名獲取該類的二進制字節流的代碼塊叫作類加載器。
主要有一下四種類加載器:
1. 啓動類加載器(Bootstrap ClassLoader)用來加載java核心類庫,沒法被java程序直接引用。
2. 擴展類加載器(extensions class loader):它用來加載 Java 的擴展庫。Java 虛擬機的實現會提供一個擴展庫目錄。該類加載器在此目錄裏面查找並加載 Java 類。
3. 系統類加載器(system class loader):它根據 Java 應用的類路徑(CLASSPATH)來加載 Java 類。通常來講,Java 應用的類都是由它來完成加載的。能夠經過 ClassLoader.getSystemClassLoader()來獲取它。
4. 用戶自定義類加載器,經過繼承 java.lang.ClassLoader類的方式實現。
對象優先在堆的Eden區分配。
大對象直接進入老年代.
長期存活的對象將直接進入老年代.當Eden區沒有足夠的空間進行分配時,虛擬機會執行一次Minor GC.Minor Gc一般發生在新生代的Eden區,在這個區的對象生存期短,每每發生Gc的頻率較高,回收速度比較快;Full Gc/Major GC 發生在老年代,通常狀況下,觸發老年代GC的時候不會觸發Minor GC,可是經過配置,能夠在Full GC以前進行一次Minor GC這樣能夠加快老年代的回收速度。
關於Java虛擬機學習,推薦《深刻理解Java虛擬機》 第二版 周志明著
以及博客文章http://wiki.jikexueyuan.com/project/java-vm/