自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

從零開始掌握 JVM

開發(fā)
無論你是剛剛接觸 Java 編程的新手,還是希望深入了解 JVM 內(nèi)部運作的技術(shù)愛好者,這篇文章都將為你提供全面而易懂的知識點介紹。

在當(dāng)今的軟件開發(fā)領(lǐng)域,Java 語言及其運行環(huán)境——Java 虛擬機(JVM)占據(jù)了舉足輕重的地位。無論是企業(yè)級應(yīng)用、Web 應(yīng)用還是移動應(yīng)用,JVM 都扮演著核心角色。然而,對于許多初學(xué)者來說,理解 JVM 的工作原理和內(nèi)部機制可能是一項挑戰(zhàn)。

本文將帶你從零開始,逐步了解 JVM 的基本概念、結(jié)構(gòu)與功能。無論你是剛剛接觸 Java 編程的新手,還是希望深入了解 JVM 內(nèi)部運作的技術(shù)愛好者,這篇文章都將為你提供全面而易懂的知識點介紹。

一、詳解JVM基礎(chǔ)概念

1.什么是JVM?JVM的作用是什么

JVM是Java設(shè)計者用于屏蔽多平臺差異,基于操作系統(tǒng)之上的一個"小型虛擬機",正是因為JVM的存在,使得Java應(yīng)用程序運行時不需要關(guān)注底層操作系統(tǒng)的差異。使得Java程序編譯只需編譯一次生成class字節(jié)碼,即可在任何操作系統(tǒng)都可以以相同的方式運行。

2.JVM運行時區(qū)域劃分

(1) JVM體系結(jié)概覽

因為JVM屏蔽了底層操作系統(tǒng)的差異,所以它自成了一套內(nèi)存結(jié)構(gòu)進行獨立的內(nèi)存管理,整體來說JVM可分為以下幾個部分:

  • 方法區(qū)
  • 堆區(qū)
  • 虛擬機棧和本地方法棧
  • 程序計數(shù)器

對應(yīng)的我們也給出一張宏觀的圖片:

(2) 方法區(qū)

我們先來說說方法區(qū),這里我們所說的方法區(qū)指的不是存放Java方法的區(qū)域,并且它也只是一個邏輯上的物理區(qū)域的概念,在不同的JDK版本中它的實現(xiàn)都會有所和不同,方法區(qū)主要存放的數(shù)據(jù)包括:

  • 類信息:例如類名、父類名、接口列表、常量池、字段表、方法表等。
  • 常量池:存儲編譯器生產(chǎn)的各種字面量和符號引用。
  • 方法代碼:包括方法的字節(jié)碼指令和其他輔助信息,例如操作數(shù)棧和局部變量表等。
  • 靜態(tài)變量:屬于類的各種靜態(tài)變量。
  • 類的構(gòu)造器和初始化塊。

(3) 堆內(nèi)存

然后就是JVM的堆區(qū),對象實例和數(shù)組大部分都會存儲在這塊內(nèi)存空間中,注意筆者這里所說的一個強調(diào)——大部分,因為現(xiàn)代即時編譯技術(shù)的進步,在JVM進行逃逸分析時如果發(fā)現(xiàn)對象并未逃逸,則會直接進行棧上分配、標(biāo)量替換等手段將其分配在??臻g,并且java堆區(qū)是線程共享區(qū)域的,所以多線程情況下操作相同對象可能存在線程安全問題。

(4) 虛擬機棧

