JAVA JVM詳解

一. JVM

JVM是Java Virtual Machine(Java虛擬機(jī))的縮寫,也就是指的JVM虛擬機(jī),是一種用于計(jì)算設(shè)備的規(guī)范,它是一個(gè)虛構(gòu)出來的計(jì)算機(jī),是通過在實(shí)際的計(jì)算機(jī)上仿真模擬各種計(jì)算機(jī)功能來實(shí)現(xiàn)的。
總所周知,java語(yǔ)言是跨平臺(tái)的,而JVM是java跨平臺(tái)的關(guān)鍵之所在。JVM上執(zhí)行java字節(jié)碼,執(zhí)行時(shí)這些字節(jié)碼可以解釋成具體平臺(tái)的機(jī)器碼,因此java擁有“一次編譯,處處運(yùn)行”這一跨平臺(tái)能力。

二、JRE、JDK和JVM的關(guān)系

JRE(Java Runtime Environment, Java運(yùn)行環(huán)境)是Java平臺(tái),所有的程序都要在JRE下才能夠運(yùn)行。包括JVM和Java核心類庫(kù)和支持文件。

JDK(Java Development Kit,Java開發(fā)工具包)是用來編譯、調(diào)試Java程序的開發(fā)工具包。包括Java工具(javac/java/jdb等)和Java基礎(chǔ)的類庫(kù)(java API )。

JVM(Java Virtual Machine, Java虛擬機(jī))是JRE的一部分。JVM主要工作是解釋自己的指令集(即字節(jié)碼)并映射到本地的CPU指令集和OS的系統(tǒng)調(diào)用。Java語(yǔ)言是跨平臺(tái)運(yùn)行的,不同的操作系統(tǒng)會(huì)有不同的JVM映射規(guī)則,使之與操作系統(tǒng)無關(guān),完成跨平臺(tái)性。

有兩個(gè)概念和JVM息息相關(guān)并且很容易搞混,那就是JRE和JDK。其中JRE(JavaRuntimeEnvironment,Java運(yùn)行環(huán)境),指的是Java平臺(tái)。所有的Java 程序都要在JRE下才能運(yùn)行。普通用戶運(yùn)行已開發(fā)好的java程序,只要安裝JRE即可。而JDK(JavaDevelopmentKit)是程序開發(fā)者用來編譯、調(diào)試java程序用的開發(fā)工具包。JDK工具包里面的工具也是Java寫的程序,因此也需要JRE才能運(yùn)行。為了保持JDK的獨(dú)立性和完整性,在JDK的安裝過程中,JRE也是安裝的一部分。所以,在JDK的安裝目錄下有一個(gè)名為jre的目錄,用于存放JRE文件。而JVM是JRE的一部分。JVM有自己完善的硬件架構(gòu),如處理器、堆棧、寄存器等,還具有相應(yīng)的指令系統(tǒng)。Java語(yǔ)言最重要的特點(diǎn)就是跨平臺(tái)運(yùn)行。使用JVM就是為了支持與操作系統(tǒng)無關(guān),實(shí)現(xiàn)跨平臺(tái)。使用JDK(調(diào)用JAVA API)開發(fā)JAVA程序后,通過JDK中的編譯程序(javac)將Java程序編譯為Java字節(jié)碼,在JRE上運(yùn)行這些字節(jié)碼,JVM會(huì)解析并映射到真實(shí)操作系統(tǒng)的CPU指令集和OS的系統(tǒng)調(diào)用。

從上面我們可以看出java運(yùn)行主要分幾個(gè)步驟:

  • 1、java源碼編譯。
  • 2、類加載。
  • 3、類執(zhí)行。

三. java源碼編譯

所謂”編譯“,通俗來講就是把我們寫的代碼“翻譯“成機(jī)器可以讀懂的機(jī)器碼。Java 技術(shù)中的編譯器可以分為如下兩類:

  • 前端編譯器:把 *.java 文件轉(zhuǎn)變?yōu)?*.class 文件的過程。比如 JDK 的 Javac。

  • 后端編譯器(兩類):

    1. 即時(shí)編譯器:Just In Time Compiler,常稱 JIT 編譯器,在「運(yùn)行期」把字節(jié)碼轉(zhuǎn)變?yōu)楸镜貦C(jī)器碼的過程。比如 HotSpot VM 的 C1、C2 編譯器,Graal 編譯器。

    2.提前編譯器:Ahead Of Time Compiler,常稱 AOT 編譯器,直接把程序編譯成與目標(biāo)機(jī)器指令集相關(guān)的二進(jìn)制代碼的過程。比如 JDK 的 Jaotc,GNU Compiler for the Java。

我們可以把將.java文件編譯成.class的編譯過程稱之為前端編譯。把將.class文件翻譯成機(jī)器指令的編譯過程稱之為后端編譯。

Javac編譯器對(duì)代碼的運(yùn)行效率幾乎沒做什么優(yōu)化,虛擬機(jī)設(shè)計(jì)者把對(duì)代碼性能的優(yōu)化集中到了后端的JIT編譯器中。之所以這樣設(shè)計(jì),因?yàn)镃lass文件擁有虛擬機(jī)規(guī)范嚴(yán)格定義的通用格式,只要符合Class文件格式,就可以被虛擬機(jī)正確加載,因此不只是Java語(yǔ)言,其他如JRuby、Groovy、Kotlin等語(yǔ)言也可以被編譯成Class文件。但不同語(yǔ)言使用的前端編譯器(將源碼文件編譯成Class文件)可能是不同的,故將優(yōu)化過程放到即時(shí)編譯器過程,可以讓不同語(yǔ)言的字節(jié)碼都能享受到性能優(yōu)化的好處。Javac編譯器本身是由Java語(yǔ)言編寫的,Javac編譯器針對(duì)程序編碼過程做了很多優(yōu)化措施,目的是改善程序員的編碼風(fēng)格和提高編碼效率。

1、前端編譯

把Java源碼文件(.java)編譯成Class文件(.class)的過程;也即把滿足Java語(yǔ)言規(guī)范的程序轉(zhuǎn)化為滿足JVM規(guī)范所要求格式的能力,稱為前端編譯。前端編譯階段中,最重要的一個(gè)編譯器就是javac 編譯器, 在命令行執(zhí)行javac命令,其實(shí)本質(zhì)是運(yùn)行了javac.exe這個(gè)應(yīng)用。Android工程師可能對(duì)于 Gradle構(gòu)建過程更為熟悉,構(gòu)建過程中有一個(gè)Task:compileDebugJavaWithJavac,其實(shí)也用到了javac 編譯器,編譯中間產(chǎn)物路徑在build/intermediates/javac/debug/classes。

優(yōu)點(diǎn):

  1. 這階段的優(yōu)化是指程序編碼方面的;
  2. 許多Java語(yǔ)法新特性("語(yǔ)法糖":泛型、內(nèi)部類等等),是靠前端編譯器實(shí)現(xiàn)的。
  3. 編譯成的Class文件可以直接給JVM解釋器解釋執(zhí)行,省去編譯時(shí)間,加快啟動(dòng)速度;

