萬字長文把[JVM]從頭到尾講一遍

第一篇:[JVM入門指南01]內(nèi)存區(qū)域與溢出異?!饕榻BJVM的運行時數(shù)據(jù)區(qū)
第二篇:[JVM入門指南02]GC垃圾回收機制——主要介紹JVM執(zhí)行引擎的垃圾回收機制
第三篇:[JVM入門指南03]類加載和Android虛擬機——主要介紹JVM類加載機制和JVM的執(zhí)行引擎


JVM架構

[JVM入門指南01]內(nèi)存區(qū)域與溢出異常

本文將介紹JVM的結構、對象的創(chuàng)建和分配過程、內(nèi)存溢出

JVM介紹

java文件執(zhí)行流程:

  • .java文件通過JDK的編譯器變成.class文件。
  • .class文件和java類庫,通過類加載器ClassLoader進入JVM的方法區(qū)。
  • JVM的執(zhí)行引擎執(zhí)行,把字節(jié)碼翻譯機器碼,并跟硬件進行交互。
流程圖

JVM是一種規(guī)范,它規(guī)范了.class文件與本地硬件交互的一種規(guī)范。比如對于c/c++開發(fā)者來說,他們擁有了對象的“所有權”,對象的開始和終結需要他們的維護。而相對Java開發(fā)者,他們在虛擬機內(nèi)存管理垃圾回收機制下,就不需要維護對象的終結,JVM幫我們自動回收對象,這使開發(fā)者更加便捷,但一旦發(fā)生內(nèi)存泄漏和溢出,如果不熟悉JVM則很難找出問題所在,所以學習JVM能幫助我們快速定位內(nèi)存泄漏和溢出原因。

JVM的特性

平臺無關性:

一套java代碼可以在window、linux、mac中運行,因為JVM這個中間平臺已經(jīng)做好了與硬件交互的解釋。

語言無關性:

JVM接收的是.class文件,所以無論你是寫java語言還是其他語言,只要按照規(guī)范生成為.class文件就可以與JVM上運行。

JVM特性

JVM的分類

JVM只是一種規(guī)范,可以按照規(guī)范自定義JVM。本篇文章講解的是Oracle公司的Hotspot虛擬機。


JVM發(fā)展

JVM整體內(nèi)存結構

JVM的整體內(nèi)存結構包括:虛擬機棧、程序計數(shù)器、堆、方法區(qū)、直接內(nèi)存。

整體內(nèi)存結構1

整體內(nèi)存結構2

運行時數(shù)據(jù)區(qū)域

虛擬機棧

每啟動一個線程都會創(chuàng)建一個新的虛擬機棧。在線程私有區(qū)中包括了虛擬機棧和本地方法棧,但在Hotspot虛擬機中把這兩個棧合成一個棧,
作用
存儲當前線程運行java方法的指令、數(shù)據(jù)、返回地址。
大小限制
可用-xxs設置虛擬機棧的大小
棧幀
它是棧元素,每執(zhí)行一個方法,就會有相應的一個棧幀進入虛擬機棧。

棧幀結構

棧幀結構

  • 局部變量表
    局部變量表是一組變量表存儲空間,用于存放方法參數(shù)和方法內(nèi)的局部變量。在java程序編譯為class文件時,就在方法的Code屬性的max_locals數(shù)據(jù)項中確定了該方法所需要分配的局部變量表的最大容量。
    局部變量表以變量槽Slot為最小單位,分別存儲了:
  1. 8大基本數(shù)據(jù)類型:boolean、byte、char、short、int、float、long、double
  2. reference:表示對一個對象實例的引用。一、此引用可以直接或間接地查找到對象在Java堆中的數(shù)據(jù)存放的起始地址索引。二、此引用可以直接或間接地查找到對象所屬數(shù)據(jù)類型在方法區(qū)中的存儲的類型信息。
  3. returnAddress:指向一條字節(jié)碼指令的地址。

