Android自我進階——JAVA之JVM

孫志江發表於2020-11-04

為什麼要了解jvm的執行原理?

1.針對系統進行記憶體和垃圾回收監控

2.解決因記憶體溢位和洩露造成的問題

3.對系統進行優化

4.提升jvm和系統效能

JVM執行原理主要有三方面:

1.記憶體管理

2.執行流程

3.垃圾回收
一、jvm記憶體管理

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

(1)程式計數器(執行緒私有):當前執行緒執行位元組碼的行號指示器,位元組碼直譯器工作時通過改變計數器的值來選擇下一條需要執行的位元組碼指令。分支、迴圈、跳轉、異常處理、執行緒回覆等基礎都需要計數器完成。由於jvm多執行緒是通過執行緒輪流切換分配處理器執行時間的方式來實現的.所以,任何一個特定時刻,一個處理器(單核,其實相當於多核cpu的一個核心)只會執行一個執行緒中的指令;多核cpu情況下多個執行緒會在多個核心上排程

備註!

單核cpu:實現多程式依靠於作業系統的程式排程演算法,比如時間片輪轉演算法,比如有3個正在執行的程式(即三個程式),作業系統會讓單核cpu輪流來執行這些程式,然後一個程式只執行2ms,這樣看起來就像多個程式同時在執行,從而實現多程式.

多執行緒其實是最大限度的利用cpu資源.一個擁有兩個執行緒的程式的執行時間可能比一個執行緒的程式執行兩遍的時間還長一點,因為執行緒的切換也需要時間.即採用多執行緒可能不會提高程式的執行速度,反而會降低速度,但是對於使用者來說,可以減少使用者的響應時間.

多核cpu:什麼是多核cpu?多核cpu是一枚處理器中整合多個完整的計算引擎(核心).

多核cpu和單核cpu對於程式來說都是併發,並不是並行.

但是多核cpu每一個核心都可以獨立執行一個執行緒,所以多核cpu可以真正實現多執行緒的並行.比如四核可以把執行緒1234分配給核心1234,如果還有執行緒567就要等待cpu的排程.執行緒1234屬於並行;如果一會核心1停止執行執行緒1改為執行執行緒5,那執行緒15屬於併發.

(2)java虛擬機器棧(執行緒私有):生命週期和執行緒相同,描述java方法執行的記憶體模型,每個方法執行的時候都會建立一個棧幀,棧幀中存有區域性變數表,運算元,動態連結和方法出口等資訊。每個方法從呼叫到執行完畢,對應著一個棧幀的在虛擬機器中的入棧和出棧過程。

(3)本地方法棧(執行緒私有):與虛擬機器棧相似。區別是:虛擬機器棧為虛擬機器執行java方法(位元組碼)服務;本地放發展則為虛擬機器是用到的Native方法服務

(4)java堆(執行緒共享):所有物件例項及陣列都要在堆上分佈(但並不是絕對的,有興趣可以搜搜棧上分配和,TLAB分配)java堆是GC管理機制的主要區域。

java堆可以處於物理上不連續的記憶體空間,只要邏輯上連續即可。

堆大小=新生代+老年代。堆的大小可以指定,擴充套件。當無法擴充套件又存不下的時候就會丟擲OOM異常

新生代:主要用來儲存新生的物件。一般佔堆的三分之一空間。由於頻繁建立物件,所以新生代會頻繁的觸發MinorGC進行垃圾回收

新生代又分為Eden區,SurvivorFrom、SurvivorTo三個區,三個區的預設比例是8:1:1。注意:From區和To區並不是固定,哪一個Survivor區為空,哪一個Survivor區就為To區。

Eden區:java新物件的出生地(如果新建立的物件佔用記憶體很大,則直接分配到老年代)

老年代:主要用來存放jvm認為生命週期較長的記憶體物件(經歷了幾次MinorGC仍然存活)。老年代Gc相對沒有那麼頻繁

(5)永久代、方法區、元空間

永久代:主要存放類定義,位元組碼和常量等很少會變更的資訊。