缺點(diǎn):

  1. 對(duì)代碼運(yùn)行效率幾乎沒有任何優(yōu)化措施;
  2. 解釋執(zhí)行效率較低,所以需要結(jié)合下面的JIT編譯;

Javac編譯的基本流程

  • 準(zhǔn)備過程
    初始化插入式注解處理器。

  • 解析與填充符號(hào)表

    1. 語(yǔ)法、詞法分析
      詞法分析將源代碼的字符轉(zhuǎn)成標(biāo)記(Token)集合,單個(gè)字符是程序編寫的最小單位,而標(biāo)記則是編譯過程的最小單位。如“int a = b + 2”這句代碼可拆分為int、a、=、b、+、2共6個(gè)標(biāo)記。
      語(yǔ)法分析是根據(jù)Token序列構(gòu)造抽象語(yǔ)法樹(AST,Abstract Syntax Tree)的過程。AST是一種用來描述程序代碼語(yǔ)法結(jié)構(gòu)的樹形表示形式,語(yǔ)法樹的每一個(gè)節(jié)點(diǎn)都代表著程序代碼中的一個(gè)語(yǔ)法結(jié)構(gòu),如包、類型、修飾符、運(yùn)算符、接口、返回值、代碼注釋等。抽象語(yǔ)法樹建立之后,編譯器基本不會(huì)再對(duì)源碼文件進(jìn)行操作了,后續(xù)的操作都建立在抽象語(yǔ)法樹之上。
    2. 填充符號(hào)表
      符號(hào)表(Symbol Table)是由一組符號(hào)地址和符號(hào)信息構(gòu)成的表格,其中保存的信息在編譯的不同階段都要用到。以下是符號(hào)表的兩個(gè)應(yīng)用場(chǎng)景:
      1. 在語(yǔ)義分析中,符號(hào)表登記的內(nèi)容將用于語(yǔ)義檢查和產(chǎn)生中間代碼。
      2. 在目標(biāo)代碼生成階段,當(dāng)對(duì)符號(hào)名進(jìn)行地址分配時(shí),符號(hào)表是地址分配的依據(jù)。
  • 注解處理器

    1. JDK1.5之后,Java語(yǔ)言提供了對(duì)注解的支持。
    2. JDK1.6中提供了一組插入式注解處理器的標(biāo)準(zhǔn)API,支持在編譯期間對(duì)注解進(jìn)行處理。
    3. 注解處理器可將其看做編譯器的插件,在這些插件里面,可以讀取、修改、添加抽象語(yǔ)法樹中的任意元素,如果這些插件在處理注解期間對(duì)語(yǔ)法樹進(jìn)行了修改,編譯器將回到解析及填充符號(hào)表的過程重新處理,直到所有插入式注解處理器都沒有再對(duì)語(yǔ)法樹進(jìn)行修改為止。
    4. 有了編譯器注解處理的標(biāo)準(zhǔn)API支持,我們的代碼才有可能干涉編譯器的行為。
  • 語(yǔ)義分析
    語(yǔ)義分析的任務(wù)是對(duì)結(jié)構(gòu)上正確的源程序進(jìn)行上下文有關(guān)性質(zhì)的審查,因?yàn)槌橄笳Z(yǔ)法樹雖然能表示一個(gè)結(jié)構(gòu)正確的源程序的抽象,但無法保證源程序是符合邏輯的。

  • 標(biāo)注檢查
    標(biāo)注檢查步驟檢查的內(nèi)容如變量使用前是否已經(jīng)被聲明、變量與賦值之間的數(shù)據(jù)類型是否匹配等。

  • 數(shù)據(jù)及控制流分析
    數(shù)據(jù)及控制流分析是對(duì)程序上下文邏輯更進(jìn)一步的驗(yàn)證,可以檢查出諸如程序局部變量是否在使用前有賦值、方法的每條路徑是否都有返回值、是否所有的受檢異常都被正確處理等。

  • 解語(yǔ)法糖
    所謂語(yǔ)法糖,指在計(jì)算機(jī)語(yǔ)言中添加某種語(yǔ)法,只是為了更方便程序員使用,如提高編碼效率或減少出錯(cuò),但對(duì)語(yǔ)言功能沒有影響。
    所謂解語(yǔ)法糖(desugar),是指在編譯階段將糖衣語(yǔ)法還原回簡(jiǎn)單的基礎(chǔ)語(yǔ)法結(jié)構(gòu),因?yàn)樘摂M機(jī)運(yùn)行時(shí)不支持這些語(yǔ)法。

    1. Java中常見的語(yǔ)法糖有:
      1. 泛型(JDK 1.5添加)——Java中的泛型其實(shí)是偽泛型,編譯后就會(huì)被替換為原生類型了,并在相應(yīng)的地方插入了強(qiáng)制轉(zhuǎn)型代碼。因此Java中的泛型實(shí)現(xiàn)方法也被稱為類型擦除。
      2. 自動(dòng)裝箱、拆箱——編譯后被轉(zhuǎn)化成了對(duì)應(yīng)的包裝和還原方法。
      3. 循環(huán)遍歷——編譯后代碼被轉(zhuǎn)成了迭代器實(shí)現(xiàn),這也是被遍歷的類需要實(shí)現(xiàn)Iterable接口的原因。
      4. 變長(zhǎng)參數(shù)——編譯后實(shí)際上被轉(zhuǎn)成數(shù)組類型的參數(shù)。
    2. Java中常見的其他語(yǔ)法糖:
      1. 其他語(yǔ)法糖還有內(nèi)部類、枚舉類、斷言語(yǔ)句、對(duì)枚舉和字符串的switch支持(JDK1.7)等,可以通過跟蹤Javac源碼、反編譯Class文件等方式了解它們的實(shí)現(xiàn)本質(zhì)。
  • 字節(jié)碼生成
    字節(jié)碼生成是Javac編譯過程的最后一個(gè)階段,字節(jié)碼生成階段不僅僅是把前面各個(gè)步驟生成的信息(AST、符號(hào)表)轉(zhuǎn)化成字節(jié)碼寫到磁盤中,編譯器還進(jìn)行了少量的代碼添加和轉(zhuǎn)換工作。完成了了對(duì)語(yǔ)法樹的遍歷和調(diào)整之后,就會(huì)把填充了所有所需信息的符號(hào)表交給com.sun.tools.javac.jvm.ClassWriter類,由這個(gè)類的writeClass()方法輸出字節(jié)碼,生成最終的Class文件,到此Javac的編譯過程結(jié)束。

Javac前端編譯器
Oracle javac、Eclipse JDT中的增量式編譯器(ECJ)等。

2、即時(shí)(JIT)編譯

