類的加載機制

目錄介紹

  • 01.Java對象的建立過程php

    • 1.0 看下建立類加載過程
    • 1.1 對象的建立
    • 1.2 對象的內存佈局
  • 02.Java內存區域java

    • 2.0 運行時數據區域
    • 2.1 程序計數器
    • 2.2 虛擬機棧
    • 2.3 本地方法棧
    • 2.4 Java堆
    • 2.5 方法區
    • 2.6 運行時常量池
    • 2.7 直接內存
  • 03.Java對象的訪問定位方式git

    • 3.1 句柄
    • 3.2 直接指針
  • 04.Java對象銷燬分析程序員

    • 4.1 JVM內存分配與回收
    • 4.2 判斷對象是否死亡
    • 4.3 不可達的對象並不是「非死不可」
    • 4.4 如何判斷一個常量是廢棄常量
    • 4.5 如何判斷一個類是無用的類
    • 4.6 GC回收算法詳解
  • 05.String類和常量池github

    • 5.1 String對象的兩種建立方式
    • 5.2 String類型的常量池

好消息

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

問題思考答疑

  • 說一下建立一個對象,類的加載過程。類信息,常量,變量,方法分別放到內存中哪裏?
  • 對於運行時數據區域,哪些是私有的,哪些是共享的,爲何要這樣設計?
  • 程序計數器會出現OOM嗎?它的生命週期是怎麼樣的?
  • 本地方法棧和Java虛擬機棧有什麼區別?本地方法棧在什麼狀況下會形成OOM?
  • java堆主要是作什麼做用的?
  • 什麼是類的加載檢查,主要檢查什麼,如何檢查呢?
  • Java對象訪問定位方式有哪些?主要有什麼區別?爲何說使用指針效率更高?
  • String類能夠new嗎?直接new和賦值的內容有什麼區別,分別放在內存中什麼地方?
  • 如何判斷對象是否死亡(兩種方法)。若是有不一樣方法,那麼之間有什麼區別?
  • 簡單的介紹一下強引用、軟引用、弱引用、虛引用(虛引用與軟引用和弱引用的區別、使用軟引用能帶來的好處)。
  • 如何判斷一個常量是廢棄常量,如何判斷一個類是無用的類?
  • 垃圾收集有哪些算法,各自的特色?常見的垃圾回收器有那些?
  • HotSpot爲何要分爲新生代和老年代?
  • 介紹一下CMS,G1收集器。Minor Gc和Full GC 有什麼不一樣呢?

01.Java對象的建立過程

1.1 看下建立類加載過程

  • Person p = new Person()請寫一下類的加載過程?面試

    1).由於new用到了Person.class,因此會先找到Person.class文件,並加載到內存中;
    2).執行該類中的static代碼塊,若是有的話,給Person.class類進行初始化;
    3).在堆內存中開闢空間分配內存地址;
    4).在堆內存中創建對象的特有屬性,並進行默認初始化;
    5).對屬性進行顯示初始化;
    6).對對象進行構造代碼塊初始化;
    7).對對象進行與之對應的構造函數進行初始化;
    8).將內存地址付給棧內存中的p變量

