java 筆記

技術從未如此性感發表於2018-03-12

由於java虛擬機器的多執行緒是通過執行緒輪流切換並分配處理器執行時間的方式來實現的。在任何一個時刻,一個處理器都只會執行一條執行緒中的指令。因此,為了執行緒切換後能恢復到正確的位置,每一條執行緒都需要有一個獨立的程式計數器,各條執行緒的程式計數器互不影響,獨立儲存。


java虛擬機器棧是執行緒私有的,它的生命週期與執行緒相同,虛擬機器棧描述的是java方法執行的記憶體模型,每一個方法在執行的同時都會建立一個棧幀用於存貯區域性 變數表、運算元棧、動態連結、方法出口等資訊。每一個方法從呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程。


java虛擬機器規範中描述:所有的物件例項以及陣列都要在堆上分配,但是隨著JIT編譯器的發展與逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化發生,所有的物件在堆上分配也漸漸的不是那麼的絕對了。


java堆和方法區是各個執行緒共享的。

堆用於存放物件的例項,方法區用於存貯已被java虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料[可以理解為存放class檔案的資訊]。

執行時常量池是方法區的一部分。class檔案中除了有類的版本資訊、欄位、方法、介面等的描述資訊外,還有一項資訊是常量池,用於存放編譯器生成的各種字面量和符號引用,這部分內容將在類載入後進入方法區的執行時常量池中存放。


虛擬機器在遇到一條new指令時,首先去檢查這個指令的引數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被載入、解析和初始化過,如果沒有,那必須先執行相應的類載入過程。


在java語言中,可以作為GC ROOTS的物件包括下面的幾種:

1、虛擬機器棧(棧幀中的本地變數表)中引用的物件

2、方法區中類靜態屬性引用的物件

3、方法區中常量引用的物件

4、本地方法棧中JNI引用的物件


即時在可達性分析演算法中不可達的物件,也並非是非死不可的,這時候他們展示處於緩刑階段,要真正宣告一個物件死亡,至少要經歷兩次標記過程:如果物件在進行了可達性分析之後發現沒有與GC ROOTS相連的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此物件是否有必要執行finalize()方法。當物件沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機器呼叫過,虛擬機器將這兩種情況都視為沒有必要執行。

如果這個物件被判定為有必要執行finalize()方法,那麼這個物件將會被放置到一個叫做F-Queue的佇列之中,並在稍後由一個由虛擬機器自動建立的、低優先順序的Finalizer執行緒去執行它。這裡的所謂的“執行”是指虛擬機器會觸發這個方法,但是並不承諾等到它執行結束,這樣做的原因是,如果一個物件在finalize()方法中執行緩慢,或者是發生死迴圈。將很可能導致F-Queue佇列中其他的物件永久處於等待,甚至導致整個記憶體回收系統崩潰。finalize()方法是物件逃脫死亡的最後一個機會,稍後GC將對F-Queue中的物件進行第二次小規模的標記,如果物件要在finalize()中成功拯救自己---只要與引用鏈上的任何一個物件建立關聯即可,比如把自己賦值給某一個類變數或者物件的成員變數,那麼在第二次標記時它將被移除“即將回收”的集合;如果物件在這個時候還沒有逃脫,那基本上是它就真的被回收了。


jvm判斷一個類是否是“無用的類”的條件,需要同時滿足一下三個條件:

1、該類的所有例項都已經被回收,也就是java堆裡面不存在該類的任何例項物件

2、載入該類的classloader已經被回收

3、該類對應的java.lang.Class物件沒有任何地方被引用,無法在任何地方通過反射訪問該類的方法。


垃圾收集演算法:

1、“標記-清除”演算法,該演算法分為標記和清除兩個階段。首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件。利用可達分析來進行標記。

2、“複製”演算法,將可用的記憶體按照容量相等分為兩塊,每次只是使用其中的一塊,當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊沒有使用過的記憶體上面,然後再把已經使用過的記憶體空間一次清理掉。