如果執(zhí)行的是實例方法(非static方法),那局部變量表中第0位索引的Slot默認是用于傳遞方法所屬對象實例的引用,在方法中可以通過this來訪問這個引用。

  • 操作數(shù)棧
    在方法執(zhí)行過程中,會有各種字節(jié)碼指令往操作數(shù)棧中寫入和提取內(nèi)容,也就是出棧和入棧操作。例如,在做算術運算的時候是通過操作數(shù)棧來進行的,又或者在調(diào)用其他方法的時候通過操作數(shù)棧來傳參。

  • 動態(tài)鏈接
    將符號引用指向直接引用

  • 返回地址
    返回棧幀方法的返回事件。

程序計數(shù)器

指向當前線程正在執(zhí)行的字節(jié)碼指令的地址。

java堆幾乎所有的對象實例都在這里分配內(nèi)存,java堆不需要連續(xù)的內(nèi)存,既可以固定大小又可以動態(tài)擴展大小,是虛擬機中管理內(nèi)存中最大的一塊,是垃圾回收機制重點回收區(qū)域。

java堆是線程共享區(qū)域,但是有些內(nèi)存分配也可以設計為多個線程的私有的分配緩沖區(qū)TLAB,以提升對象分配的效率。

java堆結構:

  • 新生代(占1/3):存放生命周期不長的小對象
    Eden空間(占 8/10)
    From空間(S0,占 1/10)
    To空間(S1,占 1/10)

  • 老年代(占2/3):存放大對象

方法區(qū)

方法區(qū)

方法區(qū)(永久代)是存放已被虛擬機加載的類型信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼緩存。方法區(qū)不需要連續(xù)的內(nèi)存,既可以固定大小又可以動態(tài)擴展大小。垃圾收集器在此區(qū)域主要回收常量池和類型(class對象)的卸載。

運行時常量池

運行時常量池

運行常量池是在常量池表中存放的編譯期生成的各種字面量和符號引用,這部分內(nèi)容將在

直接內(nèi)存

直接內(nèi)存并不是虛擬機運行時的數(shù)據(jù)區(qū)域。在JDK1.4中加入NIO類,引入一種基于通道(Channel)和緩沖區(qū)(Buffer)的I/O方式,它可以使用Native函數(shù)庫直接分配堆外內(nèi)存。然后通過堆里面的DirectByteBuffer對象作為引用進行操作。

JVM結構小結

JVM分為線程私有區(qū)和共享區(qū):
私有區(qū)中存放著虛擬機棧程序計數(shù)器。虛擬機棧跟線程一一對應,每一個線程對應著一個虛擬機棧,在虛擬機棧中以棧幀為元素,棧幀跟方法一一對應,每執(zhí)行一個方法就有一個棧幀入棧,每完成一個方法就有一個對應的棧幀出棧,棧幀的結構分為:局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口。程序計數(shù)器則記錄當前方法執(zhí)行到的字節(jié)碼的行數(shù),在輪流執(zhí)行的并發(fā)編程中能接上次執(zhí)行的位置往下執(zhí)行。

共享區(qū)主要有方法區(qū)。堆中又劃分為年輕代和老年代,其中年輕代又劃分為:Eden、From、To代,幾乎所有的對象實例都在堆中分配。方法區(qū)主要存儲類型信息和常量、靜態(tài)變量等。

對象的創(chuàng)建和分配原則

對象的創(chuàng)建過程

對象在創(chuàng)建時首先會進行類加載檢查(是否被ClassLoader加載過),即能否在常量池中定位到一個類的符號引用,并且檢查這個符號引用代表類是否被加載、解析、初始化過,如果沒有,則去ClassLoader類加載,否則進行對象分配內(nèi)存、初始化類變量的默認值、設置對象頭、執(zhí)行類的構造函數(shù)。


對象創(chuàng)建過程

這里重點講下分配內(nèi)存的方式和并發(fā)的安全問題,和對象在堆中的結構。