根據(jù)前面的內(nèi)容,我們知道編譯前端的核心編譯產(chǎn)物是:Class 文件。但是對(duì)于CPU來說,它是不認(rèn)得字節(jié)碼的。每種CPU只能“讀懂”自身支持的機(jī)器語(yǔ)言或者本地代碼(native code)。因此,Java 虛擬機(jī)在執(zhí)行字節(jié)碼時(shí),需要將字節(jié)碼翻譯為當(dāng)前平臺(tái)的本地代碼,可以分為:解釋執(zhí)行 & 編譯執(zhí)行。

  • 解釋執(zhí)行
    解釋執(zhí)行,就像python一樣,代碼運(yùn)行到哪里,就把代碼解釋到哪里。這么做的優(yōu)點(diǎn)和缺點(diǎn)都很明顯。
    • 解釋執(zhí)行優(yōu)點(diǎn)

      1. 方便更新。代碼可以在程序執(zhí)行的過程中修改.
      2. 啟動(dòng)快。拿到代碼就可以跑,沒有其他多余操作。
      3. 平臺(tái)無關(guān)。所有操作都基于jvm,全平臺(tái)通用。
    • 解釋執(zhí)行缺點(diǎn)

      1. 平臺(tái)效率低。由于程序執(zhí)行性能只依賴jvm,導(dǎo)致不同平臺(tái)特有的優(yōu)化無法發(fā)揮。
      2. 代碼效率低。無法對(duì)代碼動(dòng)態(tài)優(yōu)化,只能拿到什么執(zhí)行什么。

JVM 通過字節(jié)碼解釋器將其翻譯成對(duì)應(yīng)的機(jī)器指令,逐條讀入,逐條解釋翻譯。很顯然,經(jīng)過解釋執(zhí)行,其執(zhí)行速度必然會(huì)比可執(zhí)行的二進(jìn)制字節(jié)碼程序慢很多。這就是傳統(tǒng)的JVM的解釋器(Interpreter)的功能。為了解決這種效率問題,引入了 JIT 技術(shù)。JIT 技術(shù)指JAVA程序還是通過解釋器進(jìn)行解釋執(zhí)行,當(dāng)JVM發(fā)現(xiàn)某個(gè)方法或代碼塊運(yùn)行特別頻繁的時(shí)候,就會(huì)認(rèn)為這是“熱點(diǎn)代碼”(Hot Spot Code)。然后JIT會(huì)把部分“熱點(diǎn)代碼”翻譯成本地機(jī)器相關(guān)的機(jī)器碼,并進(jìn)行優(yōu)化,然后再把翻譯后的機(jī)器碼緩存起來,以備下次使用。

通過Java虛擬機(jī)(JVM)內(nèi)置的即時(shí)編譯器(Just In Time Compiler,JIT編譯器);在運(yùn)行時(shí)把Class文件字節(jié)碼編譯成本地機(jī)器碼的過程;
優(yōu)點(diǎn):

  1. 通過在運(yùn)行時(shí)收集監(jiān)控信息,把"熱點(diǎn)代碼"(Hot Spot Code)編譯成與本地平臺(tái)相關(guān)的機(jī)器碼,并進(jìn)行各種層次的優(yōu)化;
  2. 可以大大提高執(zhí)行效率;

缺點(diǎn):

  1. 收集監(jiān)控信息影響程序運(yùn)行;
  2. 編譯過程占用程序運(yùn)行時(shí)間(如使得啟動(dòng)速度變慢);
  3. 編譯機(jī)器碼占用內(nèi)存;
  4. JIT編譯器:HotSpot虛擬機(jī)的C1、C2編譯器等;

那么又一個(gè)問題出現(xiàn)了,既然實(shí)時(shí)編譯慢,那為什么不將代碼全部進(jìn)行JIT預(yù)編譯后再扔到機(jī)器上去跑呢,這樣就沒有這些問題了,只是預(yù)編譯時(shí)間長(zhǎng)而已。這個(gè)思路其實(shí)就是c和c++的做法,直接在不同機(jī)器上編譯出不同優(yōu)化方向的代碼,但是這樣導(dǎo)致了編譯后的代碼無法跨平臺(tái)執(zhí)行,而jvm的最大特性就是跨平臺(tái),所以這是不可行的。

注意,JIT編譯速度及編譯結(jié)果的優(yōu)劣,是衡量一個(gè)JVM性能的很重要指標(biāo);所以對(duì)程序運(yùn)行性能優(yōu)化集中到這個(gè)階段;也就是說可以對(duì)這個(gè)階段進(jìn)行JVM調(diào)優(yōu);

3、靜態(tài)提前編譯(Ahead Of Time,AOT編譯)

程序運(yùn)行前,直接把Java源碼文件(.java)編譯成本地機(jī)器碼的過程;
優(yōu)點(diǎn):

  1. 編譯不占用運(yùn)行時(shí)間,可以做一些較耗時(shí)的優(yōu)化,并可加快程序啟動(dòng);
  2. 把編譯的本地機(jī)器碼保存磁盤,不占用內(nèi)存,并可多次使用;

缺點(diǎn):

  1. 因?yàn)镴ava語(yǔ)言的動(dòng)態(tài)性(如反射)帶來了額外的復(fù)雜性,影響了靜態(tài)編譯代碼的質(zhì)量;
  2. 一般靜態(tài)編譯不如JIT編譯的質(zhì)量,這種方式用得比較少;
  3. 犧牲Java的一致性

靜態(tài)提前編譯器(AOT編譯器):JAOTC、GCJ、Excelsior JET、ART (Android Runtime)等;

4、前端編譯+JIT編譯

到這里,我們知道目前Java體系中主要還是采用前端編譯+JIT編譯的方式,如JDK中的HotSpot虛擬機(jī)。
前端編譯+JIT編譯方式的運(yùn)作過程大體如下:

  1. 首先通過前端編譯把符合Java語(yǔ)言規(guī)范的程序代碼轉(zhuǎn)化為滿足JVM規(guī)范所要求Class格式;
  2. 然后程序啟動(dòng)時(shí)Class格式文件發(fā)揮作用,解釋執(zhí)行,省去編譯時(shí)間,加快啟動(dòng)速度;
  3. 針對(duì)Class解釋執(zhí)行效率低的問題,在運(yùn)行中收集性能監(jiān)控信息,得知"熱點(diǎn)代碼";
  4. JIT逐漸發(fā)揮作用,把越來越多的熱點(diǎn)代碼"編譯優(yōu)化成本地代碼,提高執(zhí)行效率;

四. 類加載機(jī)制

".java"文件經(jīng)過Java編譯器編譯成拓展名為".class"的文件,".class"文件中保存著Java代碼經(jīng)轉(zhuǎn)換后的虛擬機(jī)指令,當(dāng)需要使用某個(gè)類時(shí),虛擬機(jī)將會(huì)加載它的".class"文件,并創(chuàng)建對(duì)應(yīng)的class對(duì)象,將class文件加載到虛擬機(jī)的內(nèi)存,這個(gè)過程稱為類加載。舉個(gè)通俗點(diǎn)的例子來說,JVM在執(zhí)行某段代碼時(shí),遇到了class A, 然而此時(shí)內(nèi)存中并沒有class A的相關(guān)信息,于是JVM就會(huì)到相應(yīng)的class文件中去尋找class A的類信息,并加載進(jìn)內(nèi)存中,這就是我們所說的類加載過程。