我們?nèi)粘ο髮嵗姆椒ㄕ{(diào)用都是在虛擬機棧上運行的,它是Java方法執(zhí)行的內(nèi)存模型,存儲著被執(zhí)行方法的局部變量表、動態(tài)鏈表、方法入口、棧的操作用(入棧和出棧)。

由于虛擬機棧是棧結(jié)構(gòu)所以方法調(diào)用按順序壓入棧中,就會倒序彈出虛擬機棧,例如我們的下面這段代碼:

public void a(){
b();
}
public void b(){
c();
}

public void c(){
}

當(dāng)線程調(diào)用a方法時,優(yōu)先為a產(chǎn)生一個棧幀A壓入棧中,發(fā)現(xiàn)a方法調(diào)用了b方法,再為b產(chǎn)生一個棧幀B壓入棧中,然后b再調(diào)用c方法,再為c產(chǎn)生一個棧幀C方法壓入棧中。

同理,執(zhí)行順序也是c先執(zhí)行結(jié)束,優(yōu)先彈出棧,然后是b,最后是a。由此我們可知Java中方法是可以嵌套調(diào)用的,但這并不意味方法可以無線層次的嵌套調(diào)用,當(dāng)方法嵌套調(diào)用深度超過了虛擬機棧規(guī)定的最大深度,就會拋出StackOverflowError,而這個錯誤也常常發(fā)生在我們編寫的無終止條件的遞歸代碼中。

虛擬機棧屬于線程獨享,所以也就沒有什么生命周期的概念,每個方法隨著調(diào)用的結(jié)束??臻g也隨之釋放,所以棧的生命周期也可以理解為和線程生命周期是一致的。

每個方法的調(diào)用都是往虛擬機棧中壓入一個棧幀,例如上述我們調(diào)用a方法,就是將a方法壓入棧幀,而每一個棧幀都有一個局部變量表,這個局部變量表用于記錄方法體內(nèi)的某些基本類型(byte、short、int、boolean、float、char、long、double)還有對象引用(不等同于對象本省,可能是一個指向?qū)ο笃鹗嫉刂返囊弥羔?和returnAddress(指向一條字節(jié)碼指令的地址)。

這些數(shù)據(jù)都會存儲在局部變量表的slot槽中,在某些情況下每個棧幀可能存在復(fù)用,我們不妨舉個例子,可以看到下面這段代碼就是在main方法上分配一個byte數(shù)組,我們添加一個-verbose:gc參數(shù)觀察gc回收情況:

public static void main(String[] args) {   
    byte[] placeHolder = new byte[1024 * 1024 * 64];
    System.gc();
 }

查看輸出結(jié)果可以看到byte數(shù)組空間沒有被回收,就是因為slot局部變量placeHolder 對應(yīng)的槽還沒有被其他變量所復(fù)用,這也就意味著此刻可達性算法分析認(rèn)為這塊placeHolder 不可被GC所以就不會被垃圾回收:

[GC (System.gc())  86054K->68541K(243712K), 0.0023357 secs]
[Full GC (System.gc())  68541K->68243K(243712K), 0.0203291 secs]

對此我們簡單調(diào)整一下代碼,將placeHolder 放在某個作用域里,只要執(zhí)行走出這個作用域,就意味著placeHolder 為無用的局部變量,后續(xù)新分配的a就會直接復(fù)用局部變量表的空間:

public static void main(String[] args) {
        {
            //placeHolder在代碼塊的作用域內(nèi)完成內(nèi)存分配
            byte[] placeHolder = new byte[1024 * 1024 * 64];
        }
        //分配一個新的變量嘗試復(fù)用上述slot
        int a = 0;
        System.gc();
    }

這也就是為什么本次gc可以回收64M的內(nèi)存空間的原因:

[GC (System.gc())  86054K->68502K(243712K), 0.0023594 secs]
[Full GC (System.gc())  68502K->2707K(243712K), 0.0221691 secs]

小結(jié)一下虛擬棧的特點:

  • 是方法執(zhí)行時的內(nèi)存模型。
  • 方法調(diào)用以棧幀形式壓入棧中。
  • 方法嵌套調(diào)用并將棧幀壓入棧幀時,深度操作虛擬機棧最大深度會報StackOverflowError。
  • 虛擬機棧的局部變量表隨著變量使用的完結(jié),之前的內(nèi)存區(qū)域可被復(fù)用。
  • 棧的生命周期跟隨線程,線程調(diào)用結(jié)束棧即可被銷毀。

本地方法棧

下面這個帶有native關(guān)鍵字的方法就是在本地方法,它就是本地方法棧管理的方法,其工作機制和特點是虛擬機棧是差不多的,所以這里就不多做介紹了。

private native void start0();

(5) 程序計數(shù)器

程序計數(shù)器和我們操作系統(tǒng)學(xué)習(xí)的程序計數(shù)器概念差不多,是一塊比較小的內(nèi)存空間,我們可以將其看作當(dāng)前現(xiàn)場所執(zhí)行的字節(jié)碼行號的指示器,記錄著當(dāng)前線程下一條要執(zhí)行的指令的地址,對于程序中的分支、循環(huán)、跳轉(zhuǎn)、異常以及線程恢復(fù)和掛起都是基于這個計數(shù)器完成的。

我們以下面這段代碼為例展示一下程序計數(shù)器實質(zhì)記錄的信息:

public static void main(String[] args) {

        int num = 1;
        int num2 = 2;
        int num3 = 3;
        System.out.println("total: " + (num + num2 + num3));
    }

可以看到實際上其編譯后的字節(jié)碼內(nèi)容如上,每一行指令前方所記錄的字節(jié)碼的偏移地址就是程序計數(shù)器所記錄的地址信息:

因為是每一個線程都有各自的計數(shù)器,所以我們可以認(rèn)為計數(shù)器是不會互相影響是線程安全的。需要注意的是程序計數(shù)器只有在記錄虛擬機棧的方法時才會有值,對于native方法,程序計數(shù)器是不工作的。

二、詳解JVM類加載器

1.什么是類加載器

類加載器實現(xiàn)將編譯后的class文件加載到內(nèi)存,并轉(zhuǎn)為為運行時區(qū)域劃分的運行時數(shù)據(jù)結(jié)構(gòu),注意類加載器只能決定類的加載,至于能不能運行則是由Execution Engine 來決定。

整體來說,類加載器對應(yīng)類的生命周期應(yīng)該是以下幾個階段:

  • 加載
  • 鏈接:分為驗證、準(zhǔn)備、解析
  • 初始化
  • 使用:此時用戶就可以基于這個類創(chuàng)建實例了
  • 卸載

2.類的加載

加載的過程本質(zhì)上就是將class文件加載到JVM中,JVM根據(jù)類的全限定名獲取定義該類的二進制字節(jié)流。

  • 將編譯后class文件加載到內(nèi)存。
  • 將靜態(tài)數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換成方法區(qū)中運行時數(shù)據(jù)結(jié)構(gòu)。
  • 在堆區(qū)創(chuàng)建一個java.lang.Class對象作為數(shù)據(jù)訪問的入口。

3.鏈接的過程

鏈接整體是分為上述所說的3個過程:

  • 驗證:分為驗證階段主要是校驗類的文件格式、元數(shù)據(jù)、字節(jié)碼、二進制兼容性
  • 準(zhǔn)備:在方法區(qū)為靜態(tài)變量常見空間,并對其進行初始化,例如private static int a=3;,在此階段就會在方法區(qū)完成創(chuàng)建,并初始默認(rèn)值0。
  • 解析:即將類的符號引用直接轉(zhuǎn)換為直接引用,引用包括但不限于類、接口、字段、類方法、接口方法、方法類型、方法句柄、發(fā)文控制修飾符等,例如import java.util.ArrayList在此階段就會直接轉(zhuǎn)為指針或者對象地址。

4.初始化

將方法區(qū)中準(zhǔn)備好的值,通過調(diào)用<cinit>完成初始化工作。<cinit>會收集好所有的賦值動作,例如上文的private static int a=3就是這時候完成賦值的。

5.卸載

當(dāng)對象使用完成后,GC將無用對象從內(nèi)存中卸載。

6.類加載器的加載順序

其實類加載器并非只有一個,按照分類我們可以將其分為:

BootStrap ClassLoader:rt.jar
Extention ClassLoader: 加載擴展的jar包
App ClassLoader:指定的classpath下面的jar包
Custom ClassLoader:自定義的類加載器

所以,為了保證JDK自帶rt.jar的類能夠正常加載,就出現(xiàn)了一種名為雙親委派的類加載機制。

舉個例子,JDK自帶的包中有一個名為String的類,而我們自定義的代碼中也有一個String類,我們自己的類肯定是由App ClassLoader完成加載,如果我們的類加載器優(yōu)先執(zhí)行,那么JDK自帶的String類就無法被使用到。

所以雙親委派機制就規(guī)定了類加載優(yōu)先由BootStrap ClassLoader先加載,只有根加載器加載不到需要的類,才會交由下層類完成加載。 正是因為雙親委派機制的存在,jdk自帶的String類才能夠正常的使用,而我們也無法通過自定義String類進行重寫。

類加載器的工作流程為:

  • 加載class文件到方法區(qū)并轉(zhuǎn)為運行時數(shù)據(jù)結(jié)構(gòu),并在堆區(qū)創(chuàng)建一個Class對象作為入口
  • 驗證class的類方法是否由危害JVM的行為
  • 準(zhǔn)備階段初始化靜態(tài)變量數(shù)據(jù)
  • 解析階段將符號引用轉(zhuǎn)為可以可直接導(dǎo)向?qū)ο蟮刂返闹苯右?/li>
  • 初始化階段通過cinit方法初始化對象實例變量等數(shù)據(jù)
  • 使用完成后該類就會被卸載。