分配內(nèi)存

對象的分配內(nèi)存就是在Java堆中分配一塊內(nèi)存給對象,但是根據(jù)堆中的已使用內(nèi)存和空閑內(nèi)存的存放位置,分為兩種分配內(nèi)存方式:

  1. 指針碰撞 : 如果JAVA堆是規(guī)整的,已使用內(nèi)存集體放一邊,可閑內(nèi)存集體放一邊,那么只需要指針往空閑的內(nèi)存移動一定位置就可以為對象分配內(nèi)存,所以叫指針碰撞

  2. 空閑列表:如果JAVA堆是不規(guī)整的,已使用內(nèi)存和空閑內(nèi)存相互交錯在一起,那么就需要維護出一張空閑內(nèi)存的列表,把新的對象分配在空閑內(nèi)存列表中,所以叫空閑列表。

多線程并發(fā)的分配問題
堆是線程共享的,所以在多線程時有可能會發(fā)生同一位置被多個線程的對象分配,那么也有兩種解決辦法:

  1. CAS加上失敗重試機制
  2. 本地線程分配緩沖(TLAB)
    每一個線程都在堆中分配一個獨立的區(qū)域,每個對象的分配內(nèi)存就是當前線程在堆中分配區(qū)域分配。

對象在堆中的結構

對象
對象

對象的內(nèi)存布局分為:

  • 對象頭
    MarkWord: 哈希碼、GC分代年齡、鎖狀態(tài)標識、線程持有的鎖、偏向線程ID、偏向時間戳
    類型指針:指向Class對象

  • 實例數(shù)據(jù)
    存放類的變量數(shù)據(jù)

  • 對齊填充
    占位符作用,對象是以8字節(jié)為單位

引用對對象實例的訪問

兩種訪問方式
  1. 使用句柄:通過指向一個句柄池間接的訪問對象,優(yōu)點:對象地址改變后不要更改reference的指向,缺點:查找地址需要時間。

  2. 直接指針:reference直接指向堆中的實例地址,優(yōu)點:時間快,缺點對象實例移動reference也需要改變。

對象的分配原則

對象分配原則

是否能在棧上分配

逃逸分析:在方法中創(chuàng)建的對象,沒有被其他方法或線程中用到,那么這個對象可以在棧中分配。

優(yōu)先在Eden中分配:

大多數(shù)情況下,對象在Eden中分配,如果Eden區(qū)沒有足夠的空間分配,將觸發(fā)Minor GC。在Minor GC時如果S0,S1的空間不夠分配或GC年齡達到指定值,則根據(jù)分配擔保機制轉(zhuǎn)移到老年代。

大對象直接進入老年代

超過指定大小的大對象將直接進入老年代。這樣可以避免Eden區(qū)和兩個S區(qū)來回復制。長的字符串和數(shù)組是常見的大對象。

長期存活的對象進入老年代

在Eden區(qū)的對象實例發(fā)生GC時會被移動到S0區(qū)并在對象頭的GC年齡+1,之后每GC一次,還在S0區(qū)的對象的GC年齡就會+1,如果超過了指定次數(shù)就會移入老年代。

動態(tài)對象年齡判斷

如果在S空間中,相同GC年齡的對象的大小的總和大于S空間的一半,則大于或等于此GC年齡的對象全部移入老年代。

空間分配擔保機制

在發(fā)生Minor GC之前要進行一次空間分配擔保機制的判斷,決定是發(fā)生Minor GC還是Full GC。因為在Minor GC時可能會有對象進入老年代,如果老年代的空間不夠就會發(fā)生OOM的風險,所以空間分配擔保機制則是盡量去降低這種風險發(fā)生。

  • 發(fā)生Minor GC
  1. 老年代的最大連續(xù)空間大于新生代的所有對象總空間
  2. 如果1不成立,則查看是否可以擔保,如果可以擔保,繼續(xù)查看老年代的最大連續(xù)空間大于歷次晉升到老年代空間的對象的平均大小。
  • 發(fā)生Full GC
  1. 如果上述1不成立,且不可以擔保
  2. 如果可以擔保,繼續(xù)查看老年代的最大連續(xù)空間小于歷次晉升到老年代空間的對象的平均大小。