1.1 對象的建立

  • Java對象的建立過程,我建議最好是能默寫出來,而且要掌握每一步在作什麼。算法

    • 1.類加載檢查
    • 2.分配內存
    • 3.初始化零值
    • 4.設置對象頭
    • 5.執行init方法
  • ①類加載檢查:segmentfault

    • 虛擬機遇到一條 new 指令時,首先將去檢查這個指令的參數是否能在常量池中定位到這個類的符號引用,而且檢查這個符號引用表明的類是否已被加載過、解析和初始化過。若是沒有,那必須先執行相應的類加載過程。
  • ②分配內存:緩存

    • 類加載檢查經過後,接下來虛擬機將爲新生對象分配內存。對象所需的內存大小在類加載完成後即可肯定,爲對象分配空間的任務等同於把一塊肯定大小的內存從 Java 堆中劃分出來。分配方式「指針碰撞」「空閒列表」 兩種,選擇那種分配方式由 Java 堆是否規整決定,而Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定
    • 內存分配的兩種方式:安全

      • 選擇以上兩種方式中的哪種,取決於 Java 堆內存是否規整。而 Java 堆內存是否規整,取決於 GC 收集器的算法是"標記-清除",仍是"標記-整理"("標記-壓縮"),值得注意的是,複製算法內存也是規整的
    • 內存分配併發問題

      • 在建立對象的時候有一個很重要的問題,就是線程安全,由於在實際開發過程當中,建立對象是很頻繁的事情,做爲虛擬機來講,必需要保證線程是安全的,一般來說,虛擬機採用兩種方式來保證線程安全:
    • CAS+失敗重試:

      • CAS 是樂觀鎖的一種實現方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操做,若是由於衝突失敗就重試,直到成功爲止。虛擬機採用 CAS 配上失敗重試的方式保證更新操做的原子性。
    • TLAB:

      • 爲每個線程預先在Eden區分配一起內存,JVM在給線程中的對象分配內存時,首先在TLAB分配,當對象大於TLAB中的剩餘內存或TLAB的內存已用盡時,再採用上述的CAS進行內存分配
  • ③初始化零值:

    • 內存分配完成後,虛擬機須要將分配到的內存空間都初始化爲零值(不包括對象頭),這一步操做保證了對象的實例字段在 Java 代碼中能夠不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。
  • ④設置對象頭:

    • 初始化零值完成以後,虛擬機要對對象進行必要的設置,例如這個對象是那個類的實例、如何才能找到類的元數據信息、對象的哈希嗎、對象的 GC 分代年齡等信息。
    • 這些信息存放在對象頭中。 另外,根據虛擬機當前運行狀態的不一樣,如是否啓用偏向鎖等,對象頭會有不一樣的設置方式。
  • ⑤執行 init 方法:

    • 在上面工做都完成以後,從虛擬機的視角來看,一個新的對象已經產生了,但從 Java 程序的視角來看,對象建立纔剛開始,<init> 方法尚未執行,全部的字段都還爲零。因此通常來講,執行 new 指令以後會接着執行 <init> 方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象纔算徹底產生出來。

1.2 對象的內存佈局

  • 在 Hotspot 虛擬機中,對象在內存中的佈局能夠分爲3快區域:對象頭實例數據對齊填充
  • Hotspot虛擬機的對象頭包括兩部分信息第一部分用於存儲對象自身的自身運行時數據(哈希嗎、GC分代年齡、鎖狀態標誌等等),另外一部分是類型指針,即對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是那個類的實例。
  • 實例數據部分是對象真正存儲的有效信息,也是在程序中所定義的各類類型的字段內容。
  • 對齊填充部分不是必然存在的,也沒有什麼特別的含義,僅僅起佔位做用。
  • 由於Hotspot虛擬機的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說就是對象的大小必須是8字節的整數倍。而對象頭部分正好是8字節的倍數(1倍或2倍),所以,當對象實例數據部分沒有對齊時,就須要經過對齊填充來補全。

02.Java內存區域

2.0 運行時數據區域

  • Java 虛擬機在執行 Java 程序的過程當中會把它管理的內存劃分紅若干個不一樣的數據區域。
  • 這些組成部分一些事線程私有的,其餘的則是線程共享的。

    • 線程私有的:

      • 程序計數器
      • 虛擬機棧
      • 本地方法棧
    • 線程共享的:

      • Java堆
      • 方法區
      • 運行時常量池
      • 直接內存
  • image

2.1 程序計數器

  • 程序計數器:是一個數據結構,用於保存當前正常執行的程序的內存地址。Java虛擬機的多線程就是經過線程輪流切換並分配處理器時間來實現的,爲了線程切換後能恢復到正確的位置,每條線程都須要一個獨立的程序計數器,互不影響,該區域爲「線程私有」。
  • 程序計數器是一塊較小的內存空間,能夠看做是當前線程所執行的字節碼的行號指示器。字節碼解釋器工做時經過改變這個計數器的值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等功能都須要依賴這個計數器來完。
  • 程序計數器主要有兩個做用:

    • 1.字節碼解釋器經過改變程序計數器來依次讀取指令,從而實現代碼的流程控制,如:順序執行、選擇、循環、異常處理。
    • 2.在多線程的狀況下,程序計數器用於記錄當前線程執行的位置,從而當線程被切換回來的時候可以知道該線程上次運行到哪兒。
  • 注意:程序計數器是惟一一個不會出現OutOfMemoryError的內存區域,它的生命週期隨着線程的建立而建立,隨着線程的結束而死亡。

2.2 虛擬機棧

  • Java虛擬機棧:線程私有的,與線程生命週期相同,用於存儲局部變量表,操做棧,方法返回值。局部變量表放着基本數據類型,還有對象的引用。
  • Java 內存能夠粗糙的區分爲堆內存(Heap)和棧內存(Stack),其中棧就是如今說的虛擬機棧,或者說是虛擬機棧中局部變量表部分。(實際上,Java虛擬機棧是由一個個棧幀組成,而每一個棧幀中都擁有:局部變量表、操做數棧、動態連接、方法出口信息。)
  • 局部變量表主要存放了編譯器可知的各類數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型,它不一樣於對象自己,多是一個指向對象起始地址的引用指針,也多是指向一個表明對象的句柄或其餘與此對象相關的位置)。
  • Java 虛擬機棧會出現兩種異常:StackOverFlowError 和 OutOfMemoryError。

    • StackOverFlowError: 若Java虛擬機棧的內存大小不容許動態擴展,那麼當線程請求棧的深度超過當前Java虛擬機棧的最大深度的時候,就拋出StackOverFlowError異常。
    • OutOfMemoryError: 若Java虛擬機棧的內存大小容許動態擴展,且當線程請求棧時內存用完了,沒法再動態擴展了,此時拋出OutOfMemoryError異常。
  • Java 虛擬機棧也是線程私有的,每一個線程都有各自的Java虛擬機棧,並且隨着線程的建立而建立,隨着線程的死亡而死亡。

2.3 本地方法棧

  • 本地方法棧:跟虛擬機棧很像, 虛擬機棧爲虛擬機執行 Java 方法 (也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的 Native 方法服務。 在 HotSpot 虛擬機中和 Java 虛擬機棧合二爲一。
  • 本地方法被執行的時候,在本地方法棧也會建立一個棧幀,用於存放該本地方法的局部變量表、操做數棧、動態連接、出口信息。
  • 方法執行完畢後相應的棧幀也會出棧並釋放內存空間,也會出現 StackOverFlowError 和 OutOfMemoryError 兩種異常。

2.4 Java堆

  • Java堆:全部線程共享的一塊內存區域,此內存區域的惟一目的就是存放對象實例,對象實例幾乎都在這分配內存。在虛擬機啓動時建立。
  • Java 堆是垃圾收集器管理的主要區域,所以也被稱做GC堆(Garbage Collected Heap).從垃圾回收的角度,因爲如今收集器基本都採用分代垃圾收集算法,因此Java堆還能夠細分爲:新生代和老年代:在細緻一點有:Eden空間、From Survivor、To Survivor空間等。進一步劃分的目的是更好地回收內存,或者更快地分配內存。

    • image
  • 在 JDK 1.8中移除整個永久代,取而代之的是一個叫元空間(Metaspace)的區域(永久代使用的是JVM的堆內存空間,而元空間使用的是物理內存,直接受到本機的物理內存限制)。

2.5 方法區

  • 方法區:各個線程共享的區域,儲存虛擬機加載的類信息,常量,靜態變量,編譯後的代碼。

    • 雖然Java虛擬機規範把方法區描述爲堆的一個邏輯部分,可是它卻有一個別名叫作 Non-Heap(非堆),目的應該是與 Java 堆區分開來。
  • 相對而言,垃圾收集行爲在這個區域是比較少出現的,但並不是數據進入方法區後就「永久存在」了。如何理解這句話?

2.6 運行時常量池

  • 運行時常量池:表明運行時每一個class文件中的常量表。包括幾種常量:編譯時的數字常量、方法或者域的引用。

    • 。Class 文件中包括類的版本、字段、方法、接口等描述信息
  • 既然運行時常量池時方法區的一部分,天然受到方法區內存的限制,當常量池沒法再申請到內存時會拋出 OutOfMemoryError 異常。JDK1.7及以後版本的 JVM已經將運行時常量池從方法區中移了出來,在Java堆(Heap)中開闢了一塊區域存放運行時常量池。

2.7 直接內存

  • 直接內存並非虛擬機運行時數據區的一部分,也不是虛擬機規範中定義的內存區域,可是這部份內存也被頻繁地使用。並且也可能致使OutOfMemoryError異常出現。
  • JDK1.4中新加入的 NIO(New Input/Output) 類,引入了一種基於通道(Channel)緩存區(Buffer) 的 I/O 方式,它能夠直接使用Native函數庫直接分配堆外內存,而後經過一個存儲在 Java 堆中的 DirectByteBuffer 對象做爲這塊內存的引用進行操做。這樣就能在一些場景中顯著提升性能,由於避免了在 Java 堆和 Native 堆之間來回複製數據
  • 本機直接內存的分配不會收到 Java 堆的限制,可是,既然是內存就會受到本機總內存大小以及處理器尋址空間的限制。

03.Java對象的訪問定位方式

  • 創建對象就是爲了使用對象,咱們的Java程序經過棧上的 reference 數據來操做堆上的具體對象。對象的訪問方式有虛擬機實現而定
  • 目前主流的訪問方式有

    • ①使用句柄
    • ②直接指針
  • 這兩種對象訪問方式各有優點。

    • 使用句柄來訪問的最大好處是 reference 中存儲的是穩定的句柄地址,在對象被移動時只會改變句柄中的實例數據指針,而 reference 自己不須要修改。
    • 使用直接指針訪問方式最大的好處就是速度快,它節省了一次指針定位的時間開銷。

3.1 句柄

  • 若是使用句柄的話,那麼Java堆中將會劃分出一塊內存來做爲句柄池,reference 中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息;

image

3.2 直接指針

  • 若是使用直接指針訪問,那麼 Java 堆對像的佈局中就必須考慮如何放置訪問類型數據的相關信息,而reference 中存儲的直接就是對象的地址。

image

04.Java對象銷燬分析

4.1 JVM內存分配與回收

  • Java 的自動內存管理主要是針對對象內存的回收和對象內存的分配。同時,Java 自動內存管理最核心的功能是 內存中對象的分配與回收。
  • JDK1.8以前的堆內存示意圖:

    • image
    • 從上圖能夠看出堆內存的分爲新生代、老年代和永久代。新生代又被進一步分爲:Eden 區+Survior1 區+Survior2 區。值得注意的是,在JDK1.8中移除整個永久代,取而代之的是一個叫元空間(Metaspace)的區域(永久代使用的是JVM的堆內存空間,而元空間使用的是物理內存,直接受到本機的物理內存限制)。
  • 分代回收算法

    • 目前主流的垃圾收集器都會採用分代回收算法,所以須要將堆內存分爲新生代和老年代,這樣咱們就能夠根據各個年代的特色選擇合適的垃圾收集算法。
    • 大多數狀況下,對象在新生代中 eden 區分配。當 eden 區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC。
  • Minor Gc和Full GC 有什麼不一樣呢?

    • 新生代GC(Minor GC):指發生新生代的的垃圾收集動做,Minor GC很是頻繁,回收速度通常也比較快。
    • 老年代GC(Major GC/Full GC):指發生在老年代的GC,出現了Major GC常常會伴隨至少一次的Minor GC(並不是絕對),Major GC的速度通常會比Minor GC的慢10倍以上。

4.2 判斷對象是否死亡

  • 堆中幾乎放着全部的對象實例,對堆垃圾回收前的第一步就是要判斷那些對象已經死亡(即不能再被任何途徑使用的對象)。

    • image
4.2.1 引用計數法
  • 給對象中添加一個引用計數器,每當有一個地方引用它,計數器就加1;當引用失效,計數器就減1;任什麼時候候計數器爲0的對象就是不可能再被使用的。

    • 這個方法實現簡單,效率高,可是目前主流的虛擬機中並無選擇這個算法來管理內存,其最主要的緣由是它很難解決對象之間相互循環引用的問題。
    • 所謂對象之間的相互引用問題,以下面代碼所示:除了對象objA和objB相互引用着對方以外,這兩個對象之間再無任何引用。可是他們由於互相引用對方,致使它們的引用計數器都不爲0,因而引用計數算法沒法通知 GC 回收器回收他們。
    public class Test {
        Object instance = null;
        public static void main(String[] args) {
            Test objA = new Test();
            Test objB = new Test();
            objA.instance = objB;
            objB.instance = objA;
            objA = null;
            objB = null;
        }
    }
4.2.2 可達性分析算法
  • 這個算法的基本思想就是經過一系列的稱爲 「GC Roots」 的對象做爲起點,從這些節點開始向下搜索,節點所走過的路徑稱爲引用鏈,當一個對象到 GC Roots 沒有任何引用鏈相連的話,則證實此對象是不可用的。

    • image
4.2.3 再談引用
  • 不管是經過引用計數法判斷對象引用數量,仍是經過可達性分析法判斷對象的引用鏈是否可達,斷定對象的存活都與「引用」有關。
  • JDK1.2之後,Java對引用的概念進行了擴充,將引用分爲強引用、軟引用、弱引用、虛引用四種(引用強度逐漸減弱)
  • 關於四種引用以及源代碼分析,能夠看個人這篇文章:https://blog.csdn.net/m0_3770...

4.3 不可達的對象並不是「非死不可」

  • 即便在可達性分析法中不可達的對象,也並不是是「非死不可」的,這時候它們暫時處於「緩刑階段」,要真正宣告一個對象死亡,至少要經歷兩次標記過程;可達性分析法中不可達的對象被第一次標記而且進行一次篩選,篩選的條件是此對象是否有必要執行 finalize 方法。當對象沒有覆蓋 finalize 方法,或 finalize 方法已經被虛擬機調用過期,虛擬機將這兩種狀況視爲沒有必要執行。
  • 被斷定爲須要執行的對象將會被放在一個隊列中進行第二次標記,除非這個對象與引用鏈上的任何一個對象創建關聯,不然就會被真的回收。

4.4 如何判斷一個常量是廢棄常量

4.5 如何判斷一個類是無用的類

  • 方法區主要回收的是無用的類,那麼如何判斷一個類是無用的類的呢?要斷定一個類是不是「無用的類」的條件則相對苛刻許多。類須要同時知足下面3個條件才能算是 「無用的類」

    • 該類全部的實例都已經被回收,也就是 Java 堆中不存在該類的任何實例。
    • 加載該類的 ClassLoader 已經被回收。
    • 該類對應的 java.lang.Class 對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。
  • 虛擬機能夠對知足上述3個條件的無用類進行回收,這裏說的僅僅是「能夠」,而並非和對象同樣不使用了就會必然被回收。

4.6 GC回收算法詳解

05.String類和常量池

5.1 String對象的兩種建立方式

  • 1 String 對象的兩種建立方式:

    String str1 = "abcd";
    String str2 = new String("abcd");
    System.out.println(str1==str2);//false
  • 這兩種不一樣的建立方法是有差異的【記住:只要使用new方法,便須要建立新的對象】

    • 第一種方式是在常量池中拿對象
    • 第二種方式是直接在堆內存空間建立一個新的對象。
  • image

5.2 String類型的常量池

  • String 類型的常量池比較特殊。它的主要使用方法有兩種:

    • 直接使用雙引號聲明出來的 String 對象會直接存儲在常量池中。
    • 若是不是用雙引號聲明的 String 對象,可使用 String 提供的 intern 方String.intern() 是一個 Native 方法,它的做用是:若是運行時常量池中已經包含一個等於此 String 對象內容的字符串,則返回常量池中該字符串的引用;若是沒有,則在常量池中建立與此 String 內容相同的字符串,並返回常量池中建立的字符串的引用。
String s1 = new String("yc");
String s2 = s1.intern();
String s3 = "yc";
System.out.println(s2);//yc
System.out.println(s1 == s2);//false,由於一個是堆內存中的String對象一個是常量池中的String對象,
System.out.println(s3 == s2);//true,由於兩個都是常量池中的String對

關於其餘內容介紹

01.關於博客彙總連接

02.關於個人博客

相關文章
相關標籤/搜索