06.Java虛擬機器問題

瀟湘劍雨發表於2019-01-19

目錄介紹

  • 6.0.0.1 執行時資料區域有哪些?Java虛擬機器棧是做什麼的?本地方法棧又是做什麼的?
  • 6.0.0.2 物件的記憶體佈局?物件的訪問定位方式有哪些?使用指標訪問和使用控制程式碼訪問各具有何優勢?
  • 6.0.0.3 說一下物件的建立過程?變數建立過程种放在虛擬機器哪裡?
  • 6.0.0.4 OutOfMemoryError異常在哪些資料區域中可能會出現?分別說一下這個資料區域出現OOM的場景和緣由?
  • 6.0.0.6 Java中堆和棧的區別?分別寫出堆記憶體溢位與棧記憶體溢位的程式?
  • 6.0.0.7 如果物件的引用被置為null,垃圾收集器是否會立即釋放物件佔用的記憶體?
  • 6.0.0.8 java中垃圾收集的方法有哪些?
  • 6.0.1.1 如和判斷一個物件是否存活?引用計數法和可達性演算法哪個更加好?如何理解一個物件不一定會被回收?
  • 6.0.1.2 Class.forName() 和ClassLoader.loadClass()區別?

好訊息

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

6.0.0.1 執行時資料區域有哪些?Java虛擬機器棧是做什麼的?本地方法棧又是做什麼的?

  • 執行時資料區域有哪些?

    • Java虛擬機器管理的記憶體包括幾個執行時資料記憶體:方法區、虛擬機器棧、本地方法棧、堆、程式計數器,其中方法區和堆是由執行緒共享的資料區,其他幾個是執行緒隔離的資料區
    • 1.1 程式計數器

      • 程式計數器是一塊較小的記憶體,他可以看做是當前執行緒所執行的行號指示器。位元組碼直譯器工作的時候就是通過改變這個計數器的值來選取下一條需要執行的位元組碼的指令,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成。如果執行緒正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機器位元組碼指令的地址;如果正在執行的是Native方法,這個計數器則為空。此記憶體區域是唯一一個在Java虛擬機器規範中沒有規定任何OutOfMemotyError情況的區域
    • 1.2 Java虛擬機器棧

      • 虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法在執行的同時都會建立一個棧幀用於儲存區域性變數表、運算元棧、動態連結、方法出口等資訊。每個方法從呼叫直至完成的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程。
      • 技術部落格大總結
      • 棧記憶體就是虛擬機器棧,或者說是虛擬機器棧中區域性變數表的部分
      • 區域性變數表存放了編輯期可知的各種基本資料型別(boolean、byte、char、short、int、float、long、double)、物件引用(refrence)型別和returnAddress型別(指向了一條位元組碼指令的地址)
      • 其中64位長度的long和double型別的資料會佔用兩個區域性變數空間,其餘的資料型別只佔用1個。
      • Java虛擬機器規範對這個區域規定了兩種異常狀況:如果執行緒請求的棧深度大於虛擬機器所允許的深度,將丟擲StackOverflowError異常。如果虛擬機器擴充套件時無法申請到足夠的記憶體,就會跑出OutOfMemoryError異常
    • 1.3 本地方法棧

      • 本地方法棧和虛擬機器棧發揮的作用是非常類似的,他們的區別是虛擬機器棧為虛擬機器執行Java方法(也就是位元組碼)服務,而本地方法棧則為虛擬機器使用到的Native方法服務
      • 本地方法棧區域也會丟擲StackOverflowError和OutOfMemoryErroy異常
    • 1.4 Java堆

      • 堆是Java虛擬機器所管理的記憶體中最大的一塊。Java堆是被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動的時候建立,此記憶體區域的唯一目的是存放物件例項,幾乎所有的物件例項都在這裡分配記憶體。所有的物件例項和陣列都在堆上分配
      • Java堆是垃圾收集器管理的主要區域。Java堆細分為新生代和老年代
      • 不管怎樣,劃分的目的都是為了更好的回收記憶體,或者更快地分配記憶體
      • Java堆可以處於物理上不連續的記憶體空間中,只要邏輯上是連續的即可。如果在堆中沒有完成例項分配,並且堆也無法在擴充套件時將會丟擲OutOfMemoryError異常
    • 1.5 方法區

      • 方法區它用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料
      • 除了Java堆一樣不需要連續的記憶體和可以選擇固定大小或者可擴充套件外,還可以選擇不實現垃圾收集。這個區域的記憶體回收目標主要是針對常量池的回收和對型別的解除安裝
      • 當方法區無法滿足記憶體分配需求時,將丟擲OutOfMemoryErroy異常
    • 1.6 執行時常量池

      • 它是方法區的一部分。Class檔案中除了有關的版本、欄位、方法、介面等描述資訊外、還有一項資訊是常量池,用於存放編輯期生成的各種字面量和符號引用,這部分內容將在類載入後進入方法區的執行時常量池中存放
      • Java語言並不要求常量一定只有編輯期才能產生,也就是可能將新的常量放入池中,這種特性被開發人員利用得比較多的便是String類的intern()方法
      • 當常量池無法再申請到記憶體時會丟擲OutOfMemoryError異常

6.0.0.2 物件的記憶體佈局?物件的訪問定位方式有哪些?使用指標訪問和使用控制程式碼訪問各具有何優勢?

  • 物件的記憶體佈局?

    • 在HotSpot虛擬機器中,物件在記憶體中儲存的佈局可以分為3塊區域:物件頭、例項資料和對齊填充
    • 物件頭包括兩部分:

      • a) 儲存物件自身的執行時資料,如雜湊碼、GC分帶年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳
      • b) 另一部分是指型別指標,即物件指向它的類後設資料的指標,虛擬機器通過這個指標來確定這個物件是那個類的例項
  • 物件的訪問定位方式有哪些?

    • 使用控制程式碼訪問

      • Java堆中將會劃分出一塊記憶體來作為控制程式碼池,reference中儲存的就是物件的控制程式碼地址,而控制程式碼中包含了物件例項資料與型別資料各自的具體地址
    • 使用直接指標訪問

      • Java堆物件的佈局就必須考慮如何訪問型別資料的相關資訊,而refreence中儲存的直接就是物件的地址
  • 使用指標訪問和使用控制程式碼訪問各具有何優勢?

    • 使用控制程式碼訪問優勢:reference中儲存的是穩點的控制程式碼地址,在物件被移動(垃圾收集時移動物件是非常普遍的行為)時只會改變控制程式碼中的例項資料指標,而reference本身不需要修改
    • 使用直接指標訪問優勢:速度更快,節省了一次指標定位的時間開銷,由於物件的訪問在Java中非常頻繁,因此這類開銷積少成多後也是一項非常可觀的執行成本
    • 技術部落格大總結

6.0.0.3 說一下物件的建立過程?變數建立過程种放在虛擬機器哪裡?

  • 說一下物件的建立過程?比如:Dog dog= new Dog();

    • 當虛擬機器執行到new指令時,它先在常量池中查詢“Dog”,看能否定位到Dog類的符號引用;如果能,說明這個類已經被載入到方法區了,則繼續執行。如果沒有,就讓Class Loader先執行類的載入。
    • 然後,虛擬機器開始為該物件分配記憶體,物件所需要的記憶體大小在類載入完成後就已經確定了。這時候只要在堆中按需求分配空間即可。具體分配記憶體時有兩種方式,第一種,記憶體絕對規整,那麼只要在被佔用記憶體和空閒記憶體間放置指標即可,每次分配空間時只要把指標向空閒記憶體空間移動相應距離即可,當某物件被GC回收後,則需要進行某些物件記憶體的遷移。第二種,空閒記憶體和非空閒記憶體夾雜在一起,那麼就需要用一個列表來記錄堆記憶體的使用情況,然後按需分配記憶體。
    • 對於多執行緒的情況,如何確保一個執行緒分配了物件記憶體但尚未修改記憶體管理指標時,其他執行緒又分配該塊記憶體而覆蓋的情況?有一種方法,就是讓每一個執行緒在堆中先預分配一小塊記憶體(TLAB本地執行緒分配緩衝),每個執行緒只在自己的記憶體中分配記憶體。但物件本身按其訪問屬性是可以執行緒共享訪問的。
    • 記憶體分配到後,虛擬機器將分配的記憶體空間都初始化為零值(不包括物件頭)。例項變數按變數型別初始化相應的預設值(數值型為0,boolan為false),所以例項變數不賦初值也能使用。接著設定物件頭資訊,比如物件的雜湊值,GC分代年齡等。技術部落格大總結
    • 從虛擬機器角度,此時一個新的物件已經建立完成了。但從我們程式執行的角度,新建物件才剛剛開始,物件的構造方法還沒有執行。只有執行完構造方法,按構造方法進行初始化後,物件才是徹底建立完成了。建構函式的執行還涉及到呼叫父類構造器,如果沒有顯式宣告呼叫父類構造器,則自動新增預設構造器。
    • new運算子可以返回堆中這個物件的引用
  • 變數建立過程种放在虛擬機器哪裡?

    • 變數是例項變數、區域性變數或靜態變數的不同將引用放在不同的地方:

      • 如果dog區域性變數,dog變數在棧幀的區域性變數表,這個物件的引用就放在棧幀。
      • 如果dog是例項變數,dog變數在堆中,物件的引用就放在堆。
      • 如果dog是靜態變數,dog變數在方法區,物件的引用就放在方法區。

6.0.0.4 OutOfMemoryError異常在哪些資料區域中可能會出現?分別說一下這個資料區域出現OOM的場景和緣由?

  • OutOfMemoryError異常在哪些資料區域中可能會出現?

    • Java堆溢位
    • 虛擬機器棧和本地方法棧溢位
    • 方法區和執行時常量池溢位
  • 分別說一下這個資料區域出現OOM的場景和緣由?

    • Java堆溢位

      • Java堆用於儲存物件例項,只要不斷的建立物件,並且保證GCRoots到物件之間有可達路徑來避免垃圾回收機制清除這些物件,那麼在數量到達最大堆的容量限制後就會產生記憶體溢位異常
      • 如果是記憶體洩漏,可進一步通過工具檢視洩漏物件到GC Roots的引用鏈。於是就能找到洩露物件是通過怎樣的路徑與GC Roots相關聯並導致垃圾收集器無法自動回收它們的。掌握了洩漏物件的型別資訊及GC Roots引用鏈的資訊,就可以比較準確地定位出洩漏程式碼的位置
      • 如果不存在洩露,換句話說,就是記憶體中的物件確實都還必須存活著,那就應當檢查虛擬機器的堆引數(-Xmx與-Xms),與機器實體記憶體對比看是否還可以調大,從程式碼上檢查是否存在某些物件生命週期過長、持有狀態時間過長的情況,嘗試減少程式執行期的記憶體消耗
    • 虛擬機器棧和本地方法棧溢位

      • 對於HotSpot來說,雖然-Xoss引數(設定本地方法棧大小)存在,但實際上是無效的,棧容量只由-Xss引數設定。關於虛擬機器棧和本地方法棧,在Java虛擬機器規範中描述了兩種異常:
      • 如果執行緒請求的棧深度大於虛擬機器所允許的最大深度,將丟擲StackOverflowError
      • 如果虛擬機器在擴充套件棧時無法申請到足夠的記憶體空間,則丟擲OutOfMemoryError異常
      • 在單執行緒下,無論由於棧幀太大還是虛擬機器棧容量太小,當記憶體無法分配的時候,虛擬機器丟擲的都是StackOverflowError異常
      • 如果是多執行緒導致的記憶體溢位,與棧空間是否足夠大並不存在任何聯絡,這個時候每個執行緒的棧分配的記憶體越大,反而越容易產生記憶體溢位異常。解決的時候是在不能減少執行緒數或更換64為的虛擬機器的情況下,就只能通過減少最大堆和減少棧容量來換取更多的執行緒
    • 方法區和執行時常量池溢位

      • String.intern()是一個Native方法,它的作用是:如果字串常量池中已經包含一個等於此String物件的字串,則返回代表池中這個字串的String物件;否則,將此String物件包含的字串新增到常量池中,並且返回此String物件的引用
      • 由於常量池分配在永久代中,可以通過-XX:PermSize和-XX:MaxPermSize限制方法區大小,從而間接限制其中常量池的容量。技術部落格大總結
      • Intern():JDK1.6 intern方法會把首次遇到的字串例項複製到永久代,返回的也是永久代中這個字串例項的引用,而由StringBuilder建立的字串例項在Java堆上,所以必然不是一個引用。JDK1.7 intern()方法的實現不會再複製例項,只是在常量池中記錄首次出現的例項引用,因此intern()返回的引用和由StringBuilder建立的那個字串例項是同一個

6.0.0.6 Java中堆和棧的區別?分別寫出堆記憶體溢位與棧記憶體溢位的程式?

  • Java中堆和棧的區別?

    • 棧記憶體:主要用來存放基本資料型別和區域性變數;當在程式碼塊定義一個變數時會在棧中為這個變數分配記憶體空間,當超過變數的作用域後這塊空間就會被自動釋放掉。
    • 堆記憶體:用來存放執行時建立的物件,比如通過new關鍵字建立出來的物件和陣列;需要由Java虛擬機器的自動垃圾回收器來管理。
  • 分別寫出堆記憶體溢位與棧記憶體溢位的程式?

    • 棧記憶體溢位
    public void A() {
        A();
    }
    • 堆記憶體溢位
    public void testd() {
        List<String> list = new ArrayList<>();
        int i = 0;
        while (true) {
            list.add(new String(i + ""));
            i++;
        }
    }

6.0.0.7 如果物件的引用被置為null,垃圾收集器是否會立即釋放物件佔用的記憶體?

  • 如果物件的引用被置為null,垃圾收集器是否會立即釋放物件佔用的記憶體?

    • 不會,在下一個垃圾回收週期中,這個物件將是可被回收的。
    • 也就是說當一個物件的引用變為null時,並不會被垃圾收集器立刻回收,而是在下一次垃圾回收時才會釋放其佔用的記憶體。

6.0.0.8 java中垃圾收集的方法有哪些?

  • java中垃圾收集的方法有哪些

    • 標記-清除:

      • 這是垃圾收集演算法中最基礎的,根據名字就可以知道,它的思想就是標記哪些要被回收的物件,然後統一回收。這種方法很簡單,但是會有兩個主要問題:1.效率不高,標記和清除的效率都很低;2.會產生大量不連續的記憶體碎片,導致以後程式在分配較大的物件時,由於沒有充足的連續記憶體而提前觸發一次GC動作。
    • 複製演算法:

      • 為了解決效率問題,複製演算法將可用記憶體按容量劃分為相等的兩部分,然後每次只使用其中的一塊,當一塊記憶體用完時,就將還存活的物件複製到第二塊記憶體上,然後一次性清楚完第一塊記憶體,再將第二塊上的物件複製到第一塊。但是這種方式,記憶體的代價太高,每次基本上都要浪費一般的記憶體。
      • 於是將該演算法進行了改進,記憶體區域不再是按照1:1去劃分,而是將記憶體劃分為8:1:1三部分,較大那份記憶體交Eden區,其餘是兩塊較小的記憶體區叫Survior區。每次都會優先使用Eden區,若Eden區滿,就將物件複製到第二塊記憶體區上,然後清除Eden區,如果此時存活的物件太多,以至於Survivor不夠時,會將這些物件通過分配擔保機制複製到老年代中。(java堆又分為新生代和老年代)
    • 標記-整理技術部落格大總結

      • 該演算法主要是為了解決標記-清除,產生大量記憶體碎片的問題;當物件存活率較高時,也解決了複製演算法的效率問題。它的不同之處就是在清除物件的時候現將可回收物件移動到一端,然後清除掉端邊界以外的物件,這樣就不會產生記憶體碎片了。
    • 分代收集

      • 現在的虛擬機器垃圾收集大多采用這種方式,它根據物件的生存週期,將堆分為新生代和老年代。在新生代中,由於物件生存期短,每次回收都會有大量物件死去,那麼這時就採用複製演算法。老年代裡的物件存活率較高,沒有額外的空間進行分配擔保,所以可以使用標記-整理 或者 標記-清除。

6.0.1.1 如和判斷一個物件是否存活?引用計數法和可達性演算法哪個更加好?如何理解一個物件不一定會被回收?

    1. 引用計數法
    • 所謂引用計數法就是給每一個物件設定一個引用計數器,每當有一個地方引用這個物件時,就將計數器加一,引用失效時,計數器就減一。當一個物件的引用計數器為零時,說明此物件沒有被引用,也就是“死物件”,將會被垃圾回收.
    • 引用計數法有一個缺陷就是無法解決迴圈引用問題,也就是說當物件A引用物件B,物件B又引用者物件A,那麼此時A,B物件的引用計數器都不為零,也就造成無法完成垃圾回收,所以主流的虛擬機器都沒有采用這種演算法。
  • 2.可達性演算法(引用鏈法)

    • 該演算法的思想是:從一個被稱為GC Roots的物件開始向下搜尋,如果一個物件到GC Roots沒有任何引用鏈相連時,則說明此物件不可用。
    • 在java中可以作為GC Roots的物件有以下幾種:

      • 虛擬機器棧中引用的物件
      • 方法區類靜態屬性引用的物件
      • 方法區常量池引用的物件
      • 本地方法棧JNI引用的物件
  • 如何理解一個物件不一定會被回收?技術部落格大總結

    • 雖然這些演算法可以判定一個物件是否能被回收,但是當滿足上述條件時,一個物件比不一定會被回收。當一個物件不可達GC Root時,這個物件並不會立馬被回收,而是出於一個死緩的階段,若要被真正的回收需要經歷兩次標記
    • 如果物件在可達性分析中沒有與GCRoot的引用鏈,那麼此時就會被第一次標記並且進行一次篩選,篩選的條件是是否有必要執行finalize()方法。當物件沒有覆蓋finalize()方法或者已被虛擬機器呼叫過,那麼就認為是沒必要的。
    • 如果該物件有必要執行finalize()方法,那麼這個物件將會放在一個稱為F-Queue的對佇列中,虛擬機器會觸發一個Finalize()執行緒去執行,此執行緒是低優先順序的,並且虛擬機器不會承諾一直等待它執行完,這是因為如果finalize()執行緩慢或者發生了死鎖,那麼就會造成F-Queue佇列一直等待,造成了記憶體回收系統的崩潰。GC對處於F-Queue中的物件進行第二次被標記,這時,該物件將被移除”即將回收”集合,等待回收。

6.0.1.2 Class.forName() 和ClassLoader.loadClass()區別?

  • Class.forName() 和ClassLoader.loadClass()區別?

    • 問到的是反射,但是在底層涉及到了虛擬機器的類載入知識。
    • Class.forName() 預設執行類載入過程中的連線與初始化動作,一旦執行初始化動作,靜態變數就會被初始化為程式設計師設定的值,如果有靜態程式碼塊,靜態程式碼塊也會被執行
    • ClassLoader.loadClass() 預設只執行類載入過程中的載入動作,後面的動作都不會執行

其他介紹

01.關於部落格彙總連結

02.關於我的部落格

相關文章