對象的創(chuàng)建分配小結:

對象的創(chuàng)建流程是,首先ClassLoader類加載檢查:如果該類的符號引用已經(jīng)被加載、解析、初始化過后則表示已經(jīng)被ClassLoader加載過。被ClassLoader加載過后,則到堆中分配內(nèi)存,分配方式有指針碰撞和空閑列表兩種,在分配內(nèi)存中有并發(fā)的安全問題所以有了兩種解決方式:CAS和TLAVB。分配內(nèi)存地址后,就開始設置類變量的初始值。設置完默認值后,就開始設置對象頭,其中對象頭包括:GC年齡、哈希碼、類型指針等。對象則包括對象頭、實例數(shù)據(jù)和對對齊填充。最后是執(zhí)行對象的構造函數(shù)進行初始化。棧中對堆的對象實例的訪問有使用句柄直接訪問兩種。

對象在分配原則有:

  1. 根據(jù)逃逸分析決定是否在棧中分配
  2. 新對象優(yōu)先分配在Eden區(qū)
  3. 大對象直接分配在老年代
  4. 長期存活的對象進入老年代
  5. 同年齡的對象總和大于S區(qū)的一半,則以上年齡的對象進入老年代。
  6. 分配擔保機制,在每次發(fā)生Minor GC之前等會判斷老年代的最大連續(xù)空間是否足夠可能從新生代進來的對象。

內(nèi)存溢出

堆溢出

java堆用于存儲對象實例,只要不斷的產(chǎn)生對象,并且GC Root到對象中有可達性導致不能垃圾回收,當堆的對象總量超過規(guī)定的最大值就會產(chǎn)生內(nèi)存溢出OutOfMemeyError。

棧溢出

  1. 如果是對虛擬機棧存放不下,比如是棧創(chuàng)建太多或棧的容量太大,則會導致內(nèi)存溢出OutOfMemeyError。
  2. 如果是對棧幀存放不下,比如棧幀太多或棧幀太大或棧最大容量太小,則會導致棧溢出StackOverflowError。

方法區(qū)和運行常量溢出

方法區(qū)(元空間)存放著類型信息,比如Class對象,而Class對象

直接內(nèi)存溢出

直接內(nèi)存的最大值默認與Java堆的最大值一致。

[JVM入門指南02]GC垃圾回收機制

概述

在JVM中主要的結構為:虛擬機棧、堆、方法區(qū)。其中虛擬機棧的棧幀在編譯器就已經(jīng)確定大小的,隨著方法的結束或線程的技術,虛擬機棧的內(nèi)存也隨著回收。而Java堆和方法區(qū)這兩個區(qū)域則有很顯著的不確定性,這部分內(nèi)存的分配和回收都是動態(tài)的,GC所關注的真是這部分內(nèi)存該如何管理。

本篇文章就以下三方面GC所要完成的三件事:

  • 哪些內(nèi)存需要回收?(對象存活算法)
  • 什么時候回收?(觸發(fā)GC的條件)
  • 如何回收?(GC的工作原理)

哪些內(nèi)存需要回收

如何判定哪些對象需要回收?在堆里面存放了幾乎所有的對象實例,在GC之前第一件事就是要確定這些對象哪些還“存活”著,哪些已經(jīng)“死去”。其中判斷對象是否存活有兩種算法:可達性分析算法、引用計數(shù)算法。

可達性分析算法

通過一系列的“GC Roots”根對象作為始節(jié)點,開始往下遍歷有引用關系的對象形成一條“引用鏈”,通過這條引用鏈的可達的對象是存活的,不可達的對象則是不再使用。

