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

真正沒有資格談明天的人,是那個不懂得珍惜今日的人。
你好,我是夢陽辰,期待與你相遇!

概述

它是一個虛構出來的計算機,是通過在實際的計算機上仿真模擬各種計算機功能來實現的。
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內存模型中規定所有變量都存儲在主內存,主內存是共享內存區域,所有線程都可以訪問,但線程對變量的操作(讀取賦值等)必須在工作內存中進行,首先要將變量從主內存拷貝到的線程自己的工作內存空間,然後對變量進行操作,操作完成後再將變量寫回主內存,不能直接操作主內存中的變量,各個線程中的工作內存中存儲着主內存中的變量副本拷貝,因此不同的線程間無法訪問對方的工作內存,線程間的通信(傳值)必須通過主內存來完成,其簡要訪問過程如下圖:
在這裏插入圖片描述

To the time to life, rather than to life in time to the time to life, rather than to life in time.

在這裏插入圖片描述
在這裏插入圖片描述


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章