天天看點

最簡單的JVM記憶體結構圖

JVM記憶體結構圖

最簡單的JVM記憶體結構圖

大家好,好幾天沒有更新了,今天的内容有點多,我們詳細介紹下JVM内部結構圖,還是和之前一樣,案例先行,友善大家了解記憶。

/**
 * @author :jiaolian
 * @date :Created in 2021-03-10 21:28
 * @description:helloworld測試jvm記憶體區域
 * @modified By:
 * 公衆号:叫練
 */
public class HelloWorldTest {

    public static void main(String[] args) {
        //建立HelloWorldTest對象;
        HelloWorldTest helloWorldTest = new HelloWorldTest();
        //建立2個線程調用sayHello
        for (int i=0; i<2; i++) {
            new Thread(()->helloWorldTest.sayHello("world")).start();
        }
    }

    /**
     * 對某人說hello
     * @param who
     */
    public void sayHello(String who) {
        System.out.println(Thread.currentThread().getName()+"hello!"+who);
    }
}       

如上代碼:在主線程中for循環建立2個線程調用sayHello,最後兩個線程分别對世界問好!這段代碼比較好了解,就不貼輸出結果了。我們編寫并運作了這段代碼,我們主要看看這段代碼在JVM中是怎麼運作的。

首先,我們編寫一個HelloWorldTest.Java檔案,經過javac編譯會轉化成位元組碼HelloWorldTest.class,為什麼要轉化成位元組碼呢?因為Java虛拟機能識别!最後由類加載子系統ClassLoader将位元組碼裝載到記憶體。每塊記憶體各有自己的作用,最後由執行引擎來執行位元組碼。下面我們重點介紹下各塊記憶體發揮的作用!

最簡單的JVM記憶體結構圖

方法區

方法區主要裝一些靜态資訊,比如:類中繼資料,常量池,方法資訊,類變量等。如上代碼HelloWorldTest.class是類中繼資料,sayHello,main都是方法資訊等都是放在方法區存儲的。方法區中還需要注意兩點:

  1. 如果方法區太大,超過設定,會報OutOfMemoryError:PermGen space錯誤。gclib工具可以動态生成類測試該錯誤。
  2. 在JDK1.7以前,方法區叫永久代,而1.8之後叫元空間。原因是JDK1.8為了釋放管理壓力,把運作時常量池交給堆去管理。

堆中主要存放執行個體對象。你可以這麼了解,隻要看到用關鍵字new的對象,資料都放在堆中。如上代碼HelloWorldTest helloWorldTest = new HelloWorldTest();helloWorldTest是HelloWorldTest對象的引用,指向new出來的HelloWorldTest對象執行個體,helloWorldTest引用是放在棧中的,也叫局部變量(方法内申明的對象類型或普通類型),我們簡單畫圖來表示下堆,棧,方法區關系。當JVM執行了HelloWorldTest helloWorldTest = new HelloWorldTest();這句話,JVM記憶體結構看起來是這樣的。如果指向對象引用消失,對象會被GC回收。

最簡單的JVM記憶體結構圖

在堆記憶體中,記憶體需要劃分成兩塊區域,新生代和老年代。如下圖所示。

  1. 新生代:在堆記憶體中,新生代又分為三塊,eden(伊甸園建立新生命,對應new對象),from,to,這三塊記憶體區域都屬于新生代,預設比例是8:1:1,每次new對象都會先存儲到eden中,如果eden區域記憶體滿了,會觸發monitor gc回收該區域,還未回收的對象會放入from或者to,from,to記憶體其中一塊是空的,友善對象在記憶體中整理标記,每GC一次,from,to兩塊空間對象每移動一次,還未回收的對象年紀也會增加1,到達一定年紀(預設是15歲),就會進入老年代了。
  2. 老年代:當老年代滿了,會觸發Full GC回收,如果系統太大,Full GC都回收不了,程式會出現類似java.lang.OutOfMemoryError: Java heap space,我們可以通過配置JVM參數:如-Xmx32m 設定最大堆記憶體為32M。

對堆分塊原因是友善JVM自動處理垃圾回收。堆記憶體是GC回收的主要區域。

最簡單的JVM記憶體結構圖

棧記憶體空間相對于堆空間比較小,也屬于線程私有,棧中主要是一堆棧幀,是先進後出的,了解起來棧幀對應就是一個方法,方法中包含局部變量,方法參數,還有方法出口,通路常量指針,和異常資訊表,其中異常資訊表和常量指針資訊我們在方法體中可能看不出來,但通過工具Jclasslib工具類在反編譯class檔案可以展現出來,異常資訊表可以處理當程式執行報錯,會跳轉到具體哪行代碼執行,JVM中就是通過異常表回報的。我們還是結合例子和圖來詳細分析下。當程式運作時,JVM中棧可能如下圖呈現狀态。

