Android 面試必備 - JVM 及 類加載機制

前言

最近準備更新 Android 面試必備基礎知識系列,有興趣的可以關注我的微信公衆號 stormjun94,有更新時,第一時間會在微信公衆號上面發佈,同時,也會同步在 GitHub 上面更新,如果覺得對你有所幫助的話,請幫忙 star。

Android 面試必備 - http 與 https 協議

Android 面試必備 - 計算機網絡基本知識(TCP,UDP,Http,https)

Android 面試必備 - 線程

Android 面試必備 - JVM 及 類加載機制

Android_interview github 地址


java 內存分區

參考博客

JVM所管理的內存分爲以下幾個運行時數據區:程序計數器、Java虛擬機棧、本地方法棧、Java堆、方法區。

程序計數器(Program Counter Register)

一塊較小的內存空間,它是當前線程所執行的字節碼的行號指示器,字節碼解釋器工作時通過改變該計數器的值來選擇下一條需要執行的字節碼指令,分支、跳轉、循環等基礎功能都要依賴它來實現。每條線程都有一個獨立的的程序計數器,各線程間的計數器互不影響,因此該區域是線程私有的。

當線程在執行一個Java方法時,該計數器記錄的是正在執行的虛擬機字節碼指令的地址,當線程在執行的是Native方法(調用本地操作系統方法)時,該計數器的值爲空。另外,該內存區域是唯一一個在Java虛擬機規範中麼有規定任何OOM(內存溢出:OutOfMemoryError)情況的區域。

Java虛擬機棧(Java Virtual Machine Stacks)

該區域也是線程私有的,它的生命週期也與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每個方法被執行的時候都會同時創建一個棧幀,棧它是用於支持續虛擬機進行方法調用和方法執行的數據結構。對於執行引擎來講,活動線程中,只有棧頂的棧幀是有效的,稱爲當前棧幀,這個棧幀所關聯的方法稱爲當前方法,執行引擎所運行的所有字節碼指令都只針對當前棧幀進行操作。棧幀用於存儲局部變量表、操作數棧、動態鏈接、方法返回地址和一些額外的附加信息。在編譯程序代碼時,棧幀中需要多大的局部變量表、多深的操作數棧都已經完全確定了,並且寫入了方法表的Code屬性之中。因此,一個棧幀需要分配多少內存,不會受到程序運行期變量數據的影響,而僅僅取決於具體的虛擬機實現。

本地方法棧(Native Method Stacks)

該區域與虛擬機棧所發揮的作用非常相似,只是虛擬機棧爲虛擬機執行Java方法服務,而本地方法棧則爲使用到的本地操作系統(Native)方法服務。

Java堆(Java Heap)

Java Heap是Java虛擬機所管理的內存中最大的一塊,它是所有線程共享的一塊內存區域。幾乎所有的對象實例和數組都在這類分配內存。Java Heap是垃圾收集器管理的主要區域,因此很多時候也被稱爲“GC堆”。

根據Java虛擬機規範的規定,Java堆可以處在物理上不連續的內存空間中,只要邏輯上是連續的即可。如果在堆中沒有內存可分配時,並且堆也無法擴展時,將會拋出OutOfMemoryError異常。

方法區(Method Area)

方法區也是各個線程共享的內存區域,它用於存儲已經被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。方法區域又被稱爲“永久代”,但這僅僅對於Sun HotSpot來講,JRockit和IBM J9虛擬機中並不存在永久代的概念。Java虛擬機規範把方法區描述爲Java堆的一個邏輯部分,而且它和Java Heap一樣不需要連續的內存,可以選擇固定大小或可擴展,另外,虛擬機規範允許該區域可以選擇不實現垃圾回收。相對而言,垃圾收集行爲在這個區域比較少出現。該區域的內存回收目標主要針是對廢棄常量的和無用類的回收。運行時常量池是方法區的一部分,Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Class文件常量池),用於存放編譯器生成的各種字面量和符號引用,這部分內容將在類加載後存放到方法區的運行時常量池中。運行時常量池相對於Class文件常量池的另一個重要特徵是具備動態性,Java語言並不要求常量一定只能在編譯期產生,也就是並非預置入Class文件中的常量池的內容才能進入方法區的運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用比較多的是String類的intern()方法。


JVM 垃圾回收器

垃圾回收器的類型大概有以下幾種:

串行回收(Serial)和並行回收(Parallel):串行回收就是不過有多少個CPU,始終只用一個CPU來執行垃圾回收操作,而並行回收就是把整個回收工作拆分成多個部分,每個部分由一個CPU來負責,從而讓多個CPU並行回收,並行回收的執行效率很高,但是複雜度增加,另外一個使內存碎片會增加。

併發執行和應用程序停止:應用程序停止的垃圾回收方式會在執行垃圾回收的同時導致應用程序的暫停,併發執行的垃圾回收雖然不會導致應用程序的暫停,但是由於併發執行垃圾回收需要解決和應用程序的執行衝突(應用程序可能會在垃圾回收過程中修改對象),因此,可能執行開銷比應用程序停止更高,而且執行時也需要更多的內存。

壓縮和不壓縮:爲了減少內存碎片,支持壓縮的垃圾回收器會把所有的獲得對象搬遷到一起,然後邊界之前的佔用內存全部回收。優點是:解決了內存碎片問題。缺點是速度較慢。

不壓縮的垃圾回收方式:回收速度快,但分配內存慢,且無法解決內存碎片問題。

新生代和老年代

垃圾回收算法大概可分爲以下幾種算法

複製式的垃圾回收:把可達的對象複製到另外一個區域,再對這個區域另外清楚,回收過程不會長生內存碎片,但是需要複製數據和額外的內存。

標記-清除:先遍歷一次,可達的標爲可達,然後再遍歷一次,把沒標記爲可達的進行回收。

標記-整理:從根開始訪問,標可達,再遍歷,把可達的複製到一起,回收不可達的內存
分代回收:按對象的存活時間,我們知道java的 大多數對象存活時間比較短,分爲三個代,按代回收。

現代垃圾回收器設計思想

現行的垃圾回收器採用分代的方式來採用不用的回收設計,分代的基本思路是根據對象生存時間的長短,把堆內存分成3個代:

Young(年輕代):由1個Eden區和2個Survior構成,絕大多數對象先分配到Eden區中,當垃圾回收機制來臨的時候,會把Eden去還存活的對象複製到survior區(其中一個),當surivior區滿的時候,會把surivior還存活的對象存放到另外一個survior去中,當這個survior去也滿的時候,會把從第一個survior區複製過來還存活的對象存放到老年代

Old(年老代):在年輕代中經歷了N次垃圾回收後仍然存活的對象,就會被放到年老代中,因此可以說存放生命週期比較長的對象

Permanent(永久代): 用於存放靜態文件,如今Java類、方法等。持久代對垃圾回收沒有顯著影響持久代大小通過-XX:MaxPermSize=<N>進行設置。

GC 的兩種類型

GC有兩種類型:Scavenge GC和Full GC。

Scavenge GC:一般情況下,當新對象生成,並且在Eden申請空間失敗時,就會觸發Scavenge GC,對Eden區域進行GC,清除非存活對象,並且把尚且存活的對象移動到Survivor區。然後整理Survivor的兩個區。這種方式的GC是對年輕代的Eden區進行,不會影響到年老代。因爲大部分對象都是從Eden區開始的,同時Eden區不會分配的很大,所以Eden區的GC會頻繁進行。因而,一般在這裏需要使用速度快、效率高的算法,使Eden去能儘快空閒出來。一般使用複製算法。

Full GC:對整個堆進行整理,包括Young、Tenured和Perm。Full GC因爲需要對整個對進行回收,所以比Scavenge GC要慢,因此應該儘可能減少Full GC的次數。在對JVM調優的過程中,很大一部分工作就是對於FullGC的調節。有如下原因可能導致Full GC:

  • 年老代(Tenured)被寫滿
  • 持久代(Perm)被寫滿
  • System.gc()被顯示調用
  • 上一次GC之後Heap的各域分配策略動態變化

對象如何實現自我救贖

我們知道要宣告一個對象死亡,至少需要兩次標記。
第一次標記會從GC Root對象開始遍歷,看是否存在相應的引用鏈,如果沒有的話,那麼 它 會被第一次標誌並且進行第一次篩選,判斷是否有必要執行篩選的條件是
是否覆蓋了 Finalize()方法 ,覆蓋了,有必要執行,沒覆蓋,沒必要執行
finilize方法 是否被虛擬機調用 過,沒有的話,有覆蓋finalize方法,執行篩選
如果這個對象 被判定有必要執行 finalize方法 ,會把它放到F-Queue的隊列當中 ,隨後jvm會啓動一個低優先級的線程來執行,如果在執行finalize 方法的 時候該對象有引用指向它,那麼它可以實現自我救贖。


