Android 高級面試-4:虛擬機相關

內存管理屬於基礎知識組織下語言便可,內存模型放在 Java 併發相關;虛擬機執行系統是重點,包括,類加載機制(類的加載、校驗階段,與熱補丁原理相關)java

學習這一塊的內容能夠參考:android

一、內存管理

  • GC 回收策略
  • Java 中內存區域與垃圾回收機制
  • 垃圾回收機制與調用 System.gc() 區別
  1. 標記-清除算法:這種算法直接在內存中把須要回收的對象「摳」出來。效率不高,清除以後會產生內容碎片,形成內存不連續,當分配較大內存對象時可能會因內存不足而觸發垃圾收集動做。
  2. 標記-整理算法:相似於標記-清除算法,只是回收了以後,它要對內存空間進行整理,以使得剩餘的對象佔用連續的存儲空間。
  3. 複製算法:將內存分紅兩塊,一次只在一塊內存中進行分配,垃圾回收一次以後,就將該內存中的未被回收的對象移動到另外一塊內存中,而後將該內存一次清理掉。
  4. 分代收集算法:根據對象存活週期的不一樣將內存劃分紅幾塊,而後根據其特色採用不一樣的回收算法。

System.gc() 函數的做用只是提醒虛擬機:程序員但願進行一次垃圾回收。可是它不能保證垃圾回收必定會進行,並且具體何時進行是取決於具體的虛擬機的,不一樣的虛擬機有不一樣的對策。git

  • Java 中對象的生命週期

一個類從被加載到虛擬機內存到卸載的整個生命週期包括:加載-驗證-準備-解析-初始化-使用-卸載 7 個階段。其中 驗證-準備-解析 3 個階段稱爲鏈接。程序員

加載發生在類被使用的時候,若是一個類以前沒有被加載,那麼就會執行加載邏輯,好比當使用new 建立類、調用靜態類對象和使用反射的時候等。加載過程主要工做包括:1). 從磁盤或者網絡中獲取類的二進制字節流;2). 將該字節流的靜態存儲結構轉換爲方法取的運行時數據結構;3). 在內存中生成表示這個類的 Class 對象,做爲方法區訪問該類的各類數據結構的入口。github

驗證階段會對加載的字節流中的信息進行各類校驗以確保它符合JVM的要求。面試

準備階段會正式爲類變量分配內存並設置類變量的初始值。注意這裏分配內存的只包括類變量,也就是靜態的變量(實例變量會在對象實例化的時候分配在堆上),而且這裏的設置初始值是指‘零值’,好比int類型的會被初始化爲 0,引用類型的會被初始化爲 null,即便你在代碼中爲其賦了值。算法

解析階段是將常量池中的符號引用替換爲直接引用的過程。符號引用與虛擬機實現的佈局無關,引用的目標並不必定要已經加載到內存中。各類虛擬機實現的內存佈局能夠各不相同,可是它們能接受的符號引用必須是一致的,只要能正肯定位到它們在內存中的位置就行。直接引用能夠是指向目標的指針,相對偏移量或是一個能間接定位到目標的句柄。若是有了直接引用,那引用的目標一定已經在內存中存在。編程

初始化是執行類構造器 <client> 方法的過程。<client> 方法是由編譯器自動收集類中的類變量的賦值操做和靜態語句塊中的語句合併而成的。虛擬機會保證 <client> 方法執行以前,父類的 <client> 方法已經執行完畢。數組

  • JVM 內存區域,開線程影響哪塊內存

內存區域大體的分佈圖,與線程對應以後的分佈圖緩存

JVM內存區域

