07.Java類加載問題

目錄介紹

  • 7.0.0.1 Java內存模型裏包含什麼?程序計數器的做用是什麼?常量池的做用是什麼?
  • 7.0.0.2 什麼是類加載器?類加載器工做機制是什麼?類加載器種類?什麼是雙親委派機制?
  • 7.0.0.3 何時發生類初始化?類初始化後對類的作了什麼,加載變量,常量,方法都內存那個位置?
  • 7.0.0.4 經過下面一個代碼案例理解類加載順序?當遇到 類名.變量 加載時,只加載變量所在類嗎?
  • 7.0.0.5 看下面這段代碼,說一下準備階段和初始化階段常量變化的原理?變量初始化過程?
  • 7.0.0.7 說收垃圾回收機制?爲何引用計數器斷定對象是否回收不可行?有哪些引用類型?
  • 7.0.0.8 談談Java的類加載過程?加載作了什麼?驗證作了什麼?準備作了什麼?解析作了什麼?初始化作了什麼?

好消息

  • 博客筆記大彙總【15年10月到至今】,包括Java基礎及深刻知識點,Android技術博客,Python學習筆記等等,還包括平時開發中遇到的bug彙總,固然也在工做之餘收集了大量的面試題,長期更新維護而且修正,持續完善……開源的文件是markdown格式的!同時也開源了生活博客,從12年起,積累共計500篇[近100萬字],將會陸續發表到網上,轉載請註明出處,謝謝!
  • 連接地址:github.com/yangchong21…
  • 若是以爲好,能夠star一下,謝謝!固然也歡迎提出建議,萬事起於忽微,量變引發質變!

7.0.0.1 Java內存模型裏包含什麼?程序計數器的做用是什麼?常量池的做用是什麼?

  • Java內存模型裏包含什麼?
    • JVM會用一段空間來存儲執行程序期間須要用到的數據和相關信息,這段空間就是運行時數據區(Runtime Data Area),也就是常說的JVM內存。JVM會將它所管理的內存劃分爲線程私有數據區和線程共享數據區兩大類。
    • 線程私有數據區包含:
      • 1.程序計數器:是一個數據結構,用於保存當前正常執行的程序的內存地址。Java虛擬機的多線程就是經過線程輪流切換並分配處理器時間來實現的,爲了線程切換後能恢復到正確的位置,每條線程都須要一個獨立的程序計數器,互不影響,該區域爲「線程私有」。
      • 2.Java虛擬機棧:線程私有的,與線程生命週期相同,用於存儲局部變量表,操做棧,方法返回值。局部變量表放着基本數據類型,還有對象的引用。
      • 3.本地方法棧:跟虛擬機棧很像,不過它是爲虛擬機使用到的Native方法服務。
    • 線程共享數據區包含:
    • 技術博客大總結
      • 4.Java堆:全部線程共享的一塊內存區域,用於存放幾乎全部的對象實例和數組;是垃圾收集器管理的主要區域,也被稱作「GC堆」;是Java虛擬機所管理的內存中最大的一塊。
      • 5.方法區:各個線程共享的區域,儲存虛擬機加載的類信息,常量,靜態變量,編譯後的代碼。
      • 6.運行時常量池:表明運行時每一個class文件中的常量表。包括幾種常量:編譯時的數字常量、方法或者域的引用。
  • 程序計數器的做用是什麼?
  • 常量池的做用是什麼?

