1. 程式人生 > >07.Java類加載問題

07.Java類加載問題

對象實例化 編碼 omr 構造器 ets 哪些 識別 尋找 算法實現

目錄介紹
  • 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萬字],將會陸續發表到網上,轉載請註明出處,謝謝!
  • 鏈接地址:https://github.com/yangchong211/YCBlogs
  • 如果覺得好,可以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;
  • 變量初始化過程?
    • 所有類變量初始化語句和靜態代碼塊都會在編譯時被前端編譯器放在收集器裏頭,存放到一個特殊的方法中,這個方法就是<clinit>方法,即類/接口初始化方法,該方法只能在類加載的過程中由JVM調用;
    • 編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變量;
    • 如果超類還沒有被初始化,那麽優先對超類初始化,但在<clinit>方法內部不會顯示調用超類的<clinit>方法,由JVM負責保證一個類的<clinit>方法執行之前,它的超類<clinit>方法已經被執行。
    • JVM必須確保一個類在初始化的過程中,如果是多線程需要同時初始化它,僅僅只能允許其中一個線程對其執行初始化操作,其余線程必須等待,只有在活動線程執行完對類的初始化操作之後,才會通知正在等待的其他線程。(所以可以利用靜態內部類實現線程安全的單例模式)
    • 如果一個類沒有聲明任何的類變量,也沒有靜態代碼塊,那麽可以沒有類<clinit>方法;

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.關於博客匯總鏈接

  • 1.技術博客匯總
  • 2.開源項目匯總
  • 3.生活博客匯總
  • 4.喜馬拉雅音頻匯總
  • 5.其他匯總

02.關於我的博客

  • 我的個人站點:www.yczbj.org,www.ycbjie.cn
  • github:https://github.com/yangchong211
  • 知乎:https://www.zhihu.com/people/yang-chong-69-24/pins/posts
  • 簡書:http://www.jianshu.com/u/b7b2c6ed9284
  • csdn:http://my.csdn.net/m0_37700275
  • 喜馬拉雅聽書:http://www.ximalaya.com/zhubo/71989305/
  • 開源中國:https://my.oschina.net/zbj1618/blog
  • 泡在網上的日子:http://www.jcodecraeer.com/member/content_list.php?channelid=1
  • 郵箱:[email protected]
  • 阿裏雲博客:https://yq.aliyun.com/users/article?spm=5176.100- 239.headeruserinfo.3.dT4bcV
  • segmentfault頭條:https://segmentfault.com/u/xiangjianyu/articles
  • 掘金:https://juejin.im/user/5939433efe88c2006afa0c6e

07.Java類加載問題