圖中由淺藍色標識的部分是全部線程共享的數據區;淡紫色標識的部分是每一個線程私有的數據區域

  1. 程序計數器線程私有,用來指示當前線程所執行的字節碼的行號,就是用來標記線程如今執行的代碼的位置;對 Java 方法,它存儲的是字節碼指令的地址;對於 Native 方法,該計數器的值爲空。
  2. 線程私有,與線程同時建立,總數與線程關聯,表明Java方法執行的內存模型。每一個方法執行時都會建立一個棧楨來存儲方法的的變量表、操做數棧、動態連接方法、返回值、返回地址等信息。一個方法的執行和退出就是用一個棧幀的入棧和出棧表示的。一般咱們不容許你使用遞歸就是由於,方法就是一個棧,太多的方法只執行而沒有退出就會致使棧溢出,不過能夠經過尾遞歸優化。棧又分爲虛擬機棧和本地方法棧,一個對應 Java 方法,一個對應 Native 方法。
  3. :用來給對象分配內存的,幾乎全部的對象實例(包括數組)都在上面分配。它是垃圾收集器的主要管理區域,所以也叫 GC 堆。它其實是一塊內存區域,因爲一些收集算法的緣由,又將其細化分爲新生代和老年代等。若是在堆中沒有內存完成實例分配,而且堆也沒法再擴展時,將會拋出 OutOfMemoryError 異常。
  4. 方法區:方法區由多線程共享,用來存儲類信息、常量、靜態變量、即便編譯後的代碼等數據。運行時常量池是方法區的一部分,它用於存放編譯器生成的各類字面量和符號引用,好比字符串常量等。根據 Java 虛擬機規範的規定,當方法區沒法知足內存分配需求時,將拋出 OutOfMemoryError 異常。
  • 軟引用、弱引用區別
  • Java 中的四種引用
  • 強引用置爲 null,會不會被回收?

四種引用類型:強引用、軟引用、弱引用和虛引用。

  1. 當使用 new 關鍵字建立一個對象的時候,這個對象就是強引用的,它絕對不會被回收,即便內存耗盡。你能夠經過將其置爲 null 來弱化對其的引用,但何時被回收還要取決於 GC 算法。
  2. 軟引用和弱引用類似,你能夠分別經過 SoftReference<T>WeakReference<T> 來使用它們,它們的區別在於後者更弱一些。當 JVM 進行垃圾回收時,不管內存是否充足,都會回收被弱引用關聯的對象;而軟引用關聯着的對象,只有在內存不足的時候 JVM 纔會回收該對象。軟引用能夠用來作緩存,由於當 JVM 內存不足的時候纔會被回收;而弱引用適合 Android 上面引用 Activity 等的時候使用,由於 Activity 被銷燬不必定是由於內存不足,多是正常的生命週期結束。若是此時使用軟引用,而 JVM 內存仍然足夠,則仍然會持有 Activity 的引用而形成內存泄漏。
  3. 虛引用在任什麼時候候均可能被垃圾回收器回收。

當一個對象再也不被引用的時候,該對象也不必定被回收,理論上它還有一次救贖的機會,即經過覆寫 finilize() 方法把對本身的引用從弱變強,即把本身賦值給全局的對象等。由於當對象不可達的時候,只有當 finilize() 沒被覆寫,或者 finilize() 已經被調用過,則該對象會被回收。不然,它會被放在一個隊列中,並在稍後由一個低優先級的 Finilizer 線程執行它。

  • 垃圾收集機制 對象建立,新生代與老年代

實際虛擬機的內存區域就是一整塊內存,不區分新生代與老年代。新生代與老年代是垃圾收集器爲了使用不一樣收集策略而定義的名稱。

JVM 內存各個區域的名稱

內存分配的策略是:1). 對象優先在Eden分配;2). 大對象直接進入老年代;3). 長期存活對象將進入老年代。

咱們以前有一次線上的問題就是代碼中查詢了太多的數據,致使大對象直接進入了老年代,查詢頻繁,致使虛擬機 GC 頻繁,進入假死狀態(停頓)。

新生代:主要是用來存放新生的對象。通常佔據堆的 1/3 空間。因爲頻繁建立對象,因此新生代會頻繁觸發 MinorGC 進行垃圾回收。

新生代又分爲 Eden 區、ServivorFrom、ServivorTo 三個區。

Eden 區:Java 新對象的出生地(若是新建立的對象佔用內存很大,則直接分配到老年代)。當Eden 區內存不夠的時候就會觸發 MinorGC,對新生代區進行一次垃圾回收。
ServivorTo:保留了一次 MinorGC 過程當中的倖存者。
ServivorFrom:上一次 GC 的倖存者,做爲這一次 GC 的被掃描者。

MinorGC 的過程:MinorGC 採用複製算法。首先,把 Eden 和 ServivorFrom 區域中存活的對象複製到 ServicorTo 區域(若是有對象的年齡以及達到了老年的標準,則賦值到老年代區),同時把這些對象的年齡+1(若是 ServicorTo 不夠位置了就放到老年區);而後,清空 Eden 和 ServicorFrom 中的對象;最後,ServicorTo 和 ServicorFrom 互換,原 ServicorTo 成爲下一次 GC 時的 ServicorFrom 區。