7.0.0.2 什麼是類加載器?類加載器工做機制是什麼?類加載器種類?什麼是雙親委派機制?

  • 什麼是類加載器?
    • 負責讀取 Java 字節代碼,並轉換成java.lang.Class類的一個實例;
  • 類加載器工做機制是什麼
    • 是虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終造成可被虛擬機直接使用的Java類型的過程。另外,類型的加載、鏈接和初始化過程都是在程序運行期完成的,從而經過犧牲一些性能開銷來換取Java程序的高度靈活性。下面介紹類加載每一個階段的任務:
      • 加載(Loading):經過類的全限定名來獲取定義此類的二進制字節流;將該二進制字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構,該數據存儲數據結構由虛擬機實現自行定義;在內存中生成一個表明這個類的java.lang.Class對象,它將做爲程序訪問方法區中的這些類型數據的外部接口
      • 驗證(Verification):確保Class文件的字節流中包含的信息符合當前虛擬機的要求,包括文件格式驗證、元數據驗證、字節碼驗證和符號引用驗證
      • 準備(Preparation):爲類變量分配內存,由於這裏的變量是由方法區分配內存的,因此僅包括類變量而不包括實例變量,後者將會在對象實例化時隨着對象一塊兒分配在Java堆中;設置類變量初始值,一般狀況下零值
      • 解析(Resolution):虛擬機將常量池內的符號引用替換爲直接引用的過程
      • 初始化(Initialization):是類加載過程的最後一步,會開始真正執行類中定義的Java字節碼。而以前的類加載過程當中,除了在『加載』階段用戶應用程序可經過自定義類加載器參與以外,其他階段均由虛擬機主導和控制
  • 類加載器種類?
    • 啓動類加載器,Bootstrap ClassLoader,加載JACA_HOME\lib,或者被-Xbootclasspath參數限定的類
    • 擴展類加載器,Extension ClassLoader,加載\lib\ext,或者被java.ext.dirs系統變量指定的類
    • 應用程序類加載器,Application ClassLoader,加載ClassPath中的類庫
    • 自定義類加載器,經過繼承ClassLoader實現,通常是加載咱們的自定義類
    • 技術博客大總結
  • 什麼是雙親委派機制?
    • 主要是表示類加載器之間的層次關係
      • 前提:除了頂層啓動類加載器外,其他類加載器都應當有本身的父類加載器,且它們之間關係通常不會以繼承(Inheritance)關係來實現,而是經過組合(Composition)關係來複用父加載器的代碼。
      • 工做過程:若一個類加載器收到了類加載的請求,它先會把這個請求委派給父類加載器,並向上傳遞,最終請求都傳送到頂層的啓動類加載器中。只有當父加載器反饋本身沒法完成這個加載請求時,子加載器纔會嘗試本身去加載。

7.0.0.3 何時發生類初始化?類初始化後對類的作了什麼,加載變量,常量,方法都內存那個位置?

  • 何時發生類初始化
    • 遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,若是類沒有進行過初始化,則須要先觸發其初始化。生成這4條指令的最多見的Java代碼場景是:使用new關鍵字實例化對象的時候,讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
      • 調用一個類型的靜態方法時(即在字節碼中執行invokestatic指令)
      • 調用一個類型或接口的靜態字段,或者對這些靜態字段執行賦值操做時(即在字節碼中,執行getstatic或者putstatic指令),不過用final修飾的靜態字段除外,它被初始化爲一個編譯時常量表達式
    • 使用java.lang.reflect包的方法對類進行反射調用的時候,若是類沒有進行過初始化,則須要先觸發其初始化。
    • 當初始化一個類的時候,若是發現其父類尚未進行過初始化,則須要先觸發其父類的初始化。
    • 當虛擬機啓動時,用戶須要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
    • 當使用JDK 1.7的動態語言支持時,若是一個java.lang.invoke.MethodHandle實例左後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而且這個方法句柄鎖對應的類沒有進行過初始化時。
  • 類初始化後對類的作了什麼技術博客大總結
    • 這個階段主要是對類變量初始化,是執行類構造器的過程。
    • 換句話說,只對static修飾的變量或語句進行初始化。
    • 若是初始化一個類的時候,其父類還沒有初始化,則優先初始化其父類。
    • 若是同時包含多個靜態變量和靜態代碼塊,則按照自上而下的順序依次執行。

