大學期間必須知道的JVM知識

豆芽59發表於2021-02-01

概述

它是一個虛構出來的計算機,是透過在實際的計算機上模擬模擬各種計算機功能來實現的。
JVM有自己完善的硬體架構,如處理器、堆疊、暫存器等,還具有相應的指令系統。Java語言最重要的特點就是跨平臺執行。使用JVM就是為了支援與作業系統無關,實現跨平臺。所以,JAVA虛擬機器JVM是屬於JRE的,而現在我們安裝JDK時也附帶安裝了JRE(當然也可以單獨安裝JRE)。

Java虛擬機器主要分為五大模組:類裝載器子系統、執行時資料區、執行引擎、本地方法介面和垃圾收集模組。
JVM是執行在作業系統之上的,它與硬體沒有直接的互動

JVM體系結構概覽

亮色區域:
執行緒共享

存在垃圾回收

01.類裝載器ClassLoader

負責 載入class檔案 class檔案在檔案開頭 有特定的檔案標示
class檔案位元組碼內容載入到記憶體中,並將這些內容轉換成 方法區 中的執行時資料結構並且ClassLoader 只負責class檔案的載入 ,至於它是否可以執行,則由Execution Engine決定。

類裝載器類似於快遞公司。

**特定標識:**cafe babe

類裝載器的種類

虛擬機器自帶的載入器

啟動類載入器(Bootstrap)

C++擴充套件類載入器(Extension)

ava應用程式類載入器(AppClassLoader)Java也叫系統類載入器,載入當前應用的classpath的所有類

使用者自定義載入器

Java.lang. ClassLoader的子類,使用者可以定製類的載入方式

 

類載入器的雙親委派機制

當一個類收到了類載入請求,他首先不會嘗試自己去載入這個類,而是把這個請求 委派給父類 去完成,每一個 層次類載入器都是如此 ,因此所有的載入請求都應該傳送到啟動類載入其中,只有當父類載入器反饋自己無法完成這個請求的時候(在它的載入路徑下沒有找到所需載入的Class),子類載入器才會嘗試自己去載入。

採用雙親委派的一個好處是比如載入位於rt.jar包中的類java.lang.Object,不管是哪個載入器載入這個類,最終都是委託給頂層的啟動類載入器進行載入,這樣就保證了使用不同的類載入器最終得到的都是同樣一個Object物件。

砂箱安全機制
保證你寫的程式碼不會汙染java內建的程式碼.

執行引擎

主要的執行技術有:解釋,即時編譯,自適應最佳化、晶片級直接執行其中解釋屬於第一代JVM,即時編譯JIT屬於第二代JVM,自適應最佳化(目前Sun的HotspotJVM採用這種技術)則吸取第一代JVM和第二代JVM的經驗,採用兩者結合的方式 。

自適應最佳化:開始對所有的程式碼都採取解釋執行的方式,並監視程式碼執行情況,然後對那些經常呼叫的方法啟動一個後臺執行緒,將其編譯為原生程式碼,並進行仔細最佳化。若方法不再頻繁使用,則取消編譯過的程式碼,仍對其進行解釋執行。

Execution Engine執行引擎負責解釋命令,提交作業系統執行

02.Native Method Stack(本地方法棧)

它的具體做法是Native Method Stack中登記native方法,在ExecutionEngine執行時載入本地方法庫。

03.Native Interface本地方法介面

本地介面的作用是融合不同的程式語言為Java所用,它的初
衷是融合C/C++程式,Java誕生的時候是C/C++橫行的時候,要想立足,必須有呼叫C/C++程式,於是就在記憶體中專門開闢了一塊區域處理標記為native的程式碼,它的具體做法是Native Method Stack中登記native方法,在Execution Engine執行時載入native libraies。

目前該方法使用的越來越少了,除非是與硬體相關的應用,比
如透過Java程式驅動印表機或者Java系統管理生產裝置,在企業級應用
中已經比較少見。因為現在的異構領域間的通訊很發達,比如可以使用Socket通訊,也可以使用Web Service等等,不多做介紹。

04.程式計數器

pc暫存器

每個執行緒都有一個程式計數器,是執行緒私有的,就是一個指標,
指向方法區中的方法位元組碼(用來儲存指向下一條指令的地址,也即將要執行的指令程式碼),由 執行引擎 讀取下一條指令,是一個非常小的記憶體空間,幾乎可以忽略不記。