類加載過程

類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括加載、驗證、準備、解析、初始化、使用、卸載。

其中類加載的過程包括了加載、驗證、準備、解析、初始化五個階段。在這五個階段中,加載、驗證、準備和初始化這四個階段發生的順序是確定的,而解析階段則不一定,它在某些情況下可以在初始化階段之後開始,這是爲了支持Java語言的運行時綁定(也成爲動態綁定或晚期綁定)。另外注意這裏的幾個階段是按順序開始,而不是按順序進行或完成,因爲這些階段通常都是互相交叉地混合進行的,通常在一個階段執行的過程中調用或激活另一個階段。

“加載”(Loading)階段是“類加載”(Class Loading)過程的第一個階段,在此階段,虛擬機需要完成以下三件事情:

  1. 通過一個類的全限定名來獲取定義此類的二進制字節流。
  2. 將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構。
  3. 在Java堆中生成一個代表這個類的java.lang.Class對象,作爲方法區這些數據的訪問入口。

驗證是連接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。

準備階段是爲類的靜態變量分配內存並將其初始化爲默認值,這些內存都將在方法區中進行分配。準備階段不分配類中的實例變量的內存,實例變量將會在對象實例化時隨着對象一起分配在Java堆中。

解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。

類初始化是類加載過程的最後一步,前面的類加載過程,除了在加載階段用戶應用程序可以通過自定義類加載器參與之外,其餘動作完全由虛擬機主導和控制。到了初始化階段,才真正開始執行類中定義的Java程序代碼。


雙親委託機制

雙親委派模型:Bootstrap ClassLoader、Extension ClassLoader、ApplicationClassLoader。

  1. 啓動類加載器,負責將存放在<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath參數所指定的路徑中,並且是虛擬機識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即時放在lib目錄中也不會被加載)類庫加載到虛擬機內存中。啓動類加載器無法被java程序直接引用。
  2. 擴展類加載器:負責加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的所有類庫,開發者可以直接使用該類加載器。
  3. 應用程序類加載器:負責加載用戶路徑上所指定的類庫,開發者可以直接使用這個類加載器,也是默認的類加載器。
    三種加載器的關係:啓動類加載器->擴展類加載器->應用程序類加載器->自定義類加載器。

這種關係即爲類加載器的雙親委派模型。其要求除啓動類加載器外,其餘的類加載器都應當有自己的父類加載器。這裏類加載器之間的父子關係一般不以繼承關係實現,而是用組合的方式來複用父類的代碼。

雙親委派模型的工作過程:如果一個類加載器接收到了類加載的請求,它首先把這個請求委託給他的父類加載器去完成,每個層次的類加載器都是如此,因此所有的加載請求都應該傳送到頂層的啓動類加載器中,只有當父加載器反饋自己無法完成這個加載請求(它在搜索範圍中沒有找到所需的類)時,子加載器纔會嘗試自己去加載。

好處:java類隨着它的類加載器一起具備了一種帶有優先級的層次關係。例如類java.lang.Object,它存放在rt.jar中,無論哪個類加載器要加載這個類,最終都會委派給啓動類加載器進行加載,因此Object類在程序的各種類加載器環境中都是同一個類。相反,如果用戶自己寫了一個名爲java.lang.Object的類,並放在程序的Classpath中,那系統中將會出現多個不同的Object類,java類型體系中最基礎的行爲也無法保證,應用程序也會變得一片混亂。

實現:在java.lang.ClassLoader的loadClass()方法中,先檢查是否已經被加載過,若沒有加載則調用父類加載器的loadClass()方法,若父加載器爲空則默認使用啓動類加載器作爲父加載器。如果父加載失敗,則拋出ClassNotFoundException異常後,再調用自己的findClass()方法進行加載。


推薦閱讀

Android 面試必備 - http 與 https 協議

Android 面試必備 - 計算機網絡基本知識(TCP,UDP,Http,https)

Android 面試必備 - 線程

Android 面試必備 - JVM 及 類加載機制

Android_interview github 地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章