7.0.0.4 經過下面一個代碼案例理解類加載順序?當遇到 類名.變量 加載時,只加載變量所在類嗎?

  • 代碼案例以下所示
    class A{
        public static int value = 134;
        static{
            System.out.println("A");
        }
    }
    
    class B extends  A{
        static{
            System.out.println("B");
        }
    }
    
    
    public class Demo {
       public static void main(String args[]){
           int s = B.value;
           System.out.println(s);
       }
    }
    複製代碼
  • a.打印錯誤結果
    A 
    B
    134 
    複製代碼
  • b.打印正確結果
    A
    134 
    複製代碼
    • 觀察代碼,發現B.value中的value變量是A類的。因此,幫主在這裏大膽的猜想一下,當遇到 類名.變量 加載時,只加載變量所在類。
  • 如何作才能打印a這種結果呢?
    class A{
        public static int valueA = 134;
        static{
            System.out.println("A");
        }
    }
    
    class B extends  A{
        public static int valueB = 245;
        static{
            System.out.println("B");
        }
    }
    
    public class Demo {
       public static void main(String args[]){
           int s = B.valueB;
           System.out.println(s);
       }
    }
    複製代碼
    A
    B
    245 
    複製代碼

7.0.0.5 看下面這段代碼,說一下準備階段和初始化階段常量變化的原理?

  • 看下面這段代碼
    public static int value1  = 5;
    public static int value2  = 6;
    static{
        value2 = 66;
    }
    複製代碼
  • 準備階段和初始化階段常量變化?
    • 結果
      • 在準備階段value1和value2都等於0;
      • 在初始化階段value1和value2分別等於5和66;
  • 變量初始化過程?
    • 全部類變量初始化語句和靜態代碼塊都會在編譯時被前端編譯器放在收集器裏頭,存放到一個特殊的方法中,這個方法就是方法,即類/接口初始化方法,該方法只能在類加載的過程當中由JVM調用;
    • 編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊以前的變量;
    • 若是超類尚未被初始化,那麼優先對超類初始化,但在方法內部不會顯示調用超類的方法,由JVM負責保證一個類的方法執行以前,它的超類方法已經被執行。
    • JVM必須確保一個類在初始化的過程當中,若是是多線程須要同時初始化它,僅僅只能容許其中一個線程對其執行初始化操做,其他線程必須等待,只有在活動線程執行完對類的初始化操做以後,纔會通知正在等待的其餘線程。(因此能夠利用靜態內部類實現線程安全的單例模式)
    • 若是一個類沒有聲明任何的類變量,也沒有靜態代碼塊,那麼能夠沒有類方法;