那么什么樣的對象可以GC Roots呢?主要是以下對象:

  1. 虛擬機棧中的引用對象
  2. 方法區(qū)的類靜態(tài)的引用對象、常量的引用對象
  3. Java虛擬機內(nèi)部引用:基本數(shù)據(jù)類型的Class對象、異常對象、系統(tǒng)類加載器
  4. 所有同步鎖持有的對象
  5. 反映虛擬機內(nèi)部情況的JMXBean、JVMTI中注冊的回調(diào)、本地代碼緩存。

可達性分析算法是大多數(shù)系統(tǒng)使用的對象存活判斷算法。

引用計數(shù)算法

在對象中添加一個引用計數(shù)器,每當有一個地方引用它,計數(shù)器就+1,當一個引用失效,計數(shù)器就-1,當計數(shù)器>0時就表示對象為存活使用狀態(tài),不得回收。大多數(shù)不會用這個辦法來判斷對象存活,因為當對象實例相互引用時,當棧中的引用已經(jīng)失效,對象也還是不能回收。


上面的可達性分析算法和引用計數(shù)算法都用到了引用,這種引用默認是強引用。在JDK1.2版本之后,Java除了有強引用外,還增加了三種引用:

  • 軟引用(SoftReference): 內(nèi)存溢出前,可對持有該引用的對象回收
  • 弱引用(WeakReference):GC就會被回收,相當于沒有引用。
  • 虛引用(PhantomReference): 無法通過其獲得對象實例,唯一目的是當對象被回收時能收到一個系統(tǒng)通知。

如果對象被可達性分析算法引用計數(shù)算法識別為無使用對象就可以被GC回收嗎?不是,一個對象要至少要經(jīng)過兩個的標志過程。其還會查詢對象是否重寫了finalize()方法,如果重寫了就會用另一個Finalize線程去執(zhí)行這個finalize方法,這個方法可以時對象重新被引用,但官方并不推薦這樣做。


堆中的對象實例可以使用可達性分析算法和引用計數(shù)算法來決定是否回收。但方法區(qū)的常量池和類型信息是否回收卻另有條件:

  • 常量池的字符串和符號引用
    虛擬機中沒有任何對象有引用到它。

  • 類型卸載
    1.該類的所有實例被回收
    2.該類的ClassLoader被回收
    3.Class對象沒有被任何地方引用
    以上三種條件同時被滿足。

什么時候回收

根據(jù)兩個分代假說:絕大多數(shù)的對象都是朝生夕死,熬過越多次的GC回收的對象就越難回收。把堆進行了分代:新生代(Eden、From、To)、老年代,在GC時也進行了分代回收。

分代回收和收集算法

Minor GC: 回收新生代的無使用對象,新生代的對象的特性是大多數(shù)是朝生夕死的。觸發(fā)時機有:

  • Eden區(qū)空間不足,觸發(fā)Minor GC
    由于Eden空間大小有限,所以Minor GC觸發(fā)的更加頻繁,這就需要收集算法速度快、效率高,一般使用標記-復制算法對這一區(qū)域進行回收(后面講)。

Major GC:回收老年代的無使用對象。一般使用標記-清除算法標記-整理算法進行回收。

Full GC: 回收堆和方法區(qū)的無使用對象。Full GC回收范圍比較大,執(zhí)行的時間較長可能會造成卡頓,所以要盡量減少Full GC的次數(shù)。觸發(fā)時機大致有:

  • 老年代的空間不足
    由新生代對象的進入老年代、大對象直接進入老年代等,如果在老年代的最大連續(xù)空間上無法存放這些對象時,就會進行一次Full GC回收。

  • 方法區(qū)的空間不足
    方法區(qū)主要存儲類型信息和常量池,也有空間不足的風險,會進行Full GC回收

  • System.gc()被顯示調(diào)用會Full GC回收

三種垃圾收集算法:

1. 標記-清除算法