3、“標記-整理”演算法,該演算法的標記過程與“標記-清除”演算法一樣,但是後續步驟不是直接對可回收物件進行清理,而是所有存活的物件都像一端移動,然後清理掉端邊界以外的記憶體。

4、“分代收集”演算法,該演算法根據物件存活週期的不同將記憶體劃分為幾塊。一般是把java對分為新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集演算法。在新生代中,每次垃圾收集的時候都發現有大批的物件死去,只有少量的存活,那就選用複製演算法,只需要付出少量存活物件複製的成本就可以完成收集。而老年代中因為物件存活率高,沒有額外空間對它進行分配擔保,就必須採用“標記-清理”或者“標記-整理”演算法來進行回收。


常量池中主要存放兩類常量:

1、字面值

    字面值比較接近與java語言層面的常量概念,如文字字串、宣告為final的常量值等

2、符號引用

    符號引用屬於編譯原理方面的概念,包括了下面三種常量:

    a、類和介面的全限定名

    b、欄位的名稱和描述符

    c、方法的名稱和描述符

當虛擬機器執行的時候,需要從常量池中獲得對應的符號引用,再在類建立的時候或者執行時解析、翻譯到具體的記憶體 地址之中


記憶體申請過程

  1. JVM會試圖為相關Java物件在Eden中初始化一塊記憶體區域;
  2. 當Eden空間足夠時,記憶體申請結束。否則到下一步;
  3. JVM試圖釋放在Eden中所有不活躍的物件(minor collection),釋放後若Eden空間仍然不足以放入新物件,則試圖將部分Eden中活躍物件放入Survivor區;
  4. Survivor區被用來作為Eden及old的中間交換區域,當old區空間足夠時,Survivor區的物件會被移到Old區,否則會被保留在Survivor區;
  5. 當old區空間不夠時,JVM會在old區進行major collection;
  6. 完全垃圾收集後,若Survivor及old區仍然無法存放從Eden複製過來的部分物件,導致JVM無法在Eden區為新物件建立記憶體區域,則出現"Out of memory錯誤";

物件衰老過程

   新建立的物件的記憶體都分配自eden。Minor collection的過程就是將eden和在用survivor space中的活物件copy到空閒survivor space中。物件在young generation裡經歷了一定次數(可以通過引數配置)的minor collection後,就會被移到old generation中,稱為tenuring。


虛擬機器把描述類的資料從Class檔案載入到記憶體,並且對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別,這就是虛擬機器的類載入機制。


java語言 型別的載入、連線和初始化過程都是在程式執行期間完成的,這種策略雖然會令類載入稍微增加一些效能開銷,但是會為java應用程式提供高度的靈活性,java可以擴充套件的語言特性就是依賴執行期動態載入和動態連線實現的。



圖中載入、驗證、準備、初始化和解除安裝這 5 個階段的順序是確定的,類的載入過程必須按照這種順序按部就班的開始,而解析階段則不一定;它在某些情況下可以是在初始化階段之後再開始,這是為了支援java語言的執行時繫結。


在載入階段,虛擬機器需要完成一下3件事情:

1、通過一個類的全限定名來獲取定義此類的二進位制位元組流

2、將這個位元組流所代表的靜態存貯結構轉化為方法區的執行時資料結構。

3、在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口。


陣列類不是通過類載入建立,它是由虛擬機器直接建立的。


驗證階段:

1、檔案格式驗證

2、後設資料驗證

3、位元組碼驗證

4、符號引用驗證


準備階段是正式為類變數分配記憶體並設定類變數初始值的階段,這些變數所使用的記憶體都將在方法區中進行分配。注意:這個階段進行記憶體分配的僅包括類變數(被static修飾的變數),而不包括例項變數,例項變數將會在物件例項化時隨著物件一起分配在java堆中。


解析階段 虛擬機器將常量池內的符號引用替換為直接引用的過程。