7.0.0.7 說收垃圾回收機制?爲何引用計數器斷定對象是否回收不可行?

  • 斷定對象可回收有兩種方法:
    • 引用計數算法:
      • 給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任什麼時候刻計數器爲0的對象就是不可能再被使用的。然而在主流的Java虛擬機裏未選用引用計數算法來管理內存,主要緣由是它難以解決對象之間相互循環引用的問題,因此出現了另外一種對象存活斷定算法。
    • 可達性分析法:
      • 經過一系列被稱爲『GC Roots』的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時,則證實此對象是不可用的。其中可做爲GC Roots的對象:虛擬機棧中引用的對象,主要是指棧幀中的本地變量、本地方法棧中Native方法引用的對象、方法區中類靜態屬性引用的對象、方法區中常量引用的對象
  • 回收算法有如下四種:
    • 分代收集算法:是當前商業虛擬機都採用的一種算法,根據對象存活週期的不一樣,將Java堆劃分爲新生代和老年代,並根據各個年代的特色採用最適當的收集算法。技術博客大總結
      • 新生代:大批對象死去,只有少許存活。使用『複製算法』,只需複製少許存活對象便可。
      • 老年代:對象存活率高。使用『標記—清理算法』或者『標記—整理算法』,只需標記較少的回收對象便可。
    • 複製算法:把可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用盡後,把還存活着的對象『複製』到另一塊上面,再將這一塊內存空間一次清理掉。
    • 標記-清除算法:首先『標記』出全部須要回收的對象,而後統一『清除』全部被標記的對象。
    • 標記-整理算法:首先『標記』出全部須要回收的對象,而後進行『整理』,使得存活的對象都向一端移動,最後直接清理掉端邊界之外的內存。
  • 垃圾收集算法分類
    • 標記-清楚算法(Mark-Sweep)
      • 在標記階段,肯定全部要回收的對象,並作標記。清除階段緊隨標記階段,將標記階段肯定不可用的對象清除。標記—清除算法是基礎的收集算法,有兩個不足:1)標記和清除階段的效率不高;2)清除後回產生大量的不連續空間,這樣當程序須要分配大內存對象時,可能沒法找到足夠的連續空間。
    • 複製算法(Copying)
      • 複製算法是把內存分紅大小相等的兩塊,每次使用其中一塊,當垃圾回收的時候,把存活的對象複製到另外一塊上,而後把這塊內存整個清理掉。複製算法實現簡單,運行效率高,可是因爲每次只能使用其中的一半,形成內存的利用率不高。如今的JVM 用複製方法收集新生代,因爲新生代中大部分對象(98%)都是朝生夕死的,因此會分紅1塊大內存Eden和兩塊小內存Survivor(大概是8:1:1),每次使用1塊大內存和1塊小內存,當回收時將2塊內存中存活的對象賦值到另外一塊小內存中,而後清理剩下的。
    • 標記—整理算法(Mark-Compact)
      • 標記—整理算法和複製算法同樣,可是標記—整理算法不是把存活對象複製到另外一塊內存,而是把存活對象往內存的一端移動,而後直接回收邊界之外的內存。標記—整理算法提升了內存的利用率,而且它適合在收集對象存活時間較長的老年代。
    • 分代收集(Generational Collection)
      • 分代收集是根據對象的存活時間把內存分爲新生代和老年代,根據各代對象的存活特色,每一個代採用不一樣的垃圾回收算法。新生代採用複製算法,老年代採用標記—整理算法。
  • 爲何引用計數器斷定對象是否回收不可行?
    • 實現簡單,斷定效率高,但不能解決循環引用問題,同時計數器的增長和減小帶來額外開銷。
  • 引用類型有哪些種
    • 強引用:默認的引用方式,不會被垃圾回收,JVM寧願拋出OutOfMemory錯誤也不會回收這種對象。
    • 軟引用(SoftReference):若是一個對象只被軟引用指向,只有內存空間不足夠時,垃圾回收器纔會回收它;
    • 弱引用(WeakReference):若是一個對象只被弱引用指向,當JVM進行垃圾回收時,不管內存是否充足,都會回收該對象。
    • 虛引用(PhantomReference):虛引用和前面的軟引用、弱引用不一樣,它並不影響對象的生命週期。若是一個對象與虛引用關聯,則跟沒有引用與之關聯同樣,在任什麼時候候均可能被垃圾回收器回收。虛引用一般和ReferenceQueue配合使用。