最簡單的JVM記憶體結構圖

一個線程可能對應多個棧幀,棧幀都是從上往下壓入,先進後出,如下圖所示,在方法A中調用方法B,在方法B中調用C,在方法C中調用方法D,主線程對應棧幀的壓棧情況,出棧順序是D->C->B->A,最終程式結束。另外還需注意:操作數棧的意思是存儲局部變量計算的中間結果,比如在方法A中定義int x = 1;在JVM中會将局部變量入操作數棧用來之後的計算。棧也是有空間大小的,如果棧太大,超過棧深度,會類似報錯,java.lang.OutOfMemoryError: Java stack space,最常見的例子就是遞歸了。你會寫demo測試遞歸例子嗎?

最簡單的JVM記憶體結構圖

程式計數器

程式計數器也是線程獨享的,多線程執行程式依賴于CPU配置設定時間片執行,畫個簡單的圖,看看多線程怎麼利用CPU時間片的。如下圖,線程0和線程1配置設定cpu時間片交替執行程式,假設此時線程0先擷取到了時間片,時間片用完後CPU會将時間片再配置設定給線程1,線程1執行完畢後,此時,時間片又回到線程0來執行,那麼問題來了,線程0上次執行到哪兒了呢?具體是代碼的多少行了呢,該行代碼有沒有執行完畢?此時程式計數器就發揮作用了,程式計數器儲存了線程的執行現場,友善下次恢複運作。這也是為什麼程式計數器是線程獨享的原因。

最簡單的JVM記憶體結構圖

本地方法棧

本地方法棧就不過多介紹了,和棧結構一樣,是一塊獨立的區域,隻是對應的是native方法。

直接記憶體

直接記憶體獨立于JVM記憶體之外的記憶體,可以直接和NIO接口互動,NIO接口會頻繁操作記憶體,如果放在JVM管理,無疑會增加JVM開銷,是以單獨将這塊提出來,而且直接記憶體操作資料相比較JVM更快,顯而易見提升了程式性能。

記憶體配置設定性能優化-逃逸分析

我們之前說過,隻要是看到關鍵字new,對象配置設定肯定在堆上,下面我們來看一個案例。

/**
 * @author :jiaolian
 * @date :Created in 2021-03-10 16:10
 * @description:逃逸分析測試
 * @modified By:
 * 公衆号:叫練
 */
public class EscapeTest {

    //private static Object object;
    public static void alloc() {
        //一個對象相當于16k大小,非逃逸對象
        //object = new Object();
        Object object = new Object();
    }

    public static void main(String[] args) throws InterruptedException {
        //億次記憶體
        long begin=System.currentTimeMillis();
        for (int i=0; i<10000000; i++) {
            alloc();
        }
        long end=System.currentTimeMillis();
        System.out.println("time:"+(end-begin));
    }
}      

如上代碼,我們在主函數裡面通過for循環1億次來new Object,一個object為16k,大緻估算下有GB資料了,此時我們手動配置JVM參數,-XX:+PrintGC -Xmx10M -XX:+DoEscapeAnalysis;設定列印GC資訊,預設最大的堆記憶體是10M。

  1. -XX:+PrintGC。表示控制台列印GC資訊。
  2. -Xmx10M。設定最大的堆記憶體為10M。
  3. -XX:+DoEscapeAnalysis。 開啟逃逸分析(預設開啟)。

執行程式,列印結果如下圖所示。一共進行了3次GC,你可能有疑問?10M堆記憶體需要容納GB資料沖擊,怎麼也需要N次GC,為什麼隻有3次GC?如果設定-XX:-DoEscapeAnalysis關閉逃逸分析,GC可能會出現上千次。運作時間也從3毫秒增至1000毫秒以上。說明了非逃逸對象沒有建立的堆上,而是建在棧上了。這樣做的好處:從程式GC執行次數和執行時間上來看,程式運作效率提高了。

最簡單的JVM記憶體結構圖
  • 原因分析:

觀察我們上述案例代碼中alloc()方法,方法中Object object = new Object();object是一個局部變量,每次建立後到下一次循環再建立,上一次建立的對象就會出棧,object引用指向的對象就會失效,失效的對象就會被GC回收了。開啟逃逸分析後,new Object()建立的對象就不在堆上配置設定空間了,而放到了棧上。這就是JVM通過逃逸分析對記憶體的優化。思考下,如果将private static Object object;注釋放開,object還會是非逃逸對象嗎?

注意:逃逸對象不能在棧上配置設定空間!

相信到這裡你已經對逃逸分析應該有一個比較清晰的認識了。

總結

好了,寫的有點累了,寫的不全同時還有許多需要修正的地方,希望親們加以指正和點評,喜歡的請點贊加關注哦。點關注,不迷路,我是【叫練】公衆号,微信号【jiaolian123abc】邊叫邊練。

最簡單的JVM記憶體結構圖