這塊記憶體區域很小,它是當前執行緒 所執行的位元組碼的行號指示器 ,位元組碼直譯器透過改變這個計數器的值來選取下一條需要執行的位元組碼指令。如果執行的是一個Native方法,那這個計數器是空的。

用以完成分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能。不會發生記憶體溢位(OutOfMemory=OOM)錯誤。

05.Method Area方法區

執行緒共享

存在垃圾回收

方法區:
供各執行緒共享的執行時記憶體區域。 它儲存了每一個類的結構資訊 (類别範本),例如執行時常量池(Runtime Constant Pool)、欄位和方法資料、建構函式和普通方法的位元組碼內容。上面講的是規範,在不同虛擬機器裡頭實現是不一樣的,最典型的就是永久代(PermGen space)和元空間(Metaspace)。

But

例項變數存在堆記憶體中,和方法區無關。

stack
棧管執行,堆管儲存

棧也叫棧記憶體,主管Java程式的執行,是線上程建立時建立,它
的生命期是跟隨執行緒的生命期,執行緒結束棧記憶體也就釋放,對於棧來說不存在垃圾回收問題,只要執行緒一結束該棧就Over,生命週期和執行緒一致,是執行緒私有的。8種基本型別的變數+物件的引用變數+例項方法都是在函式的棧記憶體中分配。

棧儲存什麼?
棧幀中主要儲存3類資料:

本地變數(Local Variables):輸入引數和輸出引數以及方法內的變數;

棧操作(Operand Stack):記錄出棧、入棧的操作;

棧幀資料(Frame Data):包括類檔案、方法等等。

棧執行原理:
棧中的資料都是以棧幀(Stack Frame)的格式存在,棧幀是一個記憶體區塊,是一個資料集,是一個有關方法(Method)和執行期資料的資料集,當一個方法A被呼叫時就產生了一個棧幀F1,並被壓入到棧中,
A方法又呼叫了B方法,於是產生棧幀F2也被壓入棧,
B方法又呼叫了C方法,於是產生棧幀F3也被壓入棧,………
執行完畢後,先彈出F3棧幀,再彈出F2棧幀,再彈出F1棧幀……
遵循“先進後出”/“後進先出”原則。

每個方法執行的同時都會建立一個棧幀,用於儲存區域性變數表、運算元棧、動態連結、方法出口等資訊,每一個方法從呼叫直至執行完畢的過程,就對應著一個棧幀在虛擬機器中入棧到出棧的過程。棧的大小和具體JVM的實現有關,通常在256K~756K之間,與等於1Mb左右。

 

每執行一個方法都會產生一個棧幀,儲存到棧(後進先出)的頂部,頂部棧就是當前的方法,該方法執行完畢後會自動將此棧幀出棧。

 

HotSpot是使用指標的方式來訪問物件:Java堆中會存放訪問類 的後設資料 的地址,reference儲存的就直接是物件的地址

06.Heap堆

一個JVM例項只存在一個堆記憶體,堆記憶體的大小是可以調節的。

類載入器讀取了類檔案後,需要把類、方法、常變數放到堆記憶體中,儲存所有引用型別的真實資訊,以方便執行器執行,堆記憶體分為三部分:

Young Generation Space 新生區  Young/New

Tenure generation space養老區  old/ Tenure

Permanent Space  永久區  Perm

Java7之前
一個JVM例項只存在一個堆記憶體,堆記憶體的大小是可以調節的。
類載入器讀取了類檔案後,需要把類、方法、常變數放到堆記憶體中,儲存所有引用型別的真實資訊,以方便執行器執行。

o1d養老區,滿了,開啟FuliGC = FGC

Full GC 多次,發現養老區空間沒辦法騰出來,OOM

Java8為元空間,不是永久代

物理上:新生+養老

新生區是類的誕生、成長、消亡的區域,一個類在這裡產生,應
用,最後被垃圾回收器收集,結束生命。新生區又分為兩部分:伊甸區(Eden space)和倖存者區(Survivor pace),所有的類都是在伊甸區被new出來的。倖存區有兩個:0區(Survivor 0 space)和1區
(Survivor 1 space)。當伊甸園的空間用完時,程式又需要建立物件,JVM的垃圾回收器將對伊甸園區進行垃圾回收(Minor GC),將伊甸園區中的不再被其他物件所引用的物件進行銷燬。然後將伊甸園中的剩餘物件移動到倖存0區。若倖存0區也滿了,再對該區進行垃圾回收,然後移動到1區。那如果1區也滿了呢?再移動到養老區。若養老區也滿了,那麼這個時候將產生MajiorGC (Eul1GC),進行養老區的記憶體清理。若養老區執行了Full GC之後發現依然無法進行物件的儲存,就會產生OOM異常“OutOfMemoryExror”。