原理:用可達性分析算法將不可用的對象進行標記,然后對無用的對象進行清除。
缺點:在對象很多的情況下,標記的效率低。清除對象之后會產(chǎn)生內(nèi)存碎片,內(nèi)存不連續(xù)。
作用:在老年代回收中一些收集器會使用此算法

2. 標記-復制算法

原理:將內(nèi)存空間一分為二,一半用于對象的存放,一半空閑。如果存放對象的區(qū)域滿了,使用可達性分析算法把存活的對象移動標記出來,然后復制到另一個空的區(qū)域,同時把之前的區(qū)域全部清空變成空的連續(xù)空間。
缺點:如果存活對象很多,要產(chǎn)品大量的內(nèi)存復制開辟。內(nèi)存空間只能用一半優(yōu)點浪費資源。
作用:在新生代朝生夕死的對象中一般用此回收算法。但新生代中對復制算法進行了優(yōu)化,但這種算法加入了分配擔保機制防止存活對象過多分配不了的情況。使用了一種Appel式的回收算法:

Appel回收

3. 標記-整理算法

原理:標記的過程跟標記-清除算法一樣,然后整理存活的對象往一端移動,然后存活邊界之外的對象全部清除
缺點:移動對象有一定的風險。對象太多效率不高
作用:主要作用在老年代。

標記-整理算法

如何回收

GC使用的垃圾收集器進行回收,隨著不斷的發(fā)展,垃圾收集器也越來越多,這里列舉常規(guī)的垃圾收集器并進行分為三類:單線程收集器、多線程收集器、并發(fā)收集器。

單線程收集器

單線程的收集器的組合有:Serial/Serial Old收集器。它們不僅僅用一個收集線程去完成收集操作,而且在收集線程工作的時候,用戶線程必須停止等待,直到收集完成為止。如圖是Serial/Serial Old收集器示意圖:

Serial/Serial Old收集器

如果客戶端的內(nèi)存資源受限,處理器核心數(shù)較少或單核處理器來說,其簡單高效的可以使收集器最快的工作完。

多線程并行收集器

多線程的收集器有:ParNew、Parallel Scavenge、Parallel Old,其中Parallel Scavenge/Parallel Old為組合收集器。這些多線程收集器僅僅是增加了垃圾收集線程,用戶線程依然是停止等待垃圾收集的。

Parallel Scavenge/Parallel Old組合收集器

parNew收集器:其實就是Serial的多線程版本,目前能與Serial收集器和CMS收集器合作。

Parallel Scavenge收集器:一般配合Parallel Old收集器使用。相比于parNew收集器,它更加注重是吞吐量的控制,吞吐量就是用戶線程執(zhí)行的時間占總CPU運行的時間,吞吐量當然是越大越好。

多線程一般用服務端,因為多線程的執(zhí)行,有時間片輪轉(zhuǎn)的消費時間,如果對于單處理器來說無疑處理效率更慢。但對于資源很好,不用與用戶交互的分析運算的服務端卻可以增加執(zhí)行效率。

并發(fā)收集器

并發(fā)收集器有:CMS收集器,是一款以系統(tǒng)停頓的時間盡量較短,用戶體驗較好為目標的收集器。它的收集線程可以與用戶線程并發(fā)執(zhí)行。CMS有三次的標記(初始標記、并發(fā)標記、重新標記)和一次清理(并發(fā)清理),在三次的標記中有兩次標記需要較短用戶線程停止,一次較長的與用戶線程并發(fā)的標記,和與用戶線程并發(fā)的清除。

CMS收集器

初始標記:標記GC Roots關聯(lián)的第一個對象,時間很短
并發(fā)標記:和用戶線程并發(fā)執(zhí)行GC Roots的引用鏈(可達性分析算法),時間較長
重新標記:重新查找在并發(fā)標記階段,用戶線程運行生成的新的引用鏈。時間比初始標記長一點。
并發(fā)清除:用標記-清除算法把無用對象進行清除。