《java虛擬機器規範》中只是規定了方法區這個概念和它的作用,並沒有規定如何去實現它。在不同的jvm上方法區的實現肯定是不同的。大多數用的jvm都是Sun公司的HotSpot,在HotSpot上使用永久代來實現方法區。永久代是HotSpot的概念,方法區是java虛擬機器規範中的定義,是一種規範,而永久代是一種實現。一個是標準,一個是實現。其他虛擬機器實現並沒有永久代這種說法。在jdk1.7之前,HotSpot使用永久代實現方法區,使用GC分代來實現方法區的記憶體回收。

元空間:java8(jdk1.8)中,移除永久代,用後設資料區(元空間)區域代替。永久代與元空間類似的方面是都是jvm規範中方法的實現。但最大的區別在於:元空間不在虛擬機器中,而是使用本地記憶體。元空間儲存類的元資訊,靜態變數和常量池併入堆中,相當於永久代的資料被分到了堆和元空間。

二、JVM執行流程。Java程式碼通過java原始碼編譯器,編譯生成class檔案,然後交給jvm執行

(1)類載入器載入位元組碼檔案

為什麼要使用類載入器?

java 語言裡,類載入都是在程式執行期間完成的。這樣做缺點是:類載入的時候稍微增加一些效能開銷。

優點是:給Java應用程式提供更搞的靈活性。例如:

(1)編寫一個面向介面的應用程式,可能等到程式執行的時候才指定其實現的子類

(2)使用者可以自定義一個類載入器,讓程式執行的時候從網路或者其他地方載入一個二進位制流作為程式程式碼的一部分(這是Android外掛化,動態更新安裝apk的基礎)

類載入機制:
在這裡插入圖片描述

載入:類載入器將class位元組載入到記憶體中,並將這些靜態資料轉換成方法區中的執行時資料結構,在堆中生成一個代表這個類的java.lang.Class物件,作為方法區類資料訪問的入口。

連結:將java類的二進位制程式碼合併到jvm執行狀態之中的過程

驗證:確保載入的類資訊符合jvm規範,沒有安全方面的問題

準備:正式為類變數分配記憶體並設定初始值

解析:虛擬機器常量池的符號引用替換位元組引用的過程

初始化:

類的主動引用(一定會發生類的初始化)

(1)new一個類的物件

(2)呼叫類的靜態成員(除了final常量)和靜態方法

(3)使用java.lang.reflect包的方法對類進行反射呼叫

(4)當虛擬機器啟動,java Demo01,則一定會初始化Demo01類,說白了就是先啟動main方法所在的類

(5)當初始化一個類,如果其父類沒有被初始化,則先初始化它父類

類的被動引用(不會發生類的初始化)

(1)當訪問一個靜態域時,只有真正聲名這個域的類才會被初始化

(2)通過子類引用父類的靜態變數,不會導致子類初始化

(3)通過陣列定義類的引用,不會觸發此類初始化

(4)引用常量不會觸發此類的初始化(常量在編譯階段就存入呼叫類的常量池中了)

三、Java垃圾回收

垃圾回收機制是由垃圾收集器Garbage Collection來實現的,GC是後臺一個低優先順序的的守護程式。在記憶體低到一定限度時,才自動執行,因此垃圾回收的時間是不確定的。GC也要消耗cpu等資源,如果GC執行過於頻繁會對Java程式執行產生較大影響。GC只能回收通過new關鍵字申請的記憶體(在堆上),但是堆上記憶體並不全是通過new申請分配的。還有一些本地方法,這些記憶體如果不手動釋放,會導致記憶體洩露。需要手動釋放後,再被GC回收。

垃圾分類

(1)改變物件的引用,如置為null或者指向其他物件

    Object obj1 = new Object();

    Object obj2 = new Object();

    obj1 = obj2; //obj1成為垃圾

    obj1 = obj2 = null ; //obj2成為垃圾

(2)引用型別

強引用:是最難被GC回收的,你坑虛擬機器丟擲異常,中斷程式,也不回收強引用指向的例項物件。強引用(指向例項物件,存在堆中)出現記憶體不夠用OOM也不會回收: Object obj=new Object();

軟引用:在記憶體不足時,GC會回收軟引用指向的物件。軟引用可以用來實現記憶體敏感的快取記憶體。軟引用可以和一個引用佇列(ReferenceQueue)聯合使用,如果軟引用的引用物件被垃圾回收,java虛擬機器就會把這個軟引用加入到與之關聯的引用佇列中。

String str=“hello world”;