由此可見,JVM不是一開始就把所有的類都加載進(jìn)內(nèi)存中,而是只有第一次遇到某個(gè)需要運(yùn)行的類時(shí)才會(huì)加載,且只加載一次。

從類被加載到虛擬機(jī)內(nèi)存中開始,到卸御出內(nèi)存為止,它的整個(gè)生命周期分為7個(gè)階段:加載(Loading)、驗(yàn)證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸載(Unloading)。其中驗(yàn)證、準(zhǔn)備、解析三個(gè)部分統(tǒng)稱為連接。

1.(裝載) 加載

類的裝載指的是將類的.class文件中的二進(jìn)制數(shù)據(jù)讀入到內(nèi)存中,將其放在運(yùn)行時(shí)數(shù)據(jù)區(qū)的方法區(qū)內(nèi),然后在堆區(qū)創(chuàng)建一個(gè)java.lang.Class對(duì)象,用來封裝類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)。類的加載的最終產(chǎn)品是位于堆區(qū)中的Class對(duì)象,Class對(duì)象封裝了類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu),并且向Java程序員提供了訪問方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)的接口。

類加載器并不需要等到某個(gè)類被“首次主動(dòng)使用”時(shí)再加載它,JVM規(guī)范允許類加載器在預(yù)料某個(gè)類將要被使用時(shí)就預(yù)先加載它,如果在預(yù)先加載的過程中遇到了.class文件缺失或存在錯(cuò)誤,類加載器必須在程序首次主動(dòng)使用該類時(shí)才報(bào)告錯(cuò)誤(LinkageError錯(cuò)誤)如果這個(gè)類一直沒有被程序主動(dòng)使用,那么類加載器就不會(huì)報(bào)告錯(cuò)誤。

相對(duì)于類加載的其他階段而言,加載階段(準(zhǔn)確地說,是加載階段獲取類的二進(jìn)制字節(jié)流的動(dòng)作)是可控性最強(qiáng)的階段,因?yàn)殚_發(fā)人員既可以使用系統(tǒng)提供的類加載器來完成加載,也可以自定義自己的類加載器來完成加載。

加載階段完成后,虛擬機(jī)外部的二進(jìn)制字節(jié)流就按照虛擬機(jī)所需的格式存儲(chǔ)在方法區(qū)之中,而且在Java堆中也創(chuàng)建一個(gè)java.lang.Class類的對(duì)象,這樣便可以通過該對(duì)象訪問方法區(qū)中的這些數(shù)據(jù)。

加載.class文件的來源方式:

  • 從本地系統(tǒng)中直接加載
  • 通過網(wǎng)絡(luò)下載.class文件
  • 從zip,jar等歸檔文件中加載.class文件
  • 從專有數(shù)據(jù)庫(kù)中提取.class文件
  • 將Java源文件動(dòng)態(tài)編譯為.class文件
2. 連接
  • 驗(yàn)證
    驗(yàn)證的目的是為了確保Class文件中的字節(jié)流包含的信息符合當(dāng)前虛擬機(jī)的要求,而且不會(huì)危害虛擬機(jī)自身的安全。不同的虛擬機(jī)對(duì)類驗(yàn)證的實(shí)現(xiàn)可能會(huì)有所不同,但大致都會(huì)完成以下四個(gè)階段的驗(yàn)證:文件格式的驗(yàn)證、元數(shù)據(jù)的驗(yàn)證、字節(jié)碼驗(yàn)證和符號(hào)引用驗(yàn)證。

  • 準(zhǔn)備
    為類的靜態(tài)變量分配內(nèi)存,并將其初始化為默認(rèn)值,這些內(nèi)存都將在方法區(qū)中分配。

    1. 這時(shí)候進(jìn)行內(nèi)存分配的僅包括類變量(static),而不包括實(shí)例變量,實(shí)例變量會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一塊分配在Java堆中。
    2. 這里所設(shè)置的初始值通常情況下是數(shù)據(jù)類型默認(rèn)的零值(如0、0L、null、false等),而不是被在Java代碼中被顯式地賦予的值。

    為類變量(即static修飾的字段變量)分配內(nèi)存并且設(shè)置該類變量的初始值,這里不包含用final修飾的static,因?yàn)閒inal在編譯的時(shí)候就會(huì)分配了,注意這里不會(huì)為實(shí)例變量分配初始化,類變量會(huì)分配在方法區(qū)中,而實(shí)例變量是會(huì)隨著對(duì)象一起分配到Java堆中。
    例如:

      public String firstName = "蘇";
      public static String middleName = "東";
      public static final String lastName = "坡";
    

    firstName 不會(huì)被分配內(nèi)存,而 middleName 會(huì);但 middleName 的初始值不是“東”而是 null。
    需要注意的是,static final 修飾的變量被稱作為常量,和類變量不同。常量一旦賦值就不會(huì)改變了,所以 lastName 在準(zhǔn)備階段的值為“坡”而不是 null。

  • 解析
    解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過程。符號(hào)引用就是class文件中的:

    • CONSTANT_Class_info
    • CONSTANT_Field_info
    • CONSTANT_Method_info 等類型的常量。

    在Java中,一個(gè)java類將會(huì)編譯成一個(gè)class文件。在編譯時(shí),java類并不知道所引用的類的實(shí)際地址,因此只能使用符號(hào)引用來代替。比如org.simple.People類引用了org.simple.Language類,在編譯時(shí)People類并不知道Language類的實(shí)際內(nèi)存地址,因此只能使用符號(hào)org.simple.Language(假設(shè)是這個(gè),當(dāng)然實(shí)際中是由類似于CONSTANT_Class_info的常量來表示的)來表示Language類的地址。

3. 初始化

類初始化階段是類加載過程的最后一步,前面的類加載過程中,除了加載(Loading)階段用戶應(yīng)用程序可以通過自定義類加載器參與之外,其余動(dòng)作完全由虛擬機(jī)主導(dǎo)和控制。類的初始化過程,簡(jiǎn)單地說就是執(zhí)行類的<clinit>()方法的過程。
JVM初始化步驟:

  1. 假如這個(gè)類還沒有被加載和連接,則程序先加載并連接該類
  2. 假如該類的直接父類還沒有被初始化,則先初始化其直接父類
  3. 假如類中有初始化語(yǔ)句,則系統(tǒng)依次執(zhí)行這些初始化語(yǔ)句