三大缺點:一:CPU敏感,并發(fā)對核心數(shù)少的處理器對用戶線程的運行可能會造成影響。二:浮動垃圾:在并發(fā)清理階段產(chǎn)生的垃圾只能等下一次GC回收。三:內(nèi)存碎片,標記-清理法會產(chǎn)品大量的不連續(xù)的內(nèi)存空間。

小結

本文從那些內(nèi)存需要回收,什么時候回收,如何回收作為執(zhí)行分別寫出了兩個對象存活判斷算法、Class區(qū)回收的條件、回收的分代機制與收集時機、三個收集算法和常用的垃圾收集器。

[JVM入門指南03]類加載和Android虛擬機

JVM的整體架構

JVM架構

ClassLoader:——負責加載已被編譯的java文件(.class),驗證連接。分配和初始化靜態(tài)變量和靜態(tài)代碼。

運行時數(shù)據(jù)區(qū):——負責所有的程序數(shù)據(jù):堆、方法區(qū)、棧。往期關于運行時數(shù)據(jù)區(qū)的介紹文章:[JVM入門指南01]內(nèi)存區(qū)域與溢出異常

執(zhí)行引擎:——執(zhí)行我們編譯和加載的代碼并清理產(chǎn)生垃圾的區(qū)域。關于垃圾回收清理,可以看往期的垃圾回收介紹文章:[JVM入門指南02]GC垃圾回收機制

關于運行時數(shù)據(jù)區(qū)垃圾回收機制在前面兩篇文章中已經(jīng)講過,所以這篇主要講類加載執(zhí)行引擎的執(zhí)行

類加載ClassLoader

Java類加載機制

java的類加載主要是把對.Class文件字節(jié)碼的檢查和生成Class對象。過程有:加載、驗證、準備、解析、初始化。其中驗證、準備、解析稱為連接,則過程有:加載、連接、初始化。
加載:根據(jù)類的全限定名讀取字節(jié)碼,并生成相應的Class對象。
連接

  1. 驗證: 對字節(jié)碼的驗證
  2. 準備:給靜態(tài)變量分配內(nèi)存,并給一個默認值(還沒有賦值)
  3. 解析:將常量池中的符號引用替換為直接引用。

初始化:初始化階段就是執(zhí)行類構造器<clinit>()方法的過程

java類加載

java類加載中有兩個特性:雙親委托、緩存機制
雙親委托:在加載一個類時,會先遞歸讓父類加載器去嘗試加載類,如果父類可以加載則下面的子類加載器則不用加在,如果父類加載器也不可以加載,則遞歸給子類加載器嘗試加載類。
緩存機制:加載過的Class都會被緩存起來,當需要使用到某個Class時,會先從緩存中查找該Class,沒有才從類加載器中加載。

Android類加載機制

Android的加載器類的通用父類為ClassLoader,其本身有一個內(nèi)部類BootClassLoader用于加載FrameWork層的類,ClassLoader主要實現(xiàn)了loadClass()方法用于實現(xiàn)緩存機制和雙親委托機制。BaseDexClassLoader同樣繼承ClassLoader,是PathClassLoaderDexClassLoader的父類,其中BaseDexClassLoader主要是實現(xiàn)了findClass()方法用于自己加載dex。PathClassLoader加載App安裝目錄內(nèi)的dex,DexClassLoader加載任意位置的dex,

Android類加載機制關系圖

執(zhí)行引擎

JVM的執(zhí)行引擎

Interpreter & JIT
這兩種解釋器是并肩工作的。
Interpreter解釋執(zhí)行解釋器,解釋執(zhí)行解釋器在程序運行時,會把字節(jié)碼翻譯成機器碼。解釋執(zhí)行的缺點是當一個方法被重復執(zhí)行,每一個都需要重新解釋執(zhí)行一遍。
JIT(Just In Time)即時編譯器,它會把一些經(jīng)常執(zhí)行的、大量重復執(zhí)行的熱區(qū)代碼進行即時編譯成機器碼并將其更改為本機代碼,下次執(zhí)行熱區(qū)代碼時就可以直接調(diào)用本機代碼不用再次解釋。