SoftReference soft=new SoftReference(str);//將強引用轉為弱引用

System.out.print(soft.get());

弱引用(WeakReference),不管記憶體足不足,只要我GC,都可能回收弱引用指向的物件。

WeakReference wReference=new WeakReference(str);

System.out.println(wReference.get());

(3)迴圈每執行一次,生成的Object物件都會成為可回收的物件。

for(int i=0;i<20;i++) {

Object obj = new Object();

System.out.println(obj.getClass());

}

虛引用(PhantomReference ),虛引用必須和引用佇列(ReferenceQueue)聯合使用。

當垃圾回收器發現一個物件有虛引用時,首先執行所引用物件的finalize()方法,在回收記憶體之前,把這個虛引用物件加入到引用佇列中,

你可以通過判斷引用佇列中是否有該虛引用物件,來了解這個物件是否將要被垃圾回收。

然後就可以利用虛引用機制完成物件回收前的一些工作。(注意:當JVM將虛引用插入到引用佇列的時候,虛引用執行的物件記憶體還是存在的。但是PhantomReference並沒有暴露API返回物件。

所以如果我想做清理工作,需要繼承PhantomReference類,以便訪問它指向的物件。)

ReferenceQueue queue=new ReferenceQueue<>();

PhantomReference phantomReference=new PhantomReference(str,queue);

System.out.println(phantomReference.get());

(4)類巢狀

class A{

A a;

}

A x=new A();//分配了一個空間

x.a=new A(); //分配了一個空間

x=null;//產生了兩個垃圾

(5)執行緒中的垃圾