類初始化時(shí)機(jī):只有當(dāng)對(duì)類的主動(dòng)使用的時(shí)候才會(huì)導(dǎo)致類的初始化,類的主動(dòng)使用包括以下六種:

  1. 創(chuàng)建類的實(shí)例,也就是new的方式
  2. 訪問某個(gè)類或接口的靜態(tài)變量,或者對(duì)該靜態(tài)變量賦值
  3. 調(diào)用類的靜態(tài)方法
  4. 反射(如Class.forName(“com.ttx.Test”))
  5. 初始化某個(gè)類的子類,則其父類也會(huì)被初始化
  6. Java虛擬機(jī)啟動(dòng)時(shí)被標(biāo)明為啟動(dòng)類的類(Java Test),直接使用java.exe命令來運(yùn)行某個(gè)主類
4. 使用

當(dāng) JVM 完成初始化階段之后,JVM 便開始從入口方法開始執(zhí)行用戶的程序代碼。這個(gè)使用階段也只是了解一下就可以了。

5. 卸載

當(dāng)用戶程序代碼執(zhí)行完畢后,JVM 便開始銷毀創(chuàng)建的 Class 對(duì)象,最后負(fù)責(zé)運(yùn)行的 JVM 也退出內(nèi)存。這個(gè)卸載階段也只是了解一下就可以了。

6. 結(jié)束生命周期

在如下幾種情況下,Java虛擬機(jī)將結(jié)束生命周期

  • 執(zhí)行了 System.exit()方法
  • 程序正常執(zhí)行結(jié)束
  • 程序在執(zhí)行過程中遇到了異常或錯(cuò)誤而異常終止
  • 由于操作系統(tǒng)出現(xiàn)錯(cuò)誤而導(dǎo)致Java虛擬機(jī)進(jìn)程終止

五. 類加載器

類加載器的任務(wù)是根據(jù)一個(gè)類的全限定名來讀取此類的二進(jìn)制字節(jié)流到JVM中,然后轉(zhuǎn)換為一個(gè)與目標(biāo)類對(duì)應(yīng)的java.lang.Class對(duì)象實(shí)例,一旦一個(gè)類被加載如JVM中,同一個(gè)類就不會(huì)被再次載入了。正如一個(gè)對(duì)象有一個(gè)唯一的標(biāo)識(shí)一樣,一個(gè)載入JVM的類也有一個(gè)唯一的標(biāo)識(shí)。在Java中,一個(gè)類用其全限定類名(包括包名和類名)作為標(biāo)識(shí);但在JVM中,一個(gè)類用其全限定類名和其類加載器作為其唯一標(biāo)識(shí)。

例如,如果在pg的包中有一個(gè)名為Person的類,被類加載器ClassLoader的實(shí)例kl負(fù)責(zé)加載,則該P(yáng)erson類對(duì)應(yīng)的Class對(duì)象在JVM中表示為(Person.pg.kl)。這意味著兩個(gè)類加載器加載的同名類:(Person.pg.kl)和(Person.pg.kl2)是不同的、它們所加載的類也是完全不同、互不兼容的。

總的來說,類加載器(class loader)用來加載 Java 類到 Java 虛擬機(jī)中。類加載器負(fù)責(zé)讀取 Java 字節(jié)代碼,并轉(zhuǎn)換成 java.lang.Class類的一個(gè)實(shí)例。有了Class類實(shí)例,就可以通過newInstance方法創(chuàng)建該類的對(duì)象。一般來說,默認(rèn)類加載器為當(dāng)前類的類加載器。比如A類中引用B類,A的類加載器為C,那么B的類加載器也為C。

在虛擬機(jī)提供了3種類加載器,引導(dǎo)(Bootstrap)類加載器、擴(kuò)展(Extension)類加載器、系統(tǒng)(System)類加載器(也稱應(yīng)用類加載器)

1. Bootstrap ClassLoader

加載JVM自身工作需要的類,它由JVM自己實(shí)現(xiàn)。它會(huì)加載$JAVA_HOME/jre/lib下的文件

2. ExtClassLoader

它是JVM的一部分,由sun.misc.Launcher[圖片上傳失敗...(image-f2fdd2-1597653733533)]
JAVA_HOME/jre/lib/ext目錄中的文件(或由System.getProperty("java.ext.dirs")所指定的文件)。

3. AppClassLoader

應(yīng)用類加載器,我們工作中接觸最多的也是這個(gè)類加載器,它由sun.misc.Launcher$AppClassLoader實(shí)現(xiàn)。它加載由System.getProperty("java.class.path")指定目錄下的文件,也就是我們通常說的classpath路徑。

4. 雙親委派模型

如果一個(gè)類加載器收到了類加載請(qǐng)求,它并不會(huì)自己先去加載,而是把這個(gè)請(qǐng)求委托給父類的加載器去執(zhí)行,如果父類加載器還存在其父類加載器,則進(jìn)一步向上委托,依次遞歸,請(qǐng)求最終將到達(dá)頂層的啟動(dòng)類加載器,如果父類加載器可以完成類加載任務(wù),就成功返回,倘若父類加載器無法完成此加載任務(wù),子加載器才會(huì)嘗試自己去加載,這就是雙親委派模式(接下來的源碼可以看出這個(gè)流程

自定義Java類加載器
從上面源碼的分析,可以知道:實(shí)現(xiàn)自定義類加載器需要繼承ClassLoader,如果想保證自定義的類加載器符合雙親委派機(jī)制,則覆寫findClass方法;如果想打破雙親委派機(jī)制,則覆寫loadClass方法。

5. 何時(shí)出發(fā)類加載動(dòng)作?

類加載的觸發(fā)可以分為隱式加載和顯示加載。

  1. 隱式加載
    隱式加載包括以下幾種情況:

    • 遇到new、getstatic、putstatic、invokestatic這4條字節(jié)碼指令時(shí)
    • 對(duì)類進(jìn)行反射調(diào)用時(shí)
    • 當(dāng)初始化一個(gè)類時(shí),如果其父類還沒有初始化,優(yōu)先加載其父類并初始化
    • 虛擬機(jī)啟動(dòng)時(shí),需指定一個(gè)包含main函數(shù)的主類,優(yōu)先加載并初始化這個(gè)主類
  2. 顯示加載
    顯示加載包含以下幾種情況:

    • 通過ClassLoader的loadClass方法
    • 通過Class.forName
    • 通過ClassLoader的findClass方法
6. 編寫自定義類加載器的意義何在?
  • 當(dāng)class文件不在ClassPath路徑下,默認(rèn)系統(tǒng)類加載器無法找到該class文件,在這種情況下我們需要實(shí)現(xiàn)一個(gè)自定義的ClassLoader來加載特定路徑下的class文件生成class對(duì)象。
  • 當(dāng)一個(gè)class文件是通過網(wǎng)絡(luò)傳輸并且可能會(huì)進(jìn)行相應(yīng)的加密操作時(shí),需要先對(duì)class文件進(jìn)行相應(yīng)的解密后再加載到JVM內(nèi)存中,這種情況下也需要編寫自定義的ClassLoader并實(shí)現(xiàn)相應(yīng)的邏輯。
  • 當(dāng)需要實(shí)現(xiàn)熱部署功能時(shí)(一個(gè)class文件通過不同的類加載器產(chǎn)生不同class對(duì)象從而實(shí)現(xiàn)熱部署功能),需要實(shí)現(xiàn)自定義ClassLoader的邏輯。

六. JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)及內(nèi)存模型(JMM)