如果出現java.lang.OutOfMemoryError: Java heap space異常,說明Java虛擬機器的堆記憶體不夠。
原因有二:
(1)Java虛擬機器的堆記憶體設定不夠,可以透過引數-Xms、-Xmx來調整。(2)程式碼中建立了大量大物件,並且長時間不能被垃圾收集器收集(存在被引用)。

from區和to區,他們的位置和名分,不是固定的,每次GC後會交換GC之後有交換,誰空誰是to

MinorGC的過程(複製>清空>互換)

1: eden、SurvivorFrom複製到Survivorlo,年齡+1

首先,當Eden區滿的時候會觸發第一次GC,把還活著的物件複製到SurvivorFrom區,當Eden區再次觸發GC的時候會掃描Eden區和From區域,對這兩個區域進行垃圾回收,經過這次回收後還存活的物件,則直接複製到To區域(如果有物件的年齡已經達到了老年的標準,則賦值到老年代區),同時把這些物件的年齡+1

2:清空eden、SurvivorFrom
然後,清空Eden和SurvivorFrom中的物件,也即複製之後有交換,誰空誰是to

3: SurvivorTo和 SurvivorErom互換
最後,SurvivorTo和SurvivorFrom互換,原SurvivorTo成為下一次GC時的SurvivorFrom區。部分物件會在From和To區域中複製來複制去,如此交換15次(由VM引數MaxTenuringThreshold決定,這個引數預設是15),最終如果還是存活,就存入到老年代。

實際而言,方法區(Method Area)和堆一樣,是各個執行緒共享的內
存區域,它用於儲存虛擬機器載入的:類資訊+普通常量+靜態常量+編譯器編譯後的程式碼等等,雖然JVM規範將方法區描述為堆的一個邏輯部分,但它卻還有一個別名叫做Non-Heap(非堆),目的就是要和堆分開。

對於HotSpot虛擬機器,很多開發者習慣將方法區稱之為“永久代
Parmanent Gen)”,但嚴格本質上說兩者不同,或者說使用永久代來實現方去區而已,永久代是方法區(相當於是一個介面interface)的一個實現,jdk1.7的反本中,已經將原本放在永久代的字串常量池移走。

永久區(java7之前有)
永久儲存區是一個常駐記憶體區域,用於存放JDK自身所攜帶的
Class,Interface 的後設資料,也就是說它儲存的是執行環境必須的類資訊,被裝載進此區域的資料是不會被垃圾回收器回收掉的,關閉JVM才會釋放此區域所佔用的記憶體。

07.堆引數調優



Java8中,永久代已經被移除,被一個稱為元空間的區域所取代。元空間的本質和永久代類似。

元空間與永久代之間最大的區別在於:
永久帶使用的JVM的堆記憶體,但是java8以後的元空間並不在虛擬機器中而是使用本機實體記憶體。

因此,預設情況下,元空間的大小僅受本地記憶體限制。類的後設資料放入native memory,字串池和類的靜態變數放入java堆中,這樣可以載入多少類的後設資料就不再由MaxPermsize控制,而由系統的實際可用空間來控制。

GC:

FullGC:



JVM在進行GC時,並非每次都對上面三個記憶體區域一起回收的,大部分時候回收的都是指新生代。

因此GC按照回收的區域又分了兩種型別,一種是普通GC(minor GC),一種是全域性GC(major GC or Full GC)

Minor GC和IFull GC的區別
普通GC(minor GC):只針對新生代區域的GC,指發生在新生代的垃圾收集動作,因為大多數Java物件存活率都不高,所以Minor GC非常頻繁,一般回收速度也比較快。
全域性GC(major GC or Full GC):指發生在老年代的垃圾收集動作,出現了Major GC,經常會伴隨至少一次的Minor GC(但並不是絕對的)。Major GC的速度一般要比Minor GC慢上10倍以上

判斷物件是否已經死亡的演算法 :引用計數演算法,可達性分析演算法;

四個垃圾收集演算法:標記清除演算法,複製演算法,標記整理演算法,分代收集演算法;

七個垃圾收集器:Serial,SerialOld,ParNew,Parallel Scavenge,Parallel Old,CMS,G1.

引用計數法