符號引用: 符號引用是以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用是能無起義的定位到目標即可,符號引用與虛擬機器實現的記憶體佈局無關,引用的目標並不一定已經載入到記憶體中 。各種虛擬機器實現的記憶體佈局可以各不相同,但是它們能接受的符號引用必須都是一致的,因為符號引用的字面量形式明確定義在java虛擬機器規範的Class檔案格式中。

直接引用:直接引用可以是直接指向目標的指標、相對偏移量或是一個能間接定位到目標的控制程式碼。直接引用是和虛擬機器實現的記憶體佈局相關的,同一個符號引用在不同虛擬機器例項上翻譯出來的直接引用一般不會相同。如果有了直接引用,那麼引用的目標必定已經在記憶體中存在。

解析動作主要針對類和介面、欄位、類方法、介面方法、方法型別、方法控制程式碼和呼叫點限定符7類符號引用進行。


<clinit>()

<clinit>() 方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊中的語句合併產生的,編譯器收集的順序是由語句在原始檔中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變數,定義在它之後的變數,在前面的靜態語句塊可以賦值,但不能訪問。

<clinit>() 方法與類的構造方法[ <init>()方法 ]不同,它不需要顯式的呼叫父類構造器,虛擬機器會保證在子類的<clinit>()方法執行之前,父類的<clinit>() 方法已經執行完畢。因此在虛擬機器中第一個被執行的<clinit>() 方法的類肯定是java.lang.Object。

由於父類的<clinit>()方法先執行,也就意味著父類中定義的靜態語句塊要優先於子類的變數賦值操作。

<clinit>()方法對於類或者介面來說並不是必須的,如果一個類中沒有靜態語句塊,也沒有對變數的賦值操作,那麼編譯器可以不為這個類生成<clinit>()方法。

介面中不能使用靜態語句塊,但仍然有變數初始化的賦值操作,因此介面與類一樣都會生成<clinit>()方法。但是介面與類不同的是,執行介面的<clinit>()方法不需要先執行父介面的<clinit>()方法。只有當父介面中的變數使用時,父介面才會初始化。另外,介面的實現類在初始化時也一樣不會執行介面的<clinit>()方法。

虛擬機器會保證一個類的<clinit>()方法在多執行緒環境中被正確的加鎖、同步,如果多個執行緒同時去初始化一個類,那麼只會有一個執行緒去執行這個類的<clinit>()方法,其他執行緒都需要阻塞等待,直到活動執行緒執行<clinit>()方法完畢。如果在一個類的<clinit>()方法中有 耗時很長的操作,就可能造成多個執行緒阻塞。


類與類載入器

    對於任何一個類,都需要由載入它的類載入器和這個類本身一同確立其在java虛擬機器中的唯一性,每一個類載入器,都有一個獨立的類名稱空間。


從開發的角度來看 類載入器可以分為一下三類:

1、啟動類載入器 [ Bootstrap ClassLoader,該類載入器是虛擬機器的一部分],載入<JAVA_HOME>\lib目錄或者是被 -Xbootclasspath引數所指定的路徑中的類。

2、擴充套件類載入器,載入<JAVA_HOME>\lib\ext 目錄中的類。

3、應用程式類載入器,載入使用者類路徑上的類。


在OSGI環境下,類載入器不再是雙親委派模型中的樹狀結構。而是進一步發展為更加複雜的網狀結構。


執行時棧幀結構

    棧幀是用於支援虛擬機器進行方法呼叫和方法執行的資料結構,它是虛擬機器執行是資料區中的虛擬機器棧(Virtual Machinr Stack) 的棧元素。棧幀儲存了方法的區域性變數表、運算元棧、動態連結和方法返回地址等資訊。每一個方法從呼叫開始至執行完成的過程,都對應著一個棧幀在虛擬機器棧裡面從入棧到出棧的過程。

    在編譯程式碼的時候,棧幀需要多大的區域性變數表,多深的運算元棧都已經完全確定了,並且寫入到方法表的Code屬性之中,因此一個棧幀需要分配多少記憶體,不會受到程式執行期變數資料的影響,而僅僅取決於具體的虛擬機器實現。