7.0.0.8 談談Java的類加載過程?加載作了什麼?驗證作了什麼?準備作了什麼?解析作了什麼?初始化作了什麼?

  • Java文件從編碼完成到最終執行過程
    • 編譯:編譯,即把咱們寫好的java文件,經過javac命令編譯成字節碼,也就是咱們常說的.class文件。
    • 運行:運行,則是把編譯聲稱的.class文件交給Java虛擬機(JVM)執行。
    • 舉個通俗點的例子來講,JVM在執行某段代碼時,遇到了classA,然而此時內存中並無classA的相關信息,因而JVM就會到相應的class文件中去尋找classA的類信息,並加載進內存中,這就是咱們所說的類加載過程。
  • 談談Java的類加載過程?
    • 類加載的過程主要分爲三個部分:
    • 加載
    • 連接
      • 而連接又能夠細分爲三個小部分:
      • 驗證
      • 準備
      • 解析
    • 初始化
  • 加載作了什麼?
    • 加載指的是把class字節碼文件從各個來源經過類加載器裝載入內存中。
      • 這裏有兩個重點:
      • 字節碼來源。通常的加載來源包括從本地路徑下編譯生成的.class文件,從jar包中的.class文件,從遠程網絡,以及動態代理實時編譯
      • 類加載器。通常包括啓動類加載器,擴展類加載器,應用類加載器,以及用戶的自定義類加載器。
    • 在加載階段(能夠參考java.lang.ClassLoader的loadClass()方法),虛擬機須要完成如下3件事情:
      • 經過一個類的全限定名來獲取定義此類的二進制字節流(並無指明要從一個Class文件中獲取,能夠從其餘渠道,譬如:網絡、動態生成、數據庫等);
      • 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構;
      • 在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口;
    • 加載階段和鏈接階段(Linking)的部份內容(如一部分字節碼文件格式驗證動做)是交叉進行的,加載階段還沒有完成,鏈接階段可能已經開始,但這些夾在加載階段之中進行的動做,仍然屬於鏈接階段的內容,這兩個階段的開始時間仍然保持着固定的前後順序。
  • 驗證作了什麼?技術博客大總結
    • 主要是爲了保證加載進來的字節流符合虛擬機規範,不會形成安全錯誤。
    • 包括對於文件格式的驗證,好比常量中是否有不被支持的常量?文件中是否有不規範的或者附加的其餘信息?
    • 對於元數據的驗證,好比該類是否繼承了被final修飾的類?類中的字段,方法是否與父類衝突?是否出現了不合理的重載?
    • 對於字節碼的驗證,保證程序語義的合理性,好比要保證類型轉換的合理性。
    • 對於符號引用的驗證,好比校驗符號引用中經過全限定名是否可以找到對應的類?校驗符號引用中的訪問性(private,public等)是否可被當前類訪問?
  • 準備作了什麼?
    • 主要是爲類變量(注意,不是實例變量)分配內存,而且賦予初值。
    • 特別須要注意,初值,不是代碼中具體寫的初始化的值,而是Java虛擬機根據不一樣變量類型的默認初始值。
    • 好比8種基本類型的初值,默認爲0;引用類型的初值則爲null;常量的初值即爲代碼中設置的值,final static a = 123, 那麼該階段a的初值就是123
  • 解析作了什麼?
    • 將常量池內的符號引用替換爲直接引用的過程。
    • 兩個重點:
      • 符號引用。即一個字符串,可是這個字符串給出了一些可以惟一性識別一個方法,一個變量,一個類的相關信息。
      • 直接引用。能夠理解爲一個內存地址,或者一個偏移量。好比類方法,類變量的直接引用是指向方法區的指針;而實例方法,實例變量的直接引用則是從實例的頭指針開始算起到這個實例變量位置的偏移量
    • 舉個例子來講,如今調用方法hello(),這個方法的地址是1234567,那麼hello就是符號引用,1234567就是直接引用。
    • 在解析階段,虛擬機會把全部的類名,方法名,字段名這些符號引用替換爲具體的內存地址或偏移量,也就是直接引用。
  • 初始化作了什麼?
    • 這個階段主要是對類變量初始化,是執行類構造器的過程。
    • 換句話說,只對static修飾的變量或語句進行初始化。
    • 若是初始化一個類的時候,其父類還沒有初始化,則優先初始化其父類。
    • 若是同時包含多個靜態變量和靜態代碼塊,則按照自上而下的順序依次執行。

其餘介紹

01.關於博客彙總連接

02.關於個人博客

相關文章
相關標籤/搜索