Java 中,引用與物件相關聯,如果要操作物件,則必須使用引用。因此,可以透過引用計數來確定物件是否可以回收。實現原則是,如果一個物件被引用一次,計數器 +1,反之亦然。當計數器為 0 時,該物件不被引用,則該物件被視為垃圾,並且可以被 GC 回收利用。

複製演算法

年輕代:

Minor GC會把Eden中的所有活的物件都移到Survivor區域中,如果Survivor區中放不下,那麼剩下的活的物件就被移到Oldgeneration中,也即一旦收集後,Eden是就變成空的了。

當物件在Eden (包括一個 Survivor區域,這裡假設是from 區域)出生後,在經過一次 Minor GC後,如果物件還存活,並且能夠被另外一塊Survivor區域所容納(上面已經假設為from 區域,這裡應為 to區域,即 to區域有足夠的記憶體空間來儲存Eden和from區域中存活的物件),則使用複製演算法將這些仍然還存活的物件複製到另外一塊 Survivor區域(即 to區域)中,然後清理所使用過的Eden 以及 Survivor區域(即from區域),並且將這些物件的年齡設定為1,以後物件在Survivor 區每熬過一次MinorGC,就將物件的年齡+1,當物件的年齡達到某個值時(預設是15歲,透過-XX:MaxTenuringThreshold 來設定引數),這些物件就會成為老年代。

-XX:MaxTenuringThreshold一設定物件在新生代中存活的次數

HotSpot JVM把年輕代分為了三部分:1個Eden區和2個Survivor區(分別叫from和to)。預設比例為8:1:1,一般情況下,新建立的物件都會被分配到Eden區(一些大物件特殊處理),這些物件經過第一次Minor GC後,如果仍然存活,將會被移到Survivor區。物件在Survivor區中每熬過一次Minor GC,年齡就會增加1歲,當它的年齡增加到一定程度時,就會被移動到年老代中。因為年輕代中的物件基本都是朝生夕死的(90%以上),所以在年輕代的垃圾回收演算法使用的是複製演算法, 複製演算法的基本思想就是將記憶體分為兩惚,每次只用其中一塊,當這一塊記憶體用完,就將還活著的物件複製到另外一塊上面。複製演算法不會產生記憶體碎片。

 

GC開始的時候,物件只會存在於Eden區和名為“From”的Survivor區,Survivor區“To”是空的。緊接著進行GC,Eden區中所有存活的物件都會被複制到“To”,而在“From”區中,仍存活的物件會根據他們的年齡值來決定去向。年齡達到一定值(年齡閾值,可以透過-XX:MaxTenuringThreshold來設定)的物件會被移動到年老代中,沒有達到閾值的物件會被複制到“To”區域。經過這次GC後,Eden區和From區已經被清空。這個時候,“From”和“To”會交換他們的角色,也就是新的“To”就是上次GC前的“From”,新的“From"1就是上次GC前的“To”。不管怎樣,都會保證名為To的Survivor區域是空的。Minor GC會一直重複這樣的過程,直到“To”區被填滿,“To”區被填滿之後,會將所有物件移動到年老代中。

因為Eden區物件一般存活率較低。一般的,使用兩塊10%的記憶體作為空閒和活動區間,而另外80%的記憶體,則是用來給新建物件分配記憶體的。一旦發生GC,將10%的from活動區間與另外80%中存活的eden物件轉移到10%的to空閒區間,接下來,將之前90%的記憶體全部釋放,以此類推。

複製演算法它的缺點也是相當明顯的。
1、它浪費了一半的記憶體,這太要命了。

2、如果物件的存活率很高,我們可以極端一點,假設是100%存活,那麼我們需要將所有物件都複製一遍,並將所有引用地址重置一遍。複製這一工作所花費的時間,在物件存活率達到一定程度時,將會變的不可忽視。所以從以上描述不難看出,複製演算法要想使用,最起碼物件的存活率要非常低才行,而且最重要的是,我們必須要克服50%記憶體的浪費

標記清除演算法(Mark-Sweep)

老年代 一般是由標記清除或者是標記清除與標記整理的混合實現

解決了記憶體空間浪費的問題,但是會出現記憶體碎片,並且速度慢。

用通俗的話解釋一下標記清除演算法,就是當程式執行期間,若可以使用的記憶體被耗盡的時候,GC執行緒就會被觸發並將程式暫停,隨後將要回收的物件標記一遍,最終統一回收這些物件,完成標記清理工作接下來便讓應用程式恢復執行。|

 