區域性變數表

    區域性變數表是一組變數值儲存空間,用於儲存方法引數和方法內部定義的區域性變數。在java程式編譯為Class檔案時,就在方法的Code屬性的max_locals資料項中確定了該方法所需要分配的區域性變數表的最大容量。

    區域性變數表的容量以變數槽(Slot)為最小單位


運算元棧

    運算元棧也常稱為操作棧,後入先出的資料結構。同區域性變數表一樣,運算元棧的最大深度也在編譯的時候寫入到Code屬性的max_stacks資料項中。

    當一個方法剛剛開始執行的時候,這個方法的運算元棧是空的,在方法的執行過程中,會有各種位元組碼指令往運算元棧中寫入和提取內容,也就是出棧/入棧操作。

    java虛擬機器的解釋執行引擎稱為“基於棧的執行引擎”,其中所指的棧就是運算元棧。


動態連線

    每個棧幀都包含一個指向執行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支援方法呼叫過程中的動態連線。


方法返回地址

    當一個方法開始執行後,只有兩種方式可以退出這個方法。第一種方式是執行引擎遇到任意一個方法返回的位元組碼指令,這時候可能會有返回值傳遞給上層的方法呼叫者,是否有返回值和返回值的型別將根據遇到何種方法返回指令來決定,這種退出方法的方式稱為正常完成出口。

    另一種退出方式是,在方法執行過程中遇到異常,並且這個異常沒有在方法體內得到處理,無論是java虛擬機器內部產生的異常,還是程式碼中使用athrow位元組碼指令產生的異常,只要在本方法的異常表中沒有搜尋到匹配的異常處理器,就會導致方法退出,這種退出方式稱為異常完成出口。一個方法使用異常完成出口的方式退出,是不會給它的上層呼叫者任何返回值的。


編譯器在過載時是通過引數的靜態型別而不是即時型別作為判定依據的。


動態實現 invokevirtual 指令的執行時解析過程大致分為一下幾個步驟:

1、找到運算元棧頂的第一個元素所指向的物件的實際型別,記作 C

2、如果在類C中找到與常量中的描述符和簡單名稱都相符的方法,則進行訪問許可權校驗,如果通過則返回這個方法的直接引用,查詢結束;如果不通過,則返回java.lang.IllegalAccessError 異常。

3、否則,按照繼承關係從上往下一次對C的各個父類進行第二步的搜尋和驗證過程。

4、如果始終沒有找到合適的方法,則丟擲java.lang.AbstractMethodError異常。


GCJ編譯器可以直接把java檔案編譯成原生程式碼,C/C++解釋執行的版本 CINT

在說java“解釋執行”的時候,只有確定了討論物件是某一種具體的java實現版本和執行引擎執行模式時,談論解釋執行還是編譯執行才會比較的確切。

                                                                            編譯過程

    java語言中,javac編譯器完成了程式程式碼經過詞法分析、語法分析到抽象語法樹,再遍歷語法樹生成線性的位元組碼指令流的過程。


基於棧的指令集與基於暫存器的指令集

    java編譯器輸出的指令流,基本上是一種基於棧的指令集架構,指令流中的指令大部分都是零地址指令,它們依賴運算元棧進行工作,也即操作指令的運算元都是存放在棧中的。與之相對的另外一套常用的指令集架構是基於暫存器的指令集,最典型的就是x86的二地址指令集,通俗點說,就是現在主流pc機中直接支援的指令集架構,這些指令依賴暫存器進行工作。

    基於棧的指令 集主要有點在於可移植,暫存器由硬體直接提供,程式直接依賴這些硬體暫存器則不可避免地受到硬體約束。如果是使用棧架構指令集,使用者程式不會直接使用這些暫存器。就可以由虛擬機器實現來自行決定把一些訪問最頻繁的資料放到暫存器中以獲取儘量好的效能,這樣實現起來也更加簡單一些,棧架構的指令集還有一些其他的優點,如程式碼相對更加緊湊(位元組碼中每個位元組就對應一條指令,而多地址指令中還需要存放引數)、編譯器實現更加簡單(不需要考慮空間分配的問題,所需空間都在棧上操作)等。

    棧架構指令集的主要缺點是執行速度相對來說稍慢一些,所有主流物理機的指令集都是暫存器架構也從側面印證了這一點。