7.用一個線程的代碼執(zhí)行解釋Java文件是如何被運行的

如下所示,我們編寫一個Student 類,他有name這個成員屬性:

/**
 * 學(xué)生類
 */
public class Student {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

然后我們編寫一個main方法,調(diào)用student類,完成屬性賦值。

public class Main {

    public static void main(String[] args) throws InterruptedException {
        Student student = new Student();
        student.setName("小明");
    }

   }

首先編譯得到Main.class文件后,系統(tǒng)會啟動一個JVM進程,從classpath中找到這個class的二進制文件,將在到方法區(qū)的運行時數(shù)據(jù)區(qū)域:

再將當(dāng)前執(zhí)行的main方法壓入虛擬機棧中:

main方法中需要new Student();,JVM發(fā)現(xiàn)方法區(qū)中沒有Student類的信息,于是開始加載這個類,將這個類的信息存放到方法區(qū),并在堆區(qū)創(chuàng)建一個Class對象作為方法區(qū)信息的入口。

new Student();在此時就會根據(jù)類元信息獲取創(chuàng)建student對象所需要的空間大小,在堆區(qū)申請并開辟一個空間調(diào)用構(gòu)造函數(shù)創(chuàng)建Student實例。

main方法調(diào)用setName,student 的引用找到堆區(qū)的Student,通過其引用找到方法區(qū)中Student 類的方法表得到方法的字節(jié)碼地址,從而完成調(diào)用。

上述步驟完成后,方法按照入棧順序后進先出的彈出,虛擬機棧隨著線程一起銷毀。

三、詳解虛擬機堆

1.區(qū)域劃分

JVM將堆內(nèi)存分為年輕代和老年代。以及一個非堆內(nèi)存區(qū)域,我們稱為永久代,注意:這里所說的永久代只有在JDK8之前才會出現(xiàn),對此我們也給出JDK8之前版本的堆內(nèi)存區(qū)域劃分圖解:

在Java8之后考慮到與其他規(guī)范的虛擬機的兼容性以及GC效率,將方法區(qū)的實現(xiàn)交由元空間實現(xiàn),元空間所使用的內(nèi)存都是本地內(nèi)存,這里的本地內(nèi)存說的就是我們物理機上的內(nèi)存,所以理論上物理機內(nèi)存多大,元空間內(nèi)存就可以分配多大,元空間大小分配和JVM從物理機上分配的內(nèi)存大小沒有任何關(guān)系。

對應(yīng)的我們也給出元空間兩個設(shè)置參數(shù):