Java內(nèi)存模型(Java Memory Model ,JMM)就是一種符合內(nèi)存模型規(guī)范的,屏蔽了各種硬件和操作系統(tǒng)的訪問差異的,保證了Java程序在各種平臺(tái)下對(duì)內(nèi)存的訪問都能保證效果一致的機(jī)制及規(guī)范。

計(jì)算機(jī)會(huì)提前給將內(nèi)存分配給軟件,由軟件控制自己內(nèi)存區(qū)域的管理。如果軟件當(dāng)前內(nèi)存已經(jīng)被占滿,我們需要將不活躍的數(shù)據(jù)清除,然后載入新的數(shù)據(jù),內(nèi)存都是可以重復(fù)被使用的。JVM也是計(jì)算機(jī)內(nèi)存中的一個(gè)程序,所以計(jì)算機(jī)會(huì)分配一定的內(nèi)存給JVM。以堆內(nèi)存為例:Xmx-最多分配內(nèi)存的大小2048M Xms-最少分配內(nèi)存的大小512M 。


首先Java源代碼文件(.java后綴)會(huì)被Java編譯器編譯為字節(jié)碼文件(.class后綴),然后由JVM中的類加載器加載各個(gè)類的字節(jié)碼文件,加載完畢之后,交由JVM執(zhí)行引擎執(zhí)行。在整個(gè)程序執(zhí)行過程中,JVM會(huì)用一段空間來存儲(chǔ)程序執(zhí)行期間需要用到的數(shù)據(jù)和相關(guān)信息,這段空間一般被稱作為Runtime Data Area(運(yùn)行時(shí)數(shù)據(jù)區(qū)),也就是我們常說的JVM內(nèi)存。因此,在Java中我們常常說到的內(nèi)存管理就是針對(duì)這段空間進(jìn)行管理(如何分配和回收內(nèi)存空間)。

Java軟件在運(yùn)行時(shí)JVM會(huì)運(yùn)行很多的類,但是計(jì)算機(jī)給我們分配的內(nèi)存又有一定的限制。所以JVM也需要管理class占用空間的大小或者通過class生成對(duì)象占用空間的大小。比如當(dāng)前JVM的大小是4G 我們不可能時(shí)時(shí)刻刻對(duì)4G的空間進(jìn)行遍歷或者資源的回收J(rèn)VM為了方便管理對(duì)象占用的內(nèi)存空間,于是將內(nèi)存運(yùn)行時(shí)數(shù)據(jù)區(qū)進(jìn)行劃分。
線程獨(dú)享:

  • 程序計(jì)數(shù)器
  • 虛擬機(jī)棧
  • 本地方法棧

線程共享:

  • 堆內(nèi)存
  • 方法區(qū)
  • 堆外內(nèi)存( metadata元數(shù)據(jù)區(qū))
1. PC寄存器(程序計(jì)數(shù)器)

由于JVM同時(shí)可以處理多個(gè)線程所以就涉及到一些線程調(diào)度,當(dāng)cpu暫停運(yùn)行線程A把時(shí)間片讓給線程B的時(shí)候我們需要保存線程A被暫停執(zhí)行前的一些現(xiàn)場(chǎng)狀態(tài),需要記錄當(dāng)前執(zhí)行到那一行字節(jié)碼了,所以PC寄存器會(huì)實(shí)時(shí)記錄當(dāng)前線程執(zhí)行的代碼行數(shù)。

虛擬機(jī)棧

Java虛擬機(jī)棧(Java Virtual Machine Stacks)是線程私有的,其生命周期和線程同步,隨著線程的啟動(dòng)而創(chuàng)建,隨線程的結(jié)束而銷毀。Java虛擬機(jī)棧和線程同時(shí)創(chuàng)建,用于存儲(chǔ)棧幀。每個(gè)方法在執(zhí)行時(shí)都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame),用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。每一個(gè)方法從調(diào)用直到執(zhí)行完成的過程就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中從入棧到出棧的過程。此區(qū)域有兩個(gè)異常:當(dāng)棧深度超過虛擬機(jī)的規(guī)定時(shí),StackOverFlowError;當(dāng)擴(kuò)展時(shí)無法申請(qǐng)到足夠的內(nèi)存,OutOfMemeryError。

棧幀(Stack Frame)

每一個(gè)方法從調(diào)用到方法返回(結(jié)束)都對(duì)應(yīng)著一個(gè)棧幀入棧出棧的過程(棧幀隨著方法調(diào)用而創(chuàng)建,隨著方法結(jié)束而銷毀)。最頂部的棧幀稱為當(dāng)前棧幀,當(dāng)前棧幀所關(guān)聯(lián)的方法稱為當(dāng)前方法,定義當(dāng)前方法的類稱為當(dāng)前類,該線程中,虛擬機(jī)有且也只會(huì)對(duì)當(dāng)前棧幀進(jìn)行操作,如果當(dāng)前方法調(diào)用了其他方法,或者當(dāng)前方法執(zhí)行結(jié)束,那這個(gè)方法的棧幀就不再是當(dāng)前棧幀了。調(diào)用新的方法時(shí),新的棧幀也會(huì)隨之創(chuàng)建。并且隨著程序控制權(quán)轉(zhuǎn)移到新方法,新的棧幀成為了當(dāng)前棧幀。方法返回之際,原棧幀會(huì)返回方法的執(zhí)行結(jié)果給之前的棧幀(返回給方法調(diào)用者),隨后虛擬機(jī)將會(huì)丟棄此棧幀。在編譯代碼時(shí),棧幀需要多大的局部變量表,多深的操作數(shù)棧都可以完全確定的,并寫入到Class 文件的方法表的 Code 屬性中。

  • 局部變量表
    是一組變量的存儲(chǔ)空間,用于存放 方法參數(shù) 和 局部變量。在Class 文件的方法表的 Code 屬性的 max_locals 指定了該方法所需局部變量表的最大容量。虛擬機(jī)通過索引定位法的方式使用局部變量表,索引值的范圍是從0到Slot的最大數(shù)量。在方法執(zhí)行時(shí),特別是執(zhí)行實(shí)例方法時(shí),那么實(shí)例變量表的第0位索引默認(rèn)是方法所屬的實(shí)例對(duì)象的引用“this”對(duì)象,接著是1到Slot參數(shù)變量到方法內(nèi)部的局部變量。局部變量表的基本單位為變量槽(Variable Slot),Java虛擬機(jī)規(guī)范并沒有定義一個(gè)槽所應(yīng)該占用內(nèi)存空間的大小,但是規(guī)定了一個(gè)槽應(yīng)該可以存放一個(gè)32位以內(nèi)的數(shù)據(jù)類型。如果Slot是32位的,則遇到一個(gè)64位數(shù)據(jù)類型的變量(如long或double型),則會(huì)連續(xù)使用兩個(gè)連續(xù)的Slot來存儲(chǔ)。

  • 操作數(shù)棧
    操作數(shù)棧,主要用于保存計(jì)算過程的中間結(jié)果,同時(shí)作為計(jì)算過程中變量臨時(shí)的存儲(chǔ)空間。也常稱為操作棧,它是一個(gè)后入先出棧(LIFO)。同局部變量表一樣,操作數(shù)棧的最大深度也在編譯的時(shí)候?qū)懭氲椒椒ǖ腃ode屬性的max_stacks數(shù)據(jù)項(xiàng)中。舉例來說,在JVM中 執(zhí)行 a = b + c 的字節(jié)碼執(zhí)行過程中操作數(shù)棧以及局部變量表的變化如下圖所示。局部變量表中存儲(chǔ)著a、b、c 三個(gè)局部變量,首先將b和c分別入棧。


    將棧頂?shù)膬蓚€(gè)數(shù)出棧執(zhí)行加法操作,并將結(jié)果保存至棧頂,之后將棧頂?shù)臄?shù)出棧賦值給a

  • 動(dòng)態(tài)連接
    動(dòng)態(tài)鏈接主要就是指向運(yùn)行時(shí)常量池的方法引用。因?yàn)?Java 是在運(yùn)行期間動(dòng)態(tài)鏈接的,所以為了支持動(dòng)態(tài)鏈接,需要將方法區(qū)里面的符號(hào)引用轉(zhuǎn)為直接引用(即:給出地址),這就叫動(dòng)態(tài)鏈接。

  • 方法返回地址
    存放調(diào)用該方法的PC寄存器的值。一個(gè)方法的結(jié)束,有兩種方式:正常執(zhí)行完成,出現(xiàn)未處理的異常,非正常退出。方法執(zhí)行完以后,根據(jù)這個(gè)值決定返回到哪里去。