sun javac編譯過程:

    1、解析與填充符號表過程

    2、插入式註解初期裡的註解處理過程

    3、分析與位元組碼生成過程


java中的泛型在編譯器就已經被擦除。


在class檔案中,只要描述符不是完全一致的兩個方法就可以共存。也就是說,兩個方法如果有相同的名稱和簽名,但是返回值不同,那它們也是可以合法地共存於一個class檔案中的。


逃逸分析

    逃逸分析是目前虛擬機器中比較前沿的優化技術,它並不是直接優化程式碼的手段,而是為其他優化手段提供依據的分析技術。

    逃逸分析的基本行為就是分析物件動態作用域:當一個物件在方法中被定義後,它可能被外部方法所引用,例如作為呼叫引數傳遞到其他方法中,稱為方法逃逸。甚至還有可能被外部執行緒訪問到,比如賦值給類變數或可以在其它執行緒的例項變數,稱為執行緒逃逸。

    如果能證明一個物件不會逃逸到方法或者是執行緒之外,則可以為這個變數做一些優化:

    1、棧上分配,這樣物件所佔的記憶體空間就可以隨著棧幀出棧而銷燬。在一般的應用中,不會逃逸的區域性物件所佔比列很大,如果能使用棧上分配,那大量的物件就會隨著方法的結束而自動銷燬了,垃圾收集系統的壓力將會小很多。

    2、同步消除,執行緒同步是一個比較耗時的操作,如果一個變數不會逃逸出執行緒,無法被其他的執行緒訪問,那麼這個執行緒的讀寫就不會有競爭了,對於這個變數實施的同步措施就可以消除。

    3、標量替換:標量是指一個資料已經無法再分解為更小的資料來表示了,java虛擬機器中的原始資料型別以及reference都不能在進一步分解,他們就可以稱為標量。相對的,如果一個資料可以繼續分解,那它就稱作聚合量,java中的物件就是最典型的聚合量。如果把一個java物件拆散,根據程式訪問的情況,將其使用到的成員變數恢復元好似型別來訪問就叫做標量替換。如果逃逸分析可以證明一個物件不會被外部訪問,並且這個物件可以被拆散的話,那程式真正執行的時候將可能不會建立這個物件,而改為直接建立它的若干個被這個方法直接使用到的成員變數來代替。將物件拆散後,出了可以讓物件的成員變數在棧上分配和讀寫之外,還可以為後續進一步的優化手段建立條件。


                                                                                處理器、快取記憶體、主記憶體間的互動關係


主記憶體與工作記憶體

    java記憶體模型的主要目標是定義程式中各個變數的訪問規則,即在虛擬機器中將變數儲存到記憶體和從記憶體中取出變數這樣的底層細節。此處的變數與java程式設計中所說的變數有所區別,它包括了例項欄位、靜態欄位和構成陣列物件的元素,但不包括區域性變數與方法引數,因為後者是執行緒私有的,不會被共享,自然就不會存在競爭問題。

    java記憶體模型中規定了所有的變數都存放在主記憶體中(虛擬機器記憶體的一部分)。每一條執行緒都有自己的工作記憶體,執行緒的工作記憶體中儲存了被該執行緒使用到的變數的主記憶體副本拷貝,執行緒對變數的所有操作都必須在工作記憶體中進行,而不能直接的操作主記憶體中變數。不同的執行緒之間也無法訪問到其他執行緒工作記憶體中的變數,執行緒間變數值的傳遞都需要通過主記憶體來完成。




volatile保證了新值能夠立即同步到主記憶體,以及每次使用前立即從主記憶體重新整理。














    





































相關文章