老年代:主要存放應用程序中生命週期長的內存對象。老年代的對象比較穩定,因此 MajorGC 不會頻繁執行。MajorGC 前通常都先進行了一次 MinorGC,使得有新生代的對象進入老年代,致使空間不夠用時才觸發。當沒法找到足夠大的連續空間分配給新建立的較大對象時也會提早觸發一次 MajorGC 進行垃圾回收騰出空間。

當老年代也滿了裝不下的時候,就會拋出 OOM 異常。

至於老年代究竟使用哪一種垃圾收集算法其實是由垃圾收集器來決定的。老年代、新生代以及新生代的各個內存區域之間的比例並非固定的,咱們可使用參數來配置。

二、虛擬機執行系統

  • 談談類加載器 classloader
  • 類加載機制,雙親委派模型

Android 中的類加載器與 Java 中的類加載器基本一致,都分紅系統類加載器和用戶自定義類加載器兩種類型。Java 中的系統類加載器包括,Bootstrap 類加載器,主要用來加載 java 運行時下面的 lib 目錄;拓展類加載器 ExtClassLoader,用來加載 Java 運行時的 lib 中的拓展目錄;應用程序類加載器,用來加載當前程序的 ClassPath 目錄下面的類。其中,引導類加載器與其餘兩個不一樣,它是在 C++ 層實現的,沒有繼承 ClassLoader 類,也沒法獲取到。

Android 中的系統類加載器包括,BootClassLoader, 用來加載經常使用的啓動類;DexClassLoader 用來加載 dex 及包含 dex 的壓縮文件;PathClassLoader 用來加載系統類和應用程序的類。三種類加載器都是在系統啓動的過程當中建立的。DexClassLoader 和 PathClassLoader 都繼承於 BaseDexClassLoader。區別在於調用父類構造器時,DexClassLoader 多傳了一個 optimizedDirectory 參數,這個目錄必須是內部存儲路徑,用來緩存系統建立的 Dex 文件。而 PathClassLoader 該參數爲 null,只能加載內部存儲目錄的 Dex 文件。因此咱們能夠用 DexClassLoader 去加載外部的 Apk.

public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
    super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
複製代碼

DexClassLoader 重載了 findClass() 方法,在加載類時會調用其內部的 DexPathList 去加載。DexPathList 是在構造 DexClassLoader 時生成的,其內部包含了 DexFile。騰訊的 qq 空間熱修復技術正是利用了 DexClassLoader 的加載機制,將須要替換的類添加到 dexElements 的前面,這樣系統會使用先找到的修復過的類。

private final DexPathList pathList;
    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(parent);
        this.originalPath = dexPath;
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class clazz = pathList.findClass(name);
        return clazz;
    }
複製代碼

本質上不論哪一種加載器加載類的時候的都是分紅兩個步驟進行的,第一是使用雙親委派模型的規則從資源當中加載類的數據到內存中。一般類都被存儲在各類文件中,因此這無非就是一些文件的讀寫操做。當將數據讀取到內存當中以後會調用 defineClass() 方法,並返回類的類型 Class 對象。這個方法底層會調用一個 native 的方法來最終完成類的加載工做。

至於雙親委派模型,這是 Java 中類加載的一種規則,比較容易理解。前提是類加載器之間存在着繼承關係,那麼當一個類進行加載以前會先判斷這個類是否已經存在。若是已經存在,則直接返回對應的 Class 便可。若是不存在則先交給本身的父類進行加載,父類加載不到,而後本身再進行加載。這樣一層層地傳遞下去,一個類的加載將是從父類開始到子類的過程,因此叫雙親委派模型。這種加載機制的好處是:第一,它能夠避免重複加載,已經加載一次的類就無需再次加載;第二,更加安全,由於類優先交給父類進行加載,按照傳遞規則,也就是先交給系統的類進行加載。那麼若是有人想要僞造一個 Object 類型,想要矇混過關的話,顯然是逃不過虛擬機的法眼了。

Android 的 ClassLoader 定義在 Dalivk 目錄下面,這裏是它在 AOSP 中的位置:dalvik-system

  • 動態加載
  • 對動態加載(OSGI)的瞭解?