2.本地方法棧

JVM運(yùn)行native方法準(zhǔn)備的空間,由于很多native方法都是用C語(yǔ)言實(shí)現(xiàn)的,所以通常又叫C棧,它與Java虛擬機(jī)棧實(shí)現(xiàn)的功能類似,只不過本地方法棧描述本地方法運(yùn)行過程的內(nèi)存模型。與虛擬機(jī)棧的區(qū)別是,虛擬機(jī)棧是為執(zhí)行Java方法服務(wù),而本地方法棧是為執(zhí)行Native方法服務(wù),同樣這個(gè)區(qū)域也會(huì)拋出StackOverFlowError、OutOfMemeryError。

3.堆內(nèi)存

堆內(nèi)存理論上是JVM中占用內(nèi)存最大的一塊區(qū)域,里面存放了java創(chuàng)建的各種引用數(shù)據(jù)類型(幾乎所有的對(duì)象、數(shù)組都在這個(gè)內(nèi)存區(qū)域分配)。堆內(nèi)存被所有線程共享,虛擬機(jī)啟動(dòng)時(shí)就會(huì)創(chuàng)建。堆內(nèi)存中的數(shù)據(jù)經(jīng)常會(huì)被回收,每次GC的垃圾占總量的90%以上,因此堆是垃圾收集器管理的主要區(qū)域。假設(shè)本次堆內(nèi)存大小為4G,為了找出垃圾對(duì)象,所花費(fèi)的時(shí)間是比較長(zhǎng)的,堆內(nèi)存為了更好的管理對(duì)象,又將堆內(nèi)存重新進(jìn)行了區(qū)域的劃分:分為新生代(Young),老年代(Old), 新生代又被劃分為三個(gè)區(qū)域Eden、From Survivor, To Survivor。當(dāng)堆中沒有足夠的內(nèi)存完成實(shí)例分配且無法擴(kuò)展時(shí),拋出OutOfMemoryError。

新生代(Young)

所有的對(duì)象創(chuàng)建都是在新生區(qū)創(chuàng)建的,每當(dāng)JVM進(jìn)行一次GC,新生代里面的對(duì)象的標(biāo)識(shí)就會(huì)進(jìn)行累加+1,如果累計(jì)超過15次GC都沒有被回收掉,說明這個(gè)對(duì)象不容易被回收,將被移入老年代。如果新生區(qū)太小,會(huì)導(dǎo)致每次垃圾回收特別頻繁。于是為了更好的管理新生區(qū),將新生區(qū)進(jìn)行區(qū)域的劃分Eden、From Survivor, To Survivor三個(gè)區(qū)域的比例為8:1:1。

當(dāng)對(duì)象在 Eden 創(chuàng)建后,在經(jīng)過一次 GC 后,如果對(duì)象還存活,并且能夠被另外一塊 Survivor 區(qū)域(假設(shè)為from 區(qū)域)所容納,則使用復(fù)制算法將這些仍然還存活的對(duì)象復(fù)制到另外一塊 Survivor 區(qū)域 ( 即 to 區(qū)域 ) 中,并且將這些對(duì)象的年齡設(shè)置為1,以后對(duì)象在 Survivor 區(qū)每熬過一次 Minor GC,就將對(duì)象的年齡 + 1,當(dāng)對(duì)象的年齡達(dá)到某個(gè)值時(shí) ( 默認(rèn)是 15 歲,可以通過參數(shù) -XX:MaxTenuringThreshold 來設(shè)定 ),這些對(duì)象就會(huì)成為老年代。然后清理所使用過的 Eden 以及 Survivor 區(qū)域 ( 即 from 區(qū)域 )。但這也不是一定的,對(duì)于一些較大的對(duì)象 ( 即需要分配一塊較大的連續(xù)內(nèi)存空間 ) 新生代放不下,則是直接進(jìn)入到老年代,JVM 認(rèn)為,一般大對(duì)象的存活時(shí)間一般比較久遠(yuǎn)。

From Survivor區(qū)域與To Survivor區(qū)域是交替切換空間,在同一時(shí)間內(nèi)兩者中只有一個(gè)不為空。

老年代(Old)

年老代里存放的都是存活時(shí)間較久的,大小較大的對(duì)象,因此年老代使用標(biāo)記整理算法。當(dāng)年老代容量滿的時(shí)候,會(huì)觸發(fā)一次Major GC(full GC),回收年老代和年輕代中不再被使用的對(duì)象資源。老年區(qū)的GC不是很頻繁,只有進(jìn)行full GC的時(shí)候才會(huì)操作老年區(qū)。

3.方法區(qū)

方法區(qū)也是線程共享,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。
用于存儲(chǔ)虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。
此區(qū)域包含運(yùn)行時(shí)常量池(Runtime Constant Pool)。
當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時(shí),將拋出OutOfMemoryError。