calss A implements Runnable{

void run(){

//…}

}

A x = new A();

x.start();

x=null; //執行緒執行完成後x物件才被認定為垃圾

垃圾回收判斷演算法

(1) 引用計數法

引用計數法是最經典的一種垃圾回收演算法。其實現很簡單,對於一個A物件,只要有任何一個物件引用了A,則A的引用計算器就加1,當引用失效時,引用計數器減1.只要A的引用計數器值為0,則物件A就不可能再被使用。

缺點:無法處理迴圈引用的問題,因此在Java的垃圾回收器中,沒有使用該演算法

優點:引用計數法實現比較簡單

引用計數器要求在每次因引用產生和消除的時候,需要伴隨一個加法操作和減法操作,對系統效能會有一定的影響

(2)可達性分析法

可達性分析法也被稱之為根搜尋法,可達性是指,如果一個物件會被至少一個在程式中的變數通過直接或間接的方式被其他可達的物件引用,則稱該物件就是可達的。更準確的說,一個物件只有滿足下述兩個條件之一,就會被判斷為可達的:

物件是屬於根集中的物件

物件被一個可達的物件引用

垃圾回收演算法

(1)標記-清除演算法

標記-清除(Tracing Collector)演算法是最基礎的收集演算法,為了解決引用計數法的問題而提出。它使用了根集的概念,它分為“標記”和“清除”兩個階段:首先標記出所需回收的物件,在標記完成後統一回收掉所有被標記的物件,它的標記過程其實就是前面的可達性分析法中判定垃圾物件的標記過程。

優點:不需要進行物件的移動,並且僅對不存活的物件進行處理,在存活物件比較多的情況下極為高效。

缺點:標記和清除過程的效率都不高,這種方法需要使用一個空閒列表來記錄所有的空閒區域以及大小,對空閒列表的管理會增加分配物件時的工作量;標記清除後會產生大量不連續的記憶體碎片,雖然空閒區域的大小是足夠的,但卻可能沒有一個單一區域能夠滿足這次分配所需的大小,因此本次分配還是會失敗,不得不觸發另一次垃圾收集動作。

(2)標記-整理演算法

標記-整理(Compacting Collector)演算法標記的過程與“標記-清除”演算法中的標記過程一樣,但對標記後出的垃圾物件的處理情況有所不同,它不是直接對可回收物件進行清理,而是讓所有的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。在基於“標記-整理”演算法的收集器的實現中,一般增加控制程式碼和控制程式碼表。

優點:經過整理之後,新物件的分配只需要通過指標碰撞便能完成,比較簡單;使用這種方法,空閒區域的位置是始終可知的,也不會再有碎片的問題了。

缺點:GC 暫停的時間會增長,因為你需要將所有的物件都拷貝到一個新的地方,還得更新它們的引用地址。

(3)複製演算法

複製(Copying Collector)演算法的提出是為了克服控制程式碼的開銷和解決堆碎片的垃圾回收。它將記憶體按容量分為大小相等的兩塊,每次只使用其中的一塊(物件面),當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊記憶體上面(空閒面),然後再把已使用過的記憶體空間一次清理掉。

複製演算法比較適合於新生代(短生存期的物件),在老年代(長生存期的物件)中,物件存活率比較高,如果執行較多的複製操作,效率將會變低,所以老年代一般會選用其他演算法,如“標記-整理”演算法。一種典型的基於複製演算法的垃圾回收是stop-and-copy演算法,它將堆分成物件區和空閒區,在物件區與空閒區的切換過程中,程式暫停執行。

優點:標記階段和複製階段可以同時進行;每次只對一塊記憶體進行回收,執行高效;只需移動棧頂指標,按順序分配記憶體即可,實現簡單;記憶體回收時不用考慮記憶體碎片的出現。

缺點:需要一塊能容納下所有存活物件的額外的記憶體空間。因此,可一次性分配的最大記憶體縮小了一半。

(4)分代收集演算法

分代收集(Generational Collector)演算法的將堆記憶體劃分為新生代、老年代和永久代。新生代又被進一步劃分為 Eden 和 Survivor 區,其中 Survivor 由 FromSpace(Survivor0)和 ToSpace(Survivor1)組成。所有通過new建立的物件的記憶體都在堆中分配,其大小可以通過-Xmx和-Xms來控制。分代收集,是基於這樣一個事實:不同的物件的生命週期是不一樣的。因此,可以將不同生命週期的物件分代,不同的代採取不同的回收演算法進行垃圾回收,以便提高回收效率。

新生代GC(MinorGC)複製演算法

在這裡插入圖片描述在後續GC這裡插入圖片描述備註:圖片裡忘記說了,後續GC,依此類推

Eden區:Java新物件的出生地(如果新建立的物件佔用記憶體很大則直接分配給老年代)。當Eden區記憶體不夠的時候就會觸發一次MinorGc,對新生代區進行一次垃圾回收。

ServivorTo:保留了一次MinorGc過程中的倖存者。

ServivorFrom: 上一次GC的倖存者,作為這一次GC的被掃描者。

當JVM無法為新建物件分配記憶體空間的時候(Eden區滿的時候),JVM觸發MinorGc。因此新生代空間佔用越低,MinorGc越頻繁。

MinorGC採用複製演算法。

老年代GC(MajorGC) MajorGC採用標記—清除演算法(或者標記—整理演算法)

MajorGC的耗時比較長,因為要先整體掃描再回收,MajorGC會產生記憶體碎片。為了減少記憶體損耗,一般需要合併或者標記出來方便下次直接分配。

當老年代也滿了裝不下的時候,就會丟擲OOM。

永久代GC(jdk1.8中已經移除)

指記憶體的永久儲存區域,主要存放Class和Meta(後設資料)的資訊。

Class在被載入的時候後設資料資訊會放入永久區域,但是GC不會在主程式執行的時候清除永久代的資訊。所以這也導致永久代的資訊會隨著類載入的增多而膨脹,最終導致OOM。

總結:

minorGC 清理新生代。

Major GC 是清理老年代。

Full GC 是清理整個堆空間—包括年輕代和老年代。

(1)MinorGC 觸發機制

Eden區滿的時候,JVM會觸發MinorGC。
(2)MajorGC 觸發機制

1 在進行MajorGC之前,一般都先進行了一次MinorGC,使得有新生代的物件進入老年代,當老年代空間不足時就會觸發MajorGC。

2 當無法找到足夠大的連續空間分配給新建立的較大物件時(如大陣列),也會觸發MajorGC進行垃圾回收騰出空間。

(3)Full GC觸發機制

1 呼叫System.gc時,系統建議執行Full GC,但是不必然執行

2 老年代空間不足

3 方法區空間不足

4 通過Minor GC後進入老年代的平均大小大於老年代的可用記憶體

5 由Eden區、survivor space1(From Space)區向survivor space2(To Space)區複製時,

4 當永久代滿時也會引發Full GC,會導致Class、Method元資訊的解除安裝。

相關文章