  • MetaspaceSize:初始化元空間大小,控制發(fā)生GC
  • MaxMetaspaceSize:限制元空間大小上限,防止占用過多物理內(nèi)存。

2.詳解新生代

我們再來聊聊年輕代,新生代又可以分為Eden和Survivor區(qū),Survivor區(qū)又被平均分為兩塊。所以年代整體比例為8:1:1。當(dāng)然這個值也可以通過-XX:+UsePSAdaptiveSurvivorSizePolicy來調(diào)整。

任何對象剛剛創(chuàng)建的時候都會放在Eden區(qū)。我們都知道堆區(qū)內(nèi)存是共享的,所以Eden區(qū)的空間也是多線程共享的,但是為了確保多線程彼此之間相對獨立(注意是線程之間彼此獨立而不是操作Eden區(qū)對象獨立),Eden區(qū)會專門劃出一塊連續(xù)的空間給每個線程分配一個獨立空間,這個空間叫做TLAB空間,每個線程都可以操作自己的TLAB空間和讀取其他線程的TLAB空間。

一旦Eden區(qū)滿了之后,就會觸發(fā)第一次Minor GC,就會將存活的對象從Eden區(qū)放到Survivor區(qū)。

需要注意的是,Survivor分為Survivor0和Survivor1區(qū)。JVM使用from和to兩個指針管理這兩塊區(qū)域,其中from指針指向有對象的區(qū)域空間,to指針指向空閑區(qū)域的Survivor空間。

從Eden區(qū)中存活下來首先會在Survivor0區(qū),一旦下一次Eden區(qū)空間滿了之后就再次觸發(fā)Minor GC 將Eden區(qū)和Survivor0區(qū)存活的對象復(fù)制到Survivor1區(qū),就這樣保存存活的對象在兩個Survivor區(qū)中來回游走,直到晉升到老年代:

經(jīng)過15次之后還活著的對象就會被存放到老年代,這里是15是由-XX:MaxTenuringThreshold指定的,-XX:MaxTenuringThreshold 占4位,默認(rèn)配置為15。 這里補充一下,同樣會將Survivor存放到老年代的第2個條件,當(dāng)Survivor區(qū)對象比例達到XX:TargetSurvivorRatio時,也會將存活的對象放到老年區(qū)。

3.詳解老年代

老年代存放的都是經(jīng)歷過無數(shù)次GC的老對象,一旦這個空間滿了之后就會出現(xiàn)一次Full GC,F(xiàn)ull GC期間所有線程都會停止手頭工作等待Full GC完成,所以在此期間,系統(tǒng)可能會出現(xiàn)卡頓現(xiàn)象。 這就意味著在高并發(fā)多對象創(chuàng)建場景的情況下,我們需要合理分配老年區(qū)的內(nèi)存。一旦Full GC后還是無法容納新對象,就會報OOM問題。

四、JVM如何判斷對象是否需要被銷毀

1.引用計數(shù)器法

一個對象被引用時+1,被解除引用時-1。我們根據(jù)引用計數(shù)結(jié)果決定是否GC,但是這種方式無法解決兩個對象互相引用的情況。例如我們棧區(qū)沒有一個引用指向當(dāng)前兩個對象,可堆區(qū)兩個對象卻互相引用對方。

2.可達性分析法

將一系列的GC ROOTS作為起始的存活對象集,查看是否有任意一個GC ROOTS可以到達這個對象,都不可達就說明這個對象要被回收了。

而以下幾種可以作為GC ROOTS:

  • 虛擬機棧中的局部變量等,被該變量引用的對象不可回收。
  • 方法區(qū)的靜態(tài)變量,被該變量引用的對象不可回收。
  • 方法區(qū)的常量,被該變量引用的對象不可回收。
  • 本地方法棧(即native修飾的方法),被該變量引用的對象不可回收。
  • 未停止且正在使用該對象的線程,被該線程引用的對象不可回收。

通過可達性算法分析對象是否被回收需要經(jīng)過兩個階段:

  • 可達性分析法發(fā)現(xiàn)不可達的對象后,就將其第一次標(biāo)記一下,然后判斷該對象的是否要執(zhí)行finalize()方法,若確定則將其存到F-Queue中。
  • 將F-Queue中的對象調(diào)用finalize(),若此時還是沒有任何引用鏈引用,則說明這個對象要被回收了。

五、詳解幾種常見垃圾回收算法

1.標(biāo)記清除法

如下圖,這種算法很簡單,標(biāo)記出需要被回收的對象的空間,然后直接清除。同樣的缺點也很明顯,容易造成內(nèi)存碎片,內(nèi)存碎片也很好理解,回收的對象空間都是一小塊一小塊的,當(dāng)我們需要創(chuàng)建一個大對象時就沒有一塊連續(xù)大空間供其使用。

2.復(fù)制算法

這種算法和上文說的survivor一樣,將空間一分為二,from存放當(dāng)前活著的對象,to作為空閑空間。在進行回收時,將沒有被標(biāo)記回收的對象挪到另一個空間,然后from指向另一個空間。這種算法缺點也很明顯,可利用空間就一半。

3.標(biāo)記整理

這種算法算是復(fù)制算法的改良版,將存活對象全部挪動到一段,確??臻e和對象空間都是連續(xù)的,且空間利用率100%。

4.分代收集算法(綜合算法)

這種算法就是上面算法的組合,即年輕代存活率低,采用復(fù)制算法。老年代存活率高,采用標(biāo)記清除算法或者標(biāo)記整理算法。例如hotspot虛擬機的搭配就是新生代采用復(fù)制算法,每次觸發(fā)Minor gc就將Eden和survivor區(qū)存活的對象移動到to指針指向的survivor區(qū),而老年代而用標(biāo)記整理法將存活的對象都?xì)w整到同一個段中:


責(zé)任編輯:趙寧寧 來源: 寫代碼的SharkChili
相關(guān)推薦

2024-11-27 16:25:54

JVMJIT編譯機制

2023-11-16 08:53:05

NumPy庫Python

2015-11-17 16:11:07

Code Review

2018-04-18 07:01:59

Docker容器虛擬機

2019-01-18 12:39:45

云計算PaaS公有云

2020-07-02 15:32:23

Kubernetes容器架構(gòu)

2017-03-14 14:04:24

Python機器學(xué)習(xí)

2010-05-26 17:35:08

配置Xcode SVN

2018-09-14 17:16:22

云計算軟件計算機網(wǎng)絡(luò)

2024-05-15 14:29:45

2024-12-13 16:03:59

2024-11-28 10:35:47

2024-04-10 07:48:41

搜索引擎場景

2015-10-15 14:16:24

2011-04-06 15:55:50

開發(fā)webOS程序webOS

2023-11-21 08:57:16

2017-02-10 09:30:33

數(shù)據(jù)化運營流量

2010-02-22 09:39:52

HTML 5Web

2011-05-24 13:37:16

jQueryAjax

2015-09-18 10:09:05

Swift
點贊
收藏

51CTO技術(shù)棧公眾號