對(duì)象的創(chuàng)建(HotSpot)
通過new關(guān)鍵字創(chuàng)建(或克隆、反序列化)
(1.) 檢查指令的參數(shù)(即工作中我們New的對(duì)象),能否在常量池中找到它的符號(hào)引用。
(2.) 如果存在,檢查符號(hào)引用代表的類是否被加載、解析、初始化過。如果沒有則執(zhí)行類的加載。
(3.) 加載通過后,虛擬機(jī)將為新生對(duì)象分配內(nèi)存。(所需內(nèi)存大小在類加載完成后便可確定)

七. 垃圾回收機(jī)制

大家都知道JVM的內(nèi)存結(jié)構(gòu)包括五大區(qū)域:程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧、堆區(qū)、方法區(qū)。其中程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧3個(gè)區(qū)域隨線程而生、隨線程而滅,因此這幾個(gè)區(qū)域的內(nèi)存分配和回收都具備確定性,就不需要過多考慮回收的問題,因?yàn)榉椒ńY(jié)束或者線程結(jié)束時(shí),內(nèi)存自然就跟隨著回收了。而Java堆區(qū)和方法區(qū)則不一樣,這部分內(nèi)存的分配和回收是動(dòng)態(tài)的,正是垃圾收集器所需關(guān)注的部分。

1. 判斷對(duì)象是否存活的算法

Java堆中存放著幾乎所有的對(duì)象實(shí)例,垃圾回收器在堆進(jìn)行垃圾回收前,首先要判斷這些對(duì)象那些還存活,那些已經(jīng)“死去”。判斷對(duì)象是否已“死”有如下幾種算法:

(1)引用計(jì)數(shù)法
給每個(gè)對(duì)象添加一個(gè)計(jì)數(shù)器,當(dāng)有地方引用該對(duì)象時(shí)計(jì)數(shù)器加1,當(dāng)引用失效時(shí)計(jì)數(shù)器減1。用對(duì)象計(jì)數(shù)器是否為0來判斷對(duì)象是否可被回收。缺點(diǎn):無法解決循環(huán)引用的問題。

優(yōu)點(diǎn):引用計(jì)數(shù)收集器執(zhí)行簡(jiǎn)單,判定效率高,交織在程序運(yùn)行中。對(duì)程序不被長(zhǎng)時(shí)間打斷的實(shí)時(shí)環(huán)境比較有利(OC的內(nèi)存管理使用該算法)。

缺點(diǎn):無法檢測(cè)出循環(huán)引用。如父對(duì)象有一個(gè)對(duì)子對(duì)象的引用,子對(duì)象反過來引用父對(duì)象。這樣,他們的引用計(jì)數(shù)永遠(yuǎn)不可能為0。同時(shí),引用計(jì)數(shù)器增加了程序執(zhí)行的開銷。

(2)可達(dá)性分析算法
可達(dá)性分析算法是從離散數(shù)學(xué)中的圖論引入的,程序把所有的引用關(guān)系看作一張圖,從一個(gè)節(jié)點(diǎn)GC ROOT開始,尋找對(duì)應(yīng)的引用節(jié)點(diǎn),找到這個(gè)節(jié)點(diǎn)以后,繼續(xù)尋找這個(gè)節(jié)點(diǎn)的引用節(jié)點(diǎn),當(dāng)所有的引用節(jié)點(diǎn)尋找完畢之后,剩余的節(jié)點(diǎn)則被認(rèn)為是沒有被引用到的節(jié)點(diǎn),即無用的節(jié)點(diǎn),無用的節(jié)點(diǎn)將會(huì)被判定為是可回收的對(duì)象。

2. 常用的垃圾回收算法

(1)標(biāo)記-清除算法

“標(biāo)記-清除”算法是最基礎(chǔ)的收集算法。算法分為標(biāo)記和清除兩個(gè)階段:首先標(biāo)記出所有需要回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對(duì)象。后續(xù)的收集算法都是基于這種思路并對(duì)其不足加以改進(jìn)而已。
“標(biāo)記-清除”算法的不足主要有兩個(gè):
效率問題:標(biāo)記和清除這兩個(gè)過程的效率都不高
空間問題:標(biāo)記清除后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會(huì)導(dǎo)致以后在程序運(yùn)行中需要分配較大對(duì)象時(shí),無法找到足夠連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集。

(2)復(fù)制算法(新生代回收算法)
“復(fù)制”算法是為了解決“標(biāo)記-清除”的效率問題。它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中一塊。當(dāng)這塊內(nèi)存需要進(jìn)行垃圾回收時(shí),會(huì)將此區(qū)域還存活著的對(duì)象復(fù)制到另一塊上面,然后再把已經(jīng)使用過的內(nèi)存區(qū)域一次清理掉。這樣做的好處是每次都是對(duì)整個(gè)半?yún)^(qū)進(jìn)行內(nèi)存回收,內(nèi)存分配時(shí)也就不需要考慮內(nèi)存碎片等的復(fù)雜情況,只需要移動(dòng)堆頂指針,按順序分配即可。此算法實(shí)現(xiàn)簡(jiǎn)單,運(yùn)行高效。

(3)分代收集算法
當(dāng)前JVM垃圾收集都采用的是"分代收集(Generational Collection)"算法,這個(gè)算法并沒有新思想,只是根據(jù)對(duì)象存活周期的不同將內(nèi)存劃分為幾塊。
一般是把Java堆分為新生代和老年代。在新生代中,每次垃圾回收都有大批對(duì)象死去,只有少量存活,因此我們采用復(fù)制算法;而老年代中對(duì)象存活率高、沒有額外空間對(duì)它進(jìn)行分配擔(dān)保,就必須采用"標(biāo)記-清理"或者"標(biāo)記-整理"算法。

八. JVM的生命周期

JVM實(shí)例對(duì)應(yīng)了一個(gè)獨(dú)立運(yùn)行的java程序它是進(jìn)程級(jí)別

  • 啟動(dòng)。啟動(dòng)一個(gè)Java程序時(shí),一個(gè)JVM實(shí)例就產(chǎn)生了,任何一個(gè)擁有public static void
    main(String[] args)函數(shù)的class都可以作為JVM實(shí)例運(yùn)行的起點(diǎn)。

  • 運(yùn)行。main()作為該程序初始線程的起點(diǎn),任何其他線程均由該線程啟動(dòng)。JVM內(nèi)部有兩種線程:守護(hù)線程和非守護(hù)線程,main()屬于非守護(hù)線程,守護(hù)線程通常由JVM自己使用,java程序也可以表明自己創(chuàng)建的線程是守護(hù)線程。

  • 消亡。當(dāng)程序中的所有非守護(hù)線程都終止時(shí),JVM才退出;若安全管理器允許,程序也可以使用Runtime類或者System.exit()來退出。

參考資料
《深入理解Java虛擬機(jī)》
Java代碼到底是如何編譯成機(jī)器指令的
Java編譯方式總結(jié):前端編譯、JIT編譯、AOT編譯
AOT上手

jvm之后端編譯與優(yōu)化
Java的編譯原理!
JVM筆記-后端編譯與優(yōu)化

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容