OSGI 一種用來實現 Java 模塊化的方式,在 2010 年左右的時候比較火,如今用得比較少了。

三、內存模型

梳理下內存模型,組織一下語言

  • JVM 內存模型,內存區域
  • JVM 內存模型

Java 內存模型,即 Java Memory Model,簡稱 JMM,它是一種抽象的概念,或者是一種協議,用來解決在併發編程過程當中內存訪問的問題,同時又能夠兼容不一樣的硬件和操做系統。

在 Java 內存模型中,全部的變量都存儲在主內存。每一個 Java 線程都存在着本身的工做內存,工做內存中保存了該線程用獲得的變量的副本,線程對變量的讀寫都在工做內存中完成,沒法直接操做主內存,也沒法直接訪問其餘線程的工做內存。當一個線程之間的變量的值的傳遞必須通過主內存。

當兩個線程 A 和線程 B 之間要完成通訊的話,要經歷以下兩步:首先,線程 A 從主內存中將共享變量讀入線程 A 的工做內存後並進行操做,以後將數據從新寫回到主內存中;而後,線程 B 從主存中讀取最新的共享變量。

此外,內存模型還規定了

  1. 主內存和工做內存交互的 8 種操做及其規則;
  2. 提供了 voliate 關鍵字用來,保證變量的可見性,和屏蔽指令重排序;
  3. 對 long 及 double 的特殊規定:讀寫操做分紅兩個 32 位操做;
  4. 先行發生原則 (happens-before) 和 as-if-serial 語義(無論怎麼重排序,程序的執行結果不能被改變)。

四、Android 虛擬機

  • ART 和 Dalvik (DVM) 的區別

ART 4.4 時發佈,5.0 以後默認使用 ART.

  1. ART 在應用安裝時會進行預編譯 (ahead of time compilation, AOT),將字節碼編譯成機器碼並存儲在本地,這樣每次運行程序時就無需編譯了,提高了效率。缺點是:1).安裝耗時更長了;2).佔用更多存儲空間。7.0 以後,ART 引入 JIT,安裝時不會將字節碼所有編譯成機器碼,而是運行時將熱點代碼編譯成機器碼。
  2. DVM 是爲 32 位 CPU 設計的,ART 支持 64 位且兼容 32 位 CPU.
  3. ART 對垃圾收集機制進行了改進,將 GC 暫停由 2 次改爲了 1 次等。
  4. ART 的運行時堆空間劃分與 DVM 不一樣
  • DVM 與 JVM 的區別
  1. 基於的架構不一樣:DVM 基於寄存器,相比於 JVM(基於棧),執行速度更快(由於無需到棧中讀取數據)。
  2. 執行的字節碼不一樣:DVM 在執行的是 dex 文件,通過 class 經 dx 轉換以後的。dex 會對 class 進行優化,整個 class,取出冗餘信息,加快加載方式。
  3. DVM 容許在有限的空間內同時運行多個進程
  4. DVM 由 Zygote 建立和初始化 Zygote 是一個 DVM 進程,當須要建立一個應用程序時,Zygote 經過 fork 自身來建立新的 DVM 實例。
  5. DVM 有共享機制,不一樣應用在運行時能夠共享相同的類。
  6. DVM 早期沒有使用 JIT 編譯器,JIT 就是即時編譯器,早期的 DVM 須要通過解釋器將 dex 碼編譯成機器碼,效率不高。2.2 以後使用了 JIT,會對熱點代碼進行編譯,生成本地機器碼,下次執行到相同的邏輯時,能夠直接執行本地機器碼,無需每次編譯。
  • DVM 與 ART 的誕生

init 進程啓動 Zygote 時會調用 app_main.cpp,它會調用 AndroidRuntime 的 start() 函數,在其中經過 startVM() 方法啓動虛擬機。在啓動虛擬機以前會經過讀取系統的屬性,判斷使用 DVM 仍是 ART 虛擬機實例。


Android 高級面試系列文章,關注做者及時獲取更多面試資料

本系列以及其餘系列的文章均維護在 Github 上面:Github / Android-notes,歡迎 Star & Fork. 若是你喜歡這篇文章,願意支持做者的工做,請爲這篇文章點個贊👍!

相關文章
相關標籤/搜索