JVM執(zhí)行引擎解釋器

Android的執(zhí)行引擎

Android手機發(fā)展以來經(jīng)歷了兩個虛擬機:Dalvik、ART。JVM是按照有無限電量和幾乎無限的存儲的設備而設計的,但是Android設備則是電量和存儲資源都很有限,所以Android設備并沒有直接采用JVM來作為虛擬機使用,而是通過規(guī)范改造優(yōu)化后的Dalvik,在Android5.0之后更是再次更換改造后的ART。

Android的主要改造優(yōu)化有三點:

  1. 運行數(shù)據(jù)區(qū)的棧更改為寄存器,減少操作數(shù)棧的出入棧操作。
  2. JVM虛擬機接收的.class文件更改為dex文件。java/kotlin經(jīng)過java/kotlin編譯器后編譯成.class文件,這些.class文件通過dex編譯器打包編譯成dex文件。dex文件的執(zhí)行效率更高,需要的空間更少。
    Android的打包
  3. 執(zhí)行引擎除了有Interpreter和JIT編譯器,在ART中還有AOT編譯器。

Dalvik的執(zhí)行引擎

Dalvik虛擬機的執(zhí)行引擎和JVM的執(zhí)行引擎一樣,都是一般代碼在運行時通過解釋執(zhí)行解釋器編譯,熱區(qū)代碼進行即時編譯器編譯。但是Dalvik在Android5.0之后就不再使用了。

Dalvik的執(zhí)行引擎

ART的執(zhí)行引擎

在Android5.0以上,安卓改用了ART作為虛擬機,ART虛擬機中新增了一個AOT編譯器,在應用安裝的時候,AOT編譯器將 dex文件編譯為一個.oat二進制文件,App運行時直接執(zhí)行.oat文件,不用再編譯文件。這樣做,使App的運行速度更快,但也帶來了兩個問題:1. 安裝應用的時間久,2. 內(nèi)存占用較大。

ART

ART的執(zhí)行引擎后優(yōu)化為:Interpreter、JIT、AOT一起執(zhí)行

  1. 如果沒有.ort文件,程序第一次執(zhí)行使用Interpreter解釋執(zhí)行解釋器執(zhí)行。
  2. 遇到熱區(qū)代碼,則用JIT解釋器執(zhí)行,并把其存儲在一個Profils文件緩存中
  3. 設置在空閑的時刻,啟動AOT解釋器把Profils文件緩存的代碼執(zhí)行為.ort文件,下次再執(zhí)行的時候,則執(zhí)行.ort文件。
ART優(yōu)化

Android的.dex編譯

這塊不屬于Android的執(zhí)行引擎部分,但本人是學Android的,所以就順帶講講。

dex文件編譯過程

通過java/kotlin編譯器生成的.class文件,還需要經(jīng)過desugarproguard兩個步驟:
desugar: 俗稱脫糖,因為在Dalivk/ART虛擬機中并不會支持那么多的java字節(jié)碼,對于一個高版本的java的語法字節(jié)碼,要通過desugar將其轉(zhuǎn)換成安卓虛擬機能識別的代碼。
proguard: 主要做一些混淆操作等。

但這也會帶來更長時間的編譯,開發(fā)人員要等待程序運行起來的時間就越久。然后Google又把dusugardex編譯器作為合并優(yōu)化合為一個D8

D8優(yōu)化

后面Google又進一步做了優(yōu)化,又用R8代替了proguard和D8,并對字節(jié)碼做了優(yōu)化。

R8

proguard和R8的優(yōu)化有:

  1. 去掉無用的類、方法、變量
  2. 代碼優(yōu)化,如指令重排
  3. 混淆,將類、方法名進行混淆。
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

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