標記壓縮演算法(Mark-ComPact)


耗時長

在整理壓縮階段,不再對標記的對像做回收,而是透過所有存活對像都向一端移動,然後直接清除邊界以外的記憶體。
可以看到,標記的存活物件將會被整理,按照記憶體地址依次排列,而未被標記的記憶體會被清理掉。如此一來,當我們需要給新物件分配記憶體時,JVM只需要持有一個記憶體的起始地址即可,這比維護一個空閒列表顯然少了許多開銷。
標記/整理演算法不僅可以彌補標記/清除演算法當中,記憶體區域分散的缺點,也消除了複製演算法當中,記憶體減半的高額代價

記憶體效率:複製演算法>標記清除演算法>標記整理演算法(此處的效率只是簡單的對比時間複雜度,實際情況不一定如此)。記憶體整齊度:複製演算法=標記整理演算法>標記清除演算法。
記憶體利用率:標記整理演算法=標記清除演算法>複製演算法。
可以看出,效率上來說,複製演算法是當之無愧的老大,但是卻浪費了太多記憶體,而為了儘量兼顧上面所提到的三個指標,標記/整理演算法相對來說更平滑一些,但效率上依然不盡如人意,它比複製演算法多了一個標記的階段,又比標記/清除多了一個整理記憶體的過程

難道就沒有一種最優演算法嗎?
回答:無,沒有最好的演算法,只有最合適的演算法。
分代收集演算法。

年輕代(Young Gen)

年輕代特點是區域相對老年代較小,對像存活率低。
這種情況複製演算法的回收整理,速度是最快的。複製演算法的效率只和當前存活對像大小有關,因而很適用於年輕代的回收。而複製演算法記憶體利用率不高的問題,透過hotspot中的兩個survivor的設計得到緩解。|

老年代(Tenure Gen)

老年代的特點是區域較大,對像存活率高。
這種情況,存在大量存活率高的對像,複製演算法明顯變得不合適。一般是由標記清除或者是標記清除與標記整理的混合實現。

Mark階段的開銷與存活對像的數量成正比,這點上:說來,對於老年代,標記清除或者標記整理有一些不符,但可以透過多核/執行緒利用,對併發、並行的形式提標記效率。

Sweep階段的開銷與所管理區域的大小形正相關,但Sweep“就地處決”的特點,回收的過程沒有對像的移動。使其相對其它有對像移動步驟的回收演算法,仍然是效率最好的。但是需要解決記憶體碎片問題。

Compact階段的開銷與存活對像的資料成開比,如上一條所描述,對於大量對像的移動是很大開銷的,做為老年代的第一選擇並不合適。

基於上面的考慮,老年代一般是由標記清除或者是標記清除與標記整理的混合實現。

08.JMM

volatile java虛擬機器提供的輕量級的同步機制。

在變數前加volatile,使得各個執行緒可見。

1.可見性

2.原子性

3.VolatileDemo程式碼演示可見性

4.有序性

JMM關於同步的規定:
1執行緒解鎖前,必須把共享變數的值重新整理回主記憶體

2執行緒加鎖前,必須讀取主記憶體的最新值到自己的工作記憶體

3加鎖解鎖是同一把鎖

JMM(Java記憶體模型Java Memory Model,簡稱JMM)本身是一種抽象的概念並不真實存在,它描述的是一組規則或規範,透過這組規範定義了程式中各個變數(包括例項欄位,靜態欄位和構成陣列物件的元素)的訪問方式。

由於JVM執行程式的實體是執行緒,而每個執行緒建立時JVM都會為其建立一個工作記憶體(有些地方稱為棧空間),工作記憶體是每個執行緒的私有資料區域,而Java記憶體模型中規定所有變數都儲存在主記憶體,主記憶體是共享記憶體區域,所有執行緒都可以訪問,但執行緒對變數的操作(讀取賦值等)必須在工作記憶體中進行,首先要將變數從主記憶體複製到的執行緒自己的工作記憶體空間,然後對變數進行操作,操作完成後再將變數寫回主記憶體,不能直接操作主記憶體中的變數,各個執行緒中的工作記憶體中儲存著主記憶體中的變數副本複製,因此不同的執行緒間無法訪問對方的工作記憶體,執行緒間的通訊(傳值)必須透過主記憶體來完成,其簡要訪問過程如下圖:

 

 

 


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69990088/viewspace-2755275/,如需轉載,請註明出處,否則將追究法律責任。

相關文章