第一部分:概述java
1 走進Java算法
1.1 概述 編程
第二部分:自動內存管理機制緩存
2.1 運行時數據區域數據結構
3.1 對象已死嗎編程語言
第三部分: 虛擬機執行子系統
8 虛擬機字節碼執行引擎
Java優勢:它擺脫了硬件平臺的東縛,實現了「一次編寫,處處運行」的理想;它提供了一個相對安全的內存管理和訪問機制,避免了絕大部分的內存泄露和指針越界問題;它實現了熱點代碼檢測和運行時編譯及優化,這使得Java應用能隨着運行時間的增長而得到更高的性能;它有一套完善的應用程序接口,還有無數來自商業機構和開源社區的第三方類庫來幫助它實現各類各樣的功能等等。
sun官方定義:
1 Java程序設計語言
2 各類硬件平臺上的Java虛擬機
3 Class文件格式
4 Java API類庫
5 來自商業機構和開源社區的第三方Java類庫
把Java程序設計語言、Java虛擬機、 Java API類庫這三部分統稱爲JDK(Java Development Kit),JDK是用於支持Java程序開發的最小環境。把 Java API類庫中的Java SE API子集和Java虛擬機這兩部分統稱爲JRE( Java Runtime Environment),JRE是支持Jav程序運行的標準環境。
(1)程序計數器
程序計數器( Program Counter Register)是一塊較小的內存空間,它能夠看做是當前線程所執行的字節碼的行號指示器。字節碼解釋器工做時就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成。
特色:因爲Java虛擬機的多線程是經過線程輪流切換並分配處理器執行時間的方式來實現的在任何一個肯定的時刻,一個處理器(對於多核處理器來講是一個內核)都只會執行一條線程中的指令。所以,爲了線程切換後能恢復到正確的執行位置,每條線程都須要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲,咱們稱這類內存區域爲「線程私有」的內存。若是線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;若是正在執行的是 Native方法,這個計數器值則爲空( Undefined)。
異常:此內存區域是惟一一個在Java虛擬機規範中沒有規定任何 Outofmemory Error狀況的區域
(2)Java虛擬機棧
虛擬機棧描述的Java方法執行的內存模型:每一個方法在執行的同時都會建立一個棧( Stack Frame)用存鋪局部變量表、操做數、動態鏈、方法口等信息。每個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機中入棧到出棧的過程
棧幀(Stack Frame):每一次函數的調用,都會在調用棧(call stack)上維護一個獨立的棧幀(stack frame).每一個獨立的棧幀通常包括:
局部變量表存放了細期可知的各類基本數據類型、對象引用( reference類型,它不等同於對象自己,多是一個指向對象起始地址的引用指針,也多是指向一個表明對象的句柄或其餘與此對象相關的位置)和returnAddress(指向了一條字節碼指令的地址)
特色:線程私有,生命週期與線程相同。
異常:StackOverflowError、OutOfMemoryError。
(3)本地方法棧
本地方法棧與虛擬機棧所發揮的做用相似,虛擬機棧爲虛擬機執行Java方法(字節碼)服務,本地方法棧爲虛擬機使用到的Native方法服務。有的虛擬機(Sun HotSpot)將兩者合二爲一。
異常:StackOverflowError、OutOfMemoryError
(4)Java堆
Java堆是被全部線程共享的一塊內存區域,在虛擬機啓動時建立。此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例都在這裏分配內存。Java堆是垃圾收集器管理的主要區域,所以不少時候也被稱作「GC堆」( GarbageCollected Heap)
異常:OutOfMemoryError
(5)方法區
方法區是各個線程共享的內存區域,存儲已經被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。在Java規範中方法區爲堆的一個邏輯部分但有一個別名Non-Heap(非堆)。
特色:垃圾收集行爲在這個區域較少出現,回收目標主要是針對常量池的回收和對類型的卸載。
(6)運行時常量池
運行時常量池是方法區的一部分,用於存放編譯期生成的各類字面量和符號引用,這部份內容當類加載完成後進入常量池存放。
特色:具有動態性,在運行期間也可將新的常量放入常量池中如String類的intern()方法
異常:OutOfMemoryError
(7)直接內存
直接內存並非虛擬機運行時數據區的一部分,也不是虛擬機規範中定義的內存區域。
(1)對象建立
從虛擬機角度而言便結束了,從Java程序語言而言纔剛開始,執行初始化方法,把對象按照代碼的設置初始化。
(2)對象的內存佈局
包含三部分:對象頭、實例數據、對齊填充。
包含兩部分信息:存儲對象自身的運行時數據;類型指針,用來肯定是哪一個對象的實例。可是第二部分類型指針不必定全部的虛擬機上都有。當對象是數組時對象頭還須要一塊用於記錄數組長度的數據。
實例數據是對象真正存儲的有效信息,也是程序代碼中所定義的各類類型的字段內容包含:父類繼承的,子類特有的。
並非必然存在的,由於HOtSpot VM的自動內存管理系統要求對象的起始地址必須是8字節的整數倍(1~2倍)。
(3)對象的訪問定位
主要關注:哪些內存須要回收?何時回收?如何回收?
(1)判斷對象是否已死:
(1)引用計數法。添加引用計數器,但很難解決對象間相互循環引用問題,實際中基本不用。
(2)可達性分析算法。以GC Roots對象爲根進行搜索。
GC Roots對象包含:虛擬機棧(棧幀中的本地變量表)中引用的對象;方法區中類靜態屬性引用的對象;方法區中常量引用的對象;本地方法棧中JNI(即Native方法)引用的對象。
(2)再談引用
在JDK1.2前引用的定義:若是 reference類型的數據中存儲的數值表明的是另一塊內存的起始地址,就稱這塊內存表明着一個引用。這種定義太過狹隘,一個對象在這種定義下只有被引用或者沒有被引用兩種狀態。咱們但願能描述這樣一類對象:當內存空間還足夠時,則能保留在內存之中,若是內存空間在進行垃圾收集後仍是很是緊張,則能夠拋棄這些對象。不少系統的緩存功能都符合這樣的應用場景。
後來對引用概念作了擴充:將引用分爲強引用( Strong Reference)、軟引用( Soft Reference)、弱引用( Weak Reference)、虛引用( Phantom Reference)4種,這4種引用強度依次逐漸減弱。
(3)生存仍是死亡
可達性分析算法中不可達的對象,處於緩刑階段,要真正宣告一個對象死亡,至少要經歷兩次標記過程。若是對象在進行可達性分析後發現沒有與 GC Roots相鏈接的引用鏈,那它將會被第一次標記而且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。若是這個對象被斷定爲有必要執行 finalizer()方法,那麼這個對象將會放置在一個叫作F-queue的隊列之中,虛擬機會觸發這個方法,但並不承諾會等待它運行結束,稍後GC將對 F-queue中的對象進行第二次小規模的標記,若是對象要在finalize()中成功拯救本身一一隻要從新與引用鏈上的任何一個對象創建關聯便可,如把本身(this關鍵字)賦值給某個類變量或者對象的成員變量,那在第二次標記時它將被移除出「即將回收」的集合;若是對象這時候尚未逃脫,那基本上它就真的被回收了。
(4)回收方法區
方法區(永久代)的垃圾收集主要回收兩部份內容:廢棄常量和無用的類。回收廢棄常量與回收Java堆中的對象很是相似。要判讀無用的類則須要知足下面三個條件:
虛擬機能夠對知足上述3個條件的無用類進行回收,但僅僅是能夠而不是必定。
(1)標記-清除算法(Mark-Sweep)
該算法分爲兩個階段:首先標記出全部須要回收的對象,在標記完成後統一回收全部被標記的對象,它的標記過程在對象標記斷定時已經介紹過了。後續的收集算法都是基於這種思路並對其不足進行改進而獲得的。
它的主要不足有兩個:一個是效率問題,標記和清除兩個過程的效率都不高:另外一個是空間問題,標記清除以後會產生大量不連續的內存碎片,空間碎片太多可能會致使之後在程序運行過程當中須要分配較大對象時,沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集動做。
(2)複製算法
複製算法是爲了解決效率問題。把內存分爲大小相等的兩個塊,每次只使用其中一塊,當一塊用完了把其中還存活的對象複製到另外一塊中。這樣也不用考慮內存分配時內存碎片問題了。
不足:只是這種算法的代價是將內存縮小爲了原來的一半。
商用中採用將內存分爲一塊較大的Eden和兩塊較小的Survivor,每次使用Eden和一塊Survivor,回收時將活着的對象複製到另外一塊Survivor中。HotSpot默認Eden和Survivor大小爲8:1。但沒有辦法保證每次回收都只有很少於10%的對象存活,當 Survivor空間不夠用時須要依賴其餘內存(這裏指老年代)進行分配擔保( Handle Promotion)。即若是另一塊 Survivor空間沒有足夠空間存放上一次新生代收集下來的存活對象時,這些對象將直接經過分配擔保機制進入老年代。
(3)標記-整理算法(Mark-Compact)
複製收集算法在對象存活率較高時就要進行較多的複製操做,效率將會變低,此外老年代對象通常是100%存活的所以不能直接選用這種算法。根據老年代的特色標記-一整理(Mark- Compact)算法,標記過程仍然與標記一清除算法同樣,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存。
(4)分代收集算法
目前普遍使用的是分代收集算法,根據對象存活週期的不一樣將內存劃分爲幾塊。通常是把Java堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少許存活,那就選用複製算法。而老年代中由於對象存活率高、沒有額外空間對它進行分配擔保,就必須使用「標記一清理」或者「標記一整理」算法來進行回收。
此部分爲看,主要是分析代碼的
收集算法是內存回收的方法論,垃圾收集器則是內存回收的具體實現。下圖中連線表示能夠相互配合:
(1)Serial收集器
單線程;在工做時必須暫停其餘全部的工做線程(Stop The World);簡單高效。
(2)ParNew收集器
Serial的多線程版本。
注:並行( Parallel):指多條垃圾收集線程並行工做,但此時用戶線程仍然處於等待狀態。
併發( Concurrent):指用戶線程與垃圾收集線程同時執行(但不必定是並行的,可能會交替執行),用戶程序在繼續運行,而垃圾收集程序運行於另ー個CPU上
(3)Parallel Scavenge收集器
其餘收集器的關注點是儘量地縮短垃圾收集時用戶線程的停頓時間,而 Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量( Throughput)。
吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間),虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。停頓時間越短就越適合須要與用戶交互的程序,良好的響應速度能提高用戶體驗,而高吞吐量則能夠高效率地利用CPU時間,儘快完成程序的運算任務,主要適合在後臺運算而不須要太多交互的任務。
Parallel Scavenge收集器提供了兩個參數用於精確控制吞吐量,分別是控制最大垃圾收集停頓時間的參數以及直接設置吞吐量大小的參數
(4)Serial Old收集器
單線程;給Client模式下的虛擬機使用;在Server模式下可做爲CMS收集器的後備預案
(5)Parallel Old收集器
多線程。在注重香吐量以及CPU資源感的場合,均可以優先考慮 Parallel Scavenge加 ParallelOd收集器
(6)CMS收集器
(7)G1收集器
Java內存自動化管理最終目的是自動化解決:給對象分配內存以及回收分配給對象的內存。
分配策略:
注: Minor GC和 Full GC區別
新生代GC( Minor GC):指發生在新生代的拉圾收集動做,由於Java對象大多都具有朝生夕滅的特性,因此 Minor GC很是頻繁,通常回收速度也比較快
老年代GC( Major GC/ Full GC):指發生在老年代的GC,出現了 Major GC,常常會伴隨至少ー次的 Minor GC(但非絕對的,在 Parallel Scavenge收集器的收集策略裏就有直接進行 Major GC的策略選擇過程)。 Major GC的速度通常會比 Minor GC慢10倍以上。
代碼編譯的結果從本地機器碼轉變爲字節碼,是存儲格式發展的一小步,倒是編程語言發展的一大步。
實現語言無關性的基礎是虛擬機和字節碼存儲格式。Java虛擬機不和包括Java在內的任何語言綁定,它只與Clas文件這種特定的二進制文件格式所關聯,Class文件中包含了Java虛擬機指令集和符號表以及若干其餘輔助信息。Java虛擬機能夠運行其餘語言編譯的字節碼文件。
Class文件是一組以8位字節爲基礎單位的二進制流,中間無分隔符。Class文件格式以下圖所示:
(1)魔數與Class文件的版本
每一個 Class文件的頭4個字節稱爲魔數( Magic Number),它的惟一做用是肯定這個文是否爲一個能被虛擬機接受的Css文件。緊接着魔數的4個字節存儲的是CS文件的版本號:第5和第6個字節是次版本號( Minor Version),第7和第8個字節是主版本號( Major Version)。
(2)常量池
主次版本以後即是常量池,從1開始而不是從0開始計數(只有常量池特例),爲了表達某些指向常量池的索引值的數據在特定的狀況下須要表達:不引用任何一個常量池項目的含義。
常量池中主要存放兩大類常量:字面量和符號引用。字面量接近於Java語言層面的常量概念,如文本字符串、聲明爲fnal的常量值等。符號引用則屬於編譯原理方面的概念,包括了下面三類常量:
(3)訪問標誌
在常量池結束以後,緊接着的兩個字節表明訪問標誌( access flags),這個標誌用於識別一些類或者接口層次的訪問信息,包括:這個Class是類仍是接口;是否認義爲 public類型;是否認爲 abstract類型;若是是類的話,是否被聲明爲 final等。
(4)類索引、父類索引、接口索引集合
類索引( this class)和父類索引( super class)都是一個u2類型的數據,而接口索引集合( interfaces)是一組u2類型的數據的集合,Cass文件中由這三項數據來肯定這個類的繼承關係。類索引用於肯定這個類的全限定名,父類索引用於肯定這個類的父類的全限定名接口索引集合用來描述這個類實現了哪些接口,這些被實現的接口將按 implements語句(若是這個類自己是一個接口,則應當是 extends語句)後的接口順序從左到右排列在接口索引集合中。
(5)字段表集合
字段表( (field info)用於描述接口或者類中聲明的變量。字段(feld)包括類級變量以實例級變量,但不包括在方法內部聲明的局部變量。
(6)方法表集合
表結構和字段表相似。
(7)屬性表集合
在Class文件、字段表、方法表均可以攜帶本身的屬性表集合,來描述某些場景專有的信息。屬性表中的屬性較多,這裏只以Code屬性爲例其他的可參考書籍。Java程序方法體中的代碼通過 Javac編譯器處理後,最終變爲字節碼指令存儲在Code屬性內。Code屬性出如今方法表的屬性集合之中,但並不是全部的方法表都必須存在這個屬性,如接口或者抽象類中的方法就不存在Code屬性。
若是把一個Java程序中的信息分爲代碼(方法體裏面的Java代碼)和元數據( 包括類、字段、方法定義及其餘信息)兩部分,那麼在整個 Class文件中,Code屬性用於描述代碼,全部的其餘數據項目都用於描述元數據。
虛擬機的類加載機制:虛擬機把描述類的數據從 Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終造成能夠被虛擬機直接使用的Java類型。
Java類的生命週期以下圖所示:
加載、驗證、準備、初始化和卸載這5個階段的順序是肯定的,類的加載過程必須按照這種順序循序漸進地開始,而解析階段則不必定:它在某些狀況下能夠在初始化階段以後再開始,這是爲了支持Java語言的運行時綁定(也稱爲動態綁定或晚期綁定)。
對於初始化階段,虛擬機規範則是嚴格規定了有且只有5種狀況必須當即對類進行「初始化」(而加載、驗證、準備天然須要在此以前開始):
除此以外,全部引用類的方式都不會觸發初始化,稱爲被動引用:
接口的初始化與類的初始化基本相似,只有類的初始化場景中的第三種不一樣:一個接口初始化並不要求其父接口所有都完成了初始化,只有在真正使用到父接口的時候(如引用接口中定義的常量)纔會初始化。
(1)加載
「加載」是「類加載」( Class Loading)過程的一個階段,在加載階段,虛擬機須要完成如下3件事情:
一個非數組類的加載階段能夠由用戶自定義的類加載器去完成,而對數組類而言因爲數組類自己不一樣過類加載器建立,是有Java虛擬機直接建立。
加載階段完成後,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區之中,方法區中的數據存儲格式由虛擬機實現自行定義。注意,加載階段與鏈接階段的部份內容(如一部分字節碼文件格式驗證動做)是交叉進行的,加載階段還沒有完成,鏈接階段可能已經開始,但這兩個階段的開始時間仍而後固定前後順序。
(2)驗證
驗證是鏈接階段的第一步,這一階段的目的是爲了確保 Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。驗證階段包含:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證。
(3)準備
準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。這時候進行內存分配的僅包括類變量(被saic修飾的變量),而不包括實例變量,實例交量將會在對象實例化時隨着對象一塊兒分配在Java雄中。其次,這裏所說的初始值一般狀況下是數據類型的零值。
(4)解析
解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。
(5)初始化
類的初始化是類加載的最後一步,到了初始化階段才真正開始執行類中的Java程序代碼(字節碼)。初始化階段是執行類構造器<clinit>()方法的過程
(1)類與類加載器
對於任意一個類,都須要由加載它的類加載器和這個類自己一同確立其在Java虛擬機中的惟一性,每個類加載器,都擁有一個獨立的類名稱空間。
(2)雙親委派模式
從Java虛擬機角度只存在兩種不一樣的類加載器:一種是啓動類加載器( Bootstrap Classloader),這個類加載器使用C++語言實現,是虛擬機自身的一部分:另外一種就是全部其餘的類加載器,這些類加載器都由Java語言實現,獨立於虛擬機外部,而且全都繼承自抽象類java.lang.ClassLoader。
上圖爲類加載器的雙親委派模型(Parents Delegation Model)。雙親委派模型要求除了頂層的啓動類加載器外,其他的類加載器都應當有本身的父類加載器。這裏類加載器之間的父子關係通常不會以繼承( Inheritance)的關係來實現,而是都使用組合( Composition)關係來複用父加載器的。
雙親委派模型的工做過程:若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每個層次的類加載器都是,所以全部的加載請求最終都應該傳送到頂層的啓動類加載器中,只有當父加載器反饋本身沒法完成這個加載請求(它的搜索範圍中沒有找到所需的類)時,子加載器オ會嘗試本身去加載。
實現雙親委派的代碼都集中在java.lang.ClassLoader的loadClass()方法之中。
0