深入理解Java虛擬機(jī)之Java自動(dòng)內(nèi)存管理機(jī)制

深入理解Java虛擬機(jī)之Java自動(dòng)內(nèi)存管理機(jī)制

概述

  • 對(duì)于Java開(kāi)發(fā)人員來(lái)說(shuō),在虛擬機(jī)的自動(dòng)內(nèi)存管理機(jī)制的幫助下,不再需要為每一個(gè)new操作去寫(xiě)匹配的delete/free代碼,不容易(但還是有可能)出現(xiàn)內(nèi)存泄漏和內(nèi)存溢出的問(wèn)題,由虛擬機(jī)管理內(nèi)存。也正是因?yàn)镴ava開(kāi)發(fā)人員將內(nèi)存控制的權(quán)利交給了JVM,一旦出現(xiàn)了內(nèi)存泄漏和溢出方面的問(wèn)題,如果不了解JVM是如何使用內(nèi)存的,那么排查錯(cuò)誤將成為一項(xiàng)異常艱難的工作。

運(yùn)行時(shí)數(shù)據(jù)區(qū)

  • JVM在執(zhí)行Java程序的過(guò)程中會(huì)把它所管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)。這些區(qū)域都有自己的用途,以及創(chuàng)建和銷(xiāo)毀的時(shí)間。有些區(qū)域隨著虛擬機(jī)進(jìn)程的啟動(dòng)而存在,有些區(qū)域依賴(lài)于用戶(hù)線(xiàn)程的啟動(dòng)和結(jié)束而建立和銷(xiāo)毀。

JVM所管理的內(nèi)存將會(huì)包括一下幾個(gè)運(yùn)行時(shí)的數(shù)據(jù)區(qū)域(即:Java的內(nèi)存分為一下幾個(gè)區(qū)域)。如圖:

java_mem.jpeg
  • 程序計(jì)數(shù)器(Program Counter Register)
    • 程序計(jì)數(shù)器是一塊比較小的內(nèi)存空間,可以看作是當(dāng)前線(xiàn)程所執(zhí)行的字節(jié)碼的行號(hào)指示器 。字節(jié)碼解釋器工作時(shí)就是通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)選擇下一條需要執(zhí)行的字節(jié)碼指令。分支、循環(huán)、跳轉(zhuǎn)、異常處理、線(xiàn)程恢復(fù)等基礎(chǔ)功能都需要依賴(lài)這個(gè)計(jì)數(shù)器來(lái)完成。
      • 對(duì)于線(xiàn)程來(lái)說(shuō)
        • 每一個(gè)線(xiàn)程都有一個(gè)獨(dú)立的程序計(jì)數(shù)器,各個(gè)線(xiàn)程之間的計(jì)數(shù)器互不影響,獨(dú)立存儲(chǔ)。故線(xiàn)程私有(JVM的多線(xiàn)程是通過(guò)線(xiàn)程輪流切換并分配處理器執(zhí)行時(shí)間的方式來(lái)實(shí)現(xiàn)的,在任何一個(gè)時(shí)確定的時(shí)刻,一個(gè)處理器都只會(huì)執(zhí)行一條線(xiàn)程中的指令,故這也是為了線(xiàn)程切換后能夠恢復(fù)到正確的執(zhí)行位置)
      • 對(duì)于方法來(lái)說(shuō)
        • 對(duì)于Java方法:程序計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址
        • 對(duì)于Native方法:程序計(jì)數(shù)器為空(undefined).
  • Java虛擬機(jī)棧(Java Virtual Machine Stacks)
    • Java虛擬機(jī)棧,線(xiàn)程私有。描述的是Java方法執(zhí)行的內(nèi)存模型:每個(gè)方法在執(zhí)行的同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀用于存儲(chǔ)局部變量表,操作數(shù)棧,動(dòng)態(tài)鏈接,方法出口等信息。每一個(gè)方法從調(diào)用直至執(zhí)行完成的過(guò)程,就對(duì)應(yīng)著一個(gè)棧幀在JVM中入棧和出棧的過(guò)程。
    • 局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類(lèi)型(boolean、byte、char、short、int、float、long、double)、對(duì)象引用類(lèi)型(reference類(lèi)型)(64位長(zhǎng)度的long和double會(huì)占用兩個(gè)局部變量空間(slot),剩余的數(shù)據(jù)類(lèi)型只占用一個(gè))。局部變量表所需的內(nèi)存空間在編譯期間完成分配,當(dāng)進(jìn)入一個(gè)方法時(shí),這個(gè)方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運(yùn)行期間不會(huì)改變局部變量表的大小。
    • 可能出現(xiàn)的異常狀況:
      • StackOverflowError異常,即線(xiàn)程請(qǐng)求的棧深度大于JVM所允許的深度
      • OutOfMemoryError 異常,即如果虛擬機(jī)??梢詣?dòng)態(tài)拓展,而在拓展的過(guò)程中無(wú)法申請(qǐng)到足夠的內(nèi)存。
  • 本地方法棧(Native Method Stack)
    • 本地方法棧是為JVM所使用到的Native方法服務(wù)。在虛擬機(jī)的規(guī)范中,對(duì)本地方法棧中方法所使用的語(yǔ)言,數(shù)據(jù)結(jié)構(gòu)沒(méi)有強(qiáng)制的規(guī)定。因此具體的虛擬機(jī)可以自由實(shí)現(xiàn)。故有的虛擬機(jī)(例如:HotSpot)直接將本地方法棧與虛擬機(jī)棧合二為一。
    • 可能出現(xiàn)的異常:(與虛擬機(jī)棧一樣)
      • StackOverflowError異常
      • OutOfMemoryError 異常
  • Java 堆(Java Heap)
    • Java堆是虛擬機(jī)中所管理的內(nèi)存中最大的一塊。Java堆是被所有的線(xiàn)程共享的一塊內(nèi)存區(qū)域。在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放變量實(shí)例。幾乎所有的對(duì)象都在這里創(chuàng)建(JIT編譯器的發(fā)展與逃逸分析技術(shù)逐漸成熟,棧上分配、標(biāo)量替換優(yōu)化技術(shù)會(huì)導(dǎo)致一系列的變化的發(fā)生,所有的對(duì)象都分配在堆上就不是那么絕對(duì)了)。
    • Java 堆是垃圾收集器管理的主要區(qū)域,因此會(huì)被稱(chēng)為"GC堆"。Java堆可以被細(xì)分為:(各個(gè)的作用在垃圾回收章節(jié)說(shuō)明)
      • 新生代(1/3)
        • Eden空間)(即傳說(shuō)的伊甸園區(qū))(8/10)
        • Survivor空間(幸存區(qū))(2/10)
          • From Survivor空間(1/10)
          • To Survivor 空間(1/10)
      • 老年代(2/3)
    • 可能出現(xiàn)的異常
      • OutOfMemoryError 異常,即在堆中沒(méi)有內(nèi)存完成實(shí)例分配,并且堆也沒(méi)有辦法再拓展時(shí)。
    • 內(nèi)存調(diào)整參數(shù)
      • -Xmx 設(shè)定程序運(yùn)行期間最大可占用的內(nèi)存大小
      • -Xms 設(shè)定程序啟動(dòng)時(shí)占用內(nèi)存大小
  • 方法區(qū)(Method Area)
    • 方法區(qū)(別名:Non-heap:非堆) 也被稱(chēng)為”永久代“。與Java堆一樣,也是各個(gè)線(xiàn)程共享的內(nèi)存區(qū)域。用于存儲(chǔ)已經(jīng)被JVM加載的類(lèi)信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。在HotSpot虛擬機(jī)中,將GC的分代收集拓展到方法區(qū),或者說(shuō)是使用永久代來(lái)實(shí)現(xiàn)方法區(qū)。那么,HotSpot的垃圾收集器就可以像管理Java堆一樣來(lái)管理這一部分的內(nèi)存。

    • 注意:

      • 在Java8中已經(jīng)使用元空間來(lái)代替永久代,也就是在Java8中已經(jīng)沒(méi)有永久代了。類(lèi)似-XX:MaxPermSize這些設(shè)置永久代內(nèi)存大小的參數(shù)均已失效了。

      • JDK 1.7 的HotSpot中,將原本放在永久代的字符串常量池移出了。

      • GC回收的目標(biāo):1.常量池 2.類(lèi)型的卸載

      • 測(cè)試:

        • 測(cè)試代碼一
            import java.util.ArrayList; 
            import java.util.List;
        
            public class StringOomMock {
            static String  base = "string";
            public static void main(String[] args) {
                List<String> list = new ArrayList<String>();
                for (int i=0;i< 50;i++){
                    String str = base + base;
                    base = str;
                    list.add(str.intern());
                }
            }
            }
        
        • JDK1.6下運(yùn)行


          1.6perm.png
        • JDK1.7下運(yùn)行


          1.7perm.png
        • JDK1.8下運(yùn)行


          1.8perm.png
      • 說(shuō)明
      • JDK 1.6下,會(huì)出現(xiàn)“PermGen Space”的內(nèi)存溢出,而在 JDK 1.7和 JDK 1.8 中,會(huì)出現(xiàn)堆內(nèi)存溢出,并且 JDK 1.8中 PermSize 和 MaxPermGen 已經(jīng)無(wú)效。因此,可以大致驗(yàn)證 JDK 1.7 和 1.8 將字符串常量由永久代轉(zhuǎn)移到堆中,并且 JDK 1.8 中已經(jīng)不存在永久代的結(jié)論
      • 元空間的本質(zhì)和永久代類(lèi)似,都是對(duì)JVM規(guī)范中方法區(qū)的實(shí)現(xiàn)。不過(guò)元空間與永久代之間最大的區(qū)別在于:元空間并不在虛擬機(jī)中,而是使用本地內(nèi)存。因此,默認(rèn)情況下,元空間的大小僅受本地內(nèi)存限制,但可以通過(guò)以下參數(shù)來(lái)指定元空間的大小
        • -XX:MetaspaceSize,初始空間大小,達(dá)到該值就會(huì)觸發(fā)垃圾收集進(jìn)行類(lèi)型卸載,同時(shí)GC會(huì)對(duì)該值進(jìn)行調(diào)整:如果釋放了大量的空間,就適當(dāng)降低該值;如果釋放了很少的空間,那么在不超過(guò)MaxMetaspaceSize時(shí),適當(dāng)提高該值
        • -XX:MaxMetaspaceSize,最大空間,默認(rèn)是沒(méi)有限制的。
        • -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空間容量的百分比,減少為分配空間所導(dǎo)致的垃圾收集
        • -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空間容量的百分比,減少為釋放空間所導(dǎo)致的垃圾收集

      • 測(cè)試代碼二
      import java.io.File;
      import java.net.URL;
      import java.net.URLClassLoader;
      import java.util.ArrayList;
      import java.util.List;
      
      public class PermGenOomMock{
          public static void main(String[] args) {
              URL url = null;
              List<ClassLoader> classLoaderList = new ArrayList<ClassLoader>();
              try {
                  url = new File("/tmp").toURI().toURL();
                  URL[] urls = {url};
                  while (true){
                      ClassLoader loader = new URLClassLoader(urls);
                      classLoaderList.add(loader);
                      loader.loadClass("com.paddx.test.memory.Test");
                  }
              } catch (Exception e) {
                  e.printStackTrace();
              }
          }
      }
      
      • JDK1.8下運(yùn)行


        1.8meta.png
      • 說(shuō)明:
      • 不再出現(xiàn)永久代溢出,而是出現(xiàn)了元空間的溢出。
      • 可能出現(xiàn)的異常
        • OutOfMemoryError異常:即方法區(qū)無(wú)法滿(mǎn)足內(nèi)存分配的需求時(shí)
  • 運(yùn)行時(shí)常量池(Runntime Constant Pool)
    • 運(yùn)行是常量池是方法區(qū)的一部分,Class文件中除了有類(lèi)的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息就是常量池(Constant Pool Table),用于存放編譯器期生成的各種字面量和符號(hào)引用,這部分內(nèi)容將在類(lèi)加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放。
    • 除了保存在Class文件中描述的符號(hào)引用,還會(huì)把翻譯出來(lái)的直接引用也存儲(chǔ)在運(yùn)行時(shí)常量池中。
    • 運(yùn)行時(shí)常量池相對(duì)于Class文件常量池的另外一個(gè)重要特征就是具備動(dòng)態(tài)性。Java語(yǔ)言并不要求常量一定只有在編譯期才能產(chǎn)生,也就是并非預(yù)置入Class文件中常量池的內(nèi)存用才能進(jìn)入方法區(qū)運(yùn)行時(shí)常量池,運(yùn)行期間也可能將新的常量放到池中
      • String類(lèi)的intern()方法
    • 可能出現(xiàn)的異常:
      • OutOfMemoryError異常:即常量池?zé)o法再申請(qǐng)到內(nèi)存時(shí)
  • 直接內(nèi)存(Direct Memory)
    • 直接內(nèi)存并不是JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是JVM規(guī)范中定義的內(nèi)存區(qū)域。只是因?yàn)檫@一部分內(nèi)存經(jīng)常使用,也可能導(dǎo)致OutOfMemoryError異常。
    • 來(lái)源:
      • 在JDK1.4 中加入了NIO(new Input/Output)類(lèi),引入了一種基于通(Channel)與緩沖區(qū)(Buffer)的I/O方式,它可以使用Native函數(shù)庫(kù)直接分配堆外內(nèi)存,然后通過(guò)一個(gè)存儲(chǔ)在Java堆上的DirectByteBuffer對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。這樣避免了在Java堆和Native堆中來(lái)回復(fù)制數(shù)據(jù)(可以學(xué)習(xí)一下Linux系統(tǒng)中的epoll(如何避免內(nèi)核在用戶(hù)態(tài)和內(nèi)核態(tài)來(lái)回切換))
    • 注意:
      • 不要隨意設(shè)置-Xmx,因?yàn)楸緳C(jī)直接內(nèi)存雖然不受Java堆大小的限制,但是會(huì)受到本機(jī)總內(nèi)存的限制(包括 RAM 、SWAP區(qū)、分頁(yè)文件)。如果忽略了直接內(nèi)存,就可能導(dǎo)致各個(gè)內(nèi)存區(qū)域的總和大于物理內(nèi)存的限制(操作系統(tǒng)的和物理的)

對(duì)象揭秘

創(chuàng)建流程

new_object.jpg
  • 說(shuō)明:
    • 如何判斷類(lèi)是否加載
      • 檢查new指令的參數(shù)是否能在常量池中定位到一個(gè)類(lèi)的符號(hào)引用并且檢查這個(gè)符號(hào)引用代表的類(lèi)是否已經(jīng)被加載、解析、初始化過(guò)。

對(duì)象的內(nèi)存布局

  • 對(duì)象在內(nèi)存中的布局分為三部分:
    • 對(duì)象頭(Header)(實(shí)現(xiàn)synchronized的基礎(chǔ))
      • 對(duì)象頭包含兩部分信息
        • 一:存儲(chǔ)對(duì)象自身的運(yùn)行數(shù)據(jù)(MarkWord)。如哈希碼、GC分代年齡、鎖狀態(tài)標(biāo)識(shí)、線(xiàn)程持有的鎖、偏向線(xiàn)程ID、偏向時(shí)間戳等(如下圖,每一行代表一種狀態(tài)即根據(jù)對(duì)象的狀態(tài)來(lái)復(fù)用自己的存儲(chǔ)空間)

          MarkWord.jpeg

        • 二:類(lèi)型指針。即對(duì)象指向它的類(lèi)元數(shù)據(jù)指針,JVM通過(guò)該指針來(lái)確定這個(gè)對(duì)像是哪個(gè)類(lèi)的實(shí)例。

    • 實(shí)例數(shù)據(jù)(Instance Data)
      • 對(duì)象整正存儲(chǔ)的有效信息,也是在程序代碼中所定義的各種類(lèi)型字段的內(nèi)容。無(wú)論是從父類(lèi)繼承下來(lái)的還是在子類(lèi)中定義的,都需要記錄下來(lái)。
    • 對(duì)齊填充(padding)
      • 因?yàn)镴VM要求對(duì)象的起始地址必須是8字節(jié)整數(shù)倍,這個(gè)部分就是為了字節(jié)對(duì)齊

對(duì)象的訪(fǎng)問(wèn)定位

  • Java程序需要通過(guò)棧上的reference數(shù)據(jù)來(lái)操作堆上具體的對(duì)象。由于reference類(lèi)型在JVM規(guī)范中只規(guī)定了一個(gè)執(zhí)行對(duì)象的引用,并沒(méi)有定義這個(gè)引用該通過(guò)什么方式去定位、訪(fǎng)問(wèn)堆上的對(duì)象的具體位置,所以對(duì)象的訪(fǎng)問(wèn)取決于虛擬機(jī)的實(shí)現(xiàn)而定的。
  • 訪(fǎng)問(wèn)的方式
    • 使用句柄訪(fǎng)問(wèn)
      • 優(yōu)勢(shì):reference中存儲(chǔ)的是穩(wěn)定的句柄地址,在對(duì)象移動(dòng)時(shí)只會(huì)改變句柄中的數(shù)據(jù)指針,而reference本身不需要改變


        jubing_fangwen.png
  • 直接使用指針訪(fǎng)問(wèn)
    • 優(yōu)勢(shì):訪(fǎng)問(wèn)速度更快。少了一次指針定位的開(kāi)銷(xiāo)。


      zhizhen_fangwen.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容