天天看點

JVM運作資料區深度解析

作者:Java技術識堂

運作資料區

位元組碼隻是一個二進制檔案存放在那裡。要想在jvm裡跑起來,先得有個運作的記憶體環境。

也就是我們所說的jvm運作時資料區。

1)運作時資料區的位置

運作時資料區是jvm中最為重要的部分,執行引擎頻繁操作的就是它。類的初始化,以及後面我們講的對象空間的配置設定、垃圾的回收都是在這塊區域發生的。

JVM運作資料區深度解析

2)區域劃分

根據《Java虛拟機規範》中的規定,在運作時資料區将記憶體細分為幾個部分

線程私有的:Java虛拟機棧(Java Virtual Machine Stack)、程式計數器(Program Counter Register)、本地方法棧(Native Method Stacks)

大家共享的:方法區(Method Area)、Java堆區(Java Heap)

JVM運作資料區深度解析

接下來我們分塊詳細來解讀,每一塊是做什麼的,如果溢出了會發生什麼事情

1.1 程式計數器

1.1.1 概述

程式計數器(Program Counter Register)

  • 每個線程一個。是一塊較小的記憶體空間,它表示目前線程執行的位元組碼指令的位址。
  • 位元組碼解釋器工作時,通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,是以整個程式無論是分支、循環、跳轉、異常處理、線程恢複等基礎功能都需要依賴這個計數器來完成。
  • 由于線程是多條并行執行的,互相之間執行到哪條指令是不一樣的,是以每條線程都需要有一個獨立的程式計數器,各條線程之間計數器互不影響,獨立存儲,我們稱這類記憶體區域為“線程私有”的記憶體。
  • 如果是native方法,這裡為空

1.1.2 溢出異常

沒有!

在虛拟機規範中,沒有對這塊區域設定記憶體溢出規範,也是唯一一個不會溢出的區域

1.1.3 案例

因為它不會溢出,是以我們沒有辦法給它造一個,但是從class類上可以找到痕迹。

回顧上面javap的反彙編,其中code所對應的編号就可以了解為計數器中所記錄的執行編号。

JVM運作資料區深度解析

1.2 虛拟機棧

JVM運作資料區深度解析

1.2.1 概述

  • 也是線程私有的!生命周期與線程相同。
  • 它描述的是Java方法執行的目前線程的記憶體模型,每個方法被執行的時候,Java虛拟機都會同步建立一個棧幀,用于存儲局部變量表、操作數棧、動态連接配接、方法出口等資訊。每一個方法被調用直至執行完畢的過程,就對應着一個棧幀在虛拟機棧中從入棧到出棧的過程。

1.2.2 溢出異常

1)棧深度超出設定

如果是建立的棧的深度大于虛拟機允許的深度,抛出

Exception in thread "main" java.lang.StackOverflowError

2)記憶體申請不足

如果棧允許記憶體擴充,但是記憶體申請不夠的時候,抛出 OutOfMemoryError

注意!這一點和具體的虛拟機有關,hotspot虛拟機并不支援棧空間擴充,是以單線程環境下,一個線程建立時,配置設定給它固定大小的一個棧,在這個固定棧空間上不會出現再去擴容申請記憶體的情況,也就不會遇到申請不到一說,隻會因為深度問題超出固定空間造成上面的StackOverflowError

如果換成多線程,毫無節制的建立線程,還是有可能造成OutOfMemoryError。但是這個和Xss棧空間大小無關。是因為線程個數太多,棧的個數太多,導緻系統配置設定給jvm程序的實體記憶體被吃光。

這時候虛拟機會附帶相關的提示:

Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread

ps: 每個線程預設配置設定1M空間(64位linux,hotspot環境)

疑問:是不是改小Xss的值就可以得到棧空間溢出呢?

答:根據上面的分析,hotspot下不可以,還是會抛出StackOverflowError,無非深度更小了。

1.2.3 案例一:進出棧順序

1)代碼

package com.itheima.jvm.demo; /** * 程式模拟進棧、出棧過程 * 先進後出 */public class StackInAndOut {    /**     * 定義方法一     */    public static void A() {        System.out.println("進入方法A");    }     /**     * 定義方法二;調用方法一     */    public static void B() {        A();        System.out.println("進入方法B");    }     public static void main(String[] args) {         B();        System.out.println("進入Main方法");    }}              

2)運作結果:

進入方法A進入方法B進入Main方法           

3)棧結構:

main方法---->B方法---->A方法

JVM運作資料區深度解析

1.2.4 案例二:棧深度溢出

1)代碼

這個容易實作,方法嵌套自己就可以:

package com.itheima.jvm.demo; /** * 通過一個程式模拟線程請求的棧深度大于虛拟機所允許的棧深度; * 抛出StackOverflowError */public class StackOverFlow {    /**     * 定義方法,循環嵌套自己     */    public static void B() {        B();        System.out.println("進入方法B");    }     public static void main(String[] args) {         B();        System.out.println("進入Main方法");    }}             

2)運作結果:

Exception in thread "main" java.lang.StackOverflowError	at com.itheima.jvm.demo.StackOverFlow.B(StackOverFlow.java:12)	at com.itheima.jvm.demo.StackOverFlow.B(StackOverFlow.java:12)	at com.itheima.jvm.demo.StackOverFlow.B(StackOverFlow.java:12)	at com.itheima.jvm.demo.StackOverFlow.B(StackOverFlow.java:12)	at com.itheima.jvm.demo.StackOverFlow.B(StackOverFlow.java:12)           

3)棧結構:

JVM運作資料區深度解析

1.2.5 案例三:棧記憶體溢出

一直不停的建立線程就可以堆滿棧

但是!這個很危險,到32系統的winxp上勇敢的小夥伴可以試一試,機器卡死不負責!

package com.itheima.jvm.demo; /** 棧記憶體溢出,注意!很危險,謹慎執行* 執行時可能會卡死系統。直到記憶體耗盡* */public class StackOutOfMem {    public static void main(String[] args) {        while (true) {            new Thread(() -> {                while(true);            }).start();        }    }}            

1.3 本地方法棧

1.3.1 概述

  • 本地方法棧的功能和特點類似于虛拟機棧,均具有線程隔離的特點
  • 不同的是,本地方法棧服務的對象是JVM執行的native方法,而虛拟機棧服務的是JVM執行的java方法
  • 虛拟機規範裡對這塊所用的語言、資料結構、沒有強制規定,虛拟機可以自由實作它
  • 甚至,hotspot把它和虛拟機棧合并成了1個

1.3.2 溢出異常

和虛拟機棧一樣,也是兩個:

如果是建立的棧的深度大于虛拟機允許的深度,抛出 StackOverFlowError

記憶體申請不夠的時候,抛出 OutOfMemoryError

1.4 堆

1.4.1 概述

與上面的3個不同,堆是所有線程共享的!所謂的線程安全不安全也是出自這裡。

在虛拟機啟動時建立。此記憶體區域的唯一目的就是存放對象執行個體,Java世界裡“幾乎”所有的對象執行個體都在這裡配置設定記憶體。

需要注意的是,《Java虛拟機規範》并沒有對堆進行細緻的劃分,是以對于堆的講解要基于具體的虛拟機,我們以使用最多的HotSpot虛拟機為例。

Java堆是垃圾收集器管理的記憶體區域,是以它也被稱作“GC堆”,這就是我們做JVM調優的重點區域部分。

1.4.2 jdk1.7

jvm的記憶體模型在1.7和1.8有較大的差別,雖然1.7目前使用的較少了,但是我們也是需要對1.7的記憶體模型有所了解,是以接下裡,我們将先學習1.7再學習1.8的記憶體模型。

JVM運作資料區深度解析
  • Young 年輕區(代)
  • Young區被劃分為三部分,Eden區和兩個大小嚴格相同的Survivor區
  • 其中,Survivor區間中,某一時刻隻有其中一個是被使用的,另外一個留做垃圾收集時複制對象用
  • 在Eden區間變滿的時候, GC就會将存活的對象移到空閑的Survivor區間中,根據JVM的政策,在經過幾次垃圾收集後,任然存活于Survivor的對象将被移動到下面的Tenured區間。
  • Tenured 年老區
  • Tenured區主要儲存生命周期長的對象,一般是一些老的對象,當一些對象在Young複制轉移一定的次數以後,對象就會被轉移到Tenured區,一般如果系統中用了application級别的緩存,緩存中的對象往往會被轉移到這一區間。
  • Perm 永久區
  • hotspot 1.6 才有這貨,現在已經成為曆史
  • Perm代主要儲存class,method,filed對象,這部份的空間一般不會溢出,除非一次性加載了很多的類,不過在涉及到熱部署的應用伺服器的時候,有時候會遇到java.lang.OutOfMemoryError : PermGen space 的錯誤,造成這個錯誤的很大原因就有可能是每次都重新部署,但是重新部署後,類的class沒有被解除安裝掉,這樣就造成了大量的class對象儲存在了perm中,這種情況下,一般重新啟動應用伺服器可以解決問題。另外一種可能是建立了大批量的jsp檔案,造成類資訊超出perm的上限而溢出。這種重新開機也解決不了。隻能調大空間。
  • Virtual區:
  • jvm參數可以設定一個範圍,最大記憶體和初始記憶體的內插補點,就是Virtual區。

1.4.3 jdk1.8

JVM運作資料區深度解析

由上圖可以看出,jdk1.8的記憶體模型是由2部分組成,年輕代 + 年老代。永久代被幹掉,換成了Metaspace(中繼資料空間)

年輕代:Eden + 2*Survivor (不變)

年老代:OldGen (不變)

元空間:原來的perm區 (重點!)

需要特别說明的是:Metaspace所占用的記憶體空間不是在虛拟機内部,而是在本地記憶體空間中,這也是與1.7的永久代最大的差別所在。

JVM運作資料區深度解析

1.4.4 溢出異常

記憶體不足時,抛出

java.lang.OutOfMemoryError: Java heap space

1.4.5 案例:堆溢出

1)代碼

配置設定大量對象,超出jvm規定的堆範圍即可

package com.itheima.jvm.demo; import java.util.ArrayList;import java.util.List; /** * 堆溢出 *   -Xms20m -Xmx20m */public class HeapOOM {    Byte[] bytes = new Byte[1024*1024];    public static void main(String[] args) {        List list = new ArrayList();        int i = 0;        while (true) {            System.out.println(++i);            list.add(new HeapOOM());        }    }}            

2)啟動

注意啟動時,指定一下堆的大小:

JVM運作資料區深度解析

2)輸出

12345Exception in thread "main" java.lang.OutOfMemoryError: Java heap space	at com.itheima.jvm.demo.HeapOOM.<init>(HeapOOM.java:7)	at com.itheima.jvm.demo.HeapOOM.main(HeapOOM.java:13)           

1.5 方法區

1.5.1 概述

同樣,線程共享的。

它主要用來存儲類的資訊、類裡定義的常量、靜态變量、編譯器編譯後的代碼緩存。

注意!方法區在虛拟機規範裡這是一個邏輯概念,它具體放在那個區域裡沒有嚴格的規定。

是以,hotspot 1.7 将它放在了堆的永久代裡,1.8+單獨開辟了一塊叫metaspace來存放一部分内容(不是全部!定義的類對象在堆裡)

具體方法區主要存什麼東西呢?粗略的分,可以劃分為兩類:

  • 類資訊:主要指類相關的版本、字段、方法、接口描述、引用等
  • 運作時常量池:編譯階段生成的常量與符号引用、運作時加入的動态變量
  • (常量池裡的類變量,如對象或字元串,比較特殊,1.6和1.8位置不同,下面會講到)

小提示:

這裡經常會跟上面堆裡的永久代混為一談,實際上這是兩碼事

永久代是hotspot在1.7及之前才有的設計,1.8+,以及其他虛拟機并不存在這個東西。

可以說,永久代是1.7的hotspot偷懶的結果,他在堆裡劃分了一塊來實作方法區的功能,叫永久代。因為這樣可以借助堆的垃圾回收來管理方法區的記憶體,而不用單獨為方法區再去編寫記憶體管理程式。懶惰!

同時代的其他虛拟機,如J9,Jrockit等,沒有這個概念。後來hotspot認識到,永久代來做這件事不是一個好主意。1.7已經從永久代拿走了一部分資料,直到1.8+徹底去掉了永久代,方法區大部分被移到了metaspace(再強調一下,不是全部!)

結論:

方法區是一定存在的,這是虛拟機規定的,但是是個邏輯概念,在哪裡虛拟機自己去決定

而永久代不一定存在(hotspot 1.7 才有),已成為曆史

1.5.2 溢出異常

1.6:OutOfMemoryError: PermGen space

1.8:OutOfMemoryError: Metaspace

1.5.3 案例:1.6方法區溢出

1)原理

JVM運作資料區深度解析

在1.6裡,字元串常量是運作時常量池的一部分,也就是歸屬于方法區,放在了永久代裡。

是以1.6環境下,讓方法區溢出,隻需要可勁造往字元串常量池中造字元串即可,這裡用到一個方法:

/*如果字元串常量池裡有這個字元串,直接傳回引用,不再額外添加如果沒有,加進去,傳回新建立的引用*/String.intern()           

2)代碼

/** * 方法區溢出,注意限制一下永久代的大小 * 編譯的時候注意pom裡的版本,要設定1.6,否則啟動會有問題 * jdk1.6  :     -XX:PermSize=6M -XX:MaxPermSize=6M */public class ConstantOOM {     public static void main(String[] args) {        ConstantOOM oom = new ConstantOOM();        Set<String> stringSet = new HashSet();        int i = 0;        while (true) {            System.out.println(++i);            stringSet.add(String.valueOf(i).intern());        }    }}           

3)建立啟動環境

JVM運作資料區深度解析

4)異常資訊:

...191181911919120Exception in thread "main" java.lang.OutOfMemoryError: PermGen space	at java.lang.String.intern(Native Method)	at com.itheima.jvm.demo.ConstantOOM.main(ConstantOOM.java:19)           

1.5.4 案例:1.8方法區溢出

1)到了1.8,情況發生了變化

可以測試一下,1.8下無論指定下面的哪個參數,常量池運作都不會溢出,會一直列印下去

-XX:PermSize=6M -XX:MaxPermSize=6M-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M           

2)配置運作環境

JVM運作資料區深度解析

3)控制台資訊

不會抛出異常,隻要你jvm堆記憶體夠,理論上可以一直打下去

JVM運作資料區深度解析

4)為什麼呢?

永久代我們加了限制,結果沒意義,因為1.8裡已經沒有這貨了

元空間也加了限制,同樣沒意義,那說明字元串常量池它不在元空間裡!

那麼,它在哪裡呢?

JVM運作資料區深度解析

jdk1.8以後,字元串常量池被移到了堆空間,和其他對象一樣,接受堆的控制。

其他的運作時的類資訊、基本資料類型等在元空間。

我們可以驗證一下,對上面的運作時參數再加一個堆上限限制:

-Xms10m-Xmx10m           

運作環境如下:

JVM運作資料區深度解析

運作沒多久,你會得到以下異常:

……840148401584016840178401884019Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded	at java.lang.Integer.toString(Integer.java:403)	at java.lang.String.valueOf(String.java:3099)	at com.itheima.jvm.demo.ConstantOOM.main(ConstantOOM.java:18)            

說明:1.8裡,字元串inter()被放在了堆裡,受最大堆空間的限制。

5)那如何才能讓元空間溢出呢?

既然字元串常量池不在這裡,那就換其他的。類的基本資訊總在元空間吧?我們來試一下

cglib是一個apache下的位元組碼庫,它可以在運作時生成大量的對象,我們while循環同時限制metaspace試試:

附:https://gitee.com/mirrors/cglib (想深入了解這個工具的猛擊左邊,這裡不做過多讨論)

package com.itheima.jvm.demo; import net.sf.cglib.proxy.Enhancer;import net.sf.cglib.proxy.MethodInterceptor;import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; /** * jdk8方法區溢出 *   -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M */public class ConstantOOM8 {    public static void main(final String[] args) {        while (true) {            Enhancer enhancer = new Enhancer();            enhancer.setSuperclass(OOM.class);            enhancer.setUseCache(false);            enhancer.setCallback(new MethodInterceptor() {                @Override                public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {                    return methodProxy.invokeSuper(objects,args);                }            });            enhancer.create();        }    }     static class OOM{     }}            

6)運作設定

JVM運作資料區深度解析

7)運作結果

Caused by: java.lang.OutOfMemoryError: Metaspace	at java.lang.ClassLoader.defineClass1(Native Method)	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)            

結論:

jdk8引入元空間來存儲方法區後,記憶體溢出的風險比曆史版本小多了,但是在類超出控制的時候,依然會打爆方法區

1.6 一個案例

為便于大家了解和記憶,下面我們用一個案例,把上面各個區串通起來。

假設有個Bootstrap的類,執行main方法。在jvm裡,它從class檔案到跑起來,大緻經過如下步驟:

JVM運作資料區深度解析
  1. 首先JVM會先将這個Bootstrap.class 資訊加載到記憶體中的方法區
  2. 接着,主線程開辟一塊記憶體空間,準備好程式計數器pc,虛拟機棧、本地方法棧
  3. 然後,JVM會在Heap堆上為Bootstrap.class 建立一個Bootstrap.class 的類執行個體
  4. JVM開始執行main方法,這時在虛拟機棧裡為main方法建立一個棧幀
  5. main方法在執行的過程之中,調用了greeting方法,則JVM會為greeting方法再建立一個棧幀,推到虛拟機棧頂,在main的上面,每次隻有一個棧幀處于活動狀态,目前為greeting
  6. 當greeting方法運作完成後,則greeting方法出棧,目前活動幀指向main,方法繼續往下運作

1.7 歸納總結

JVM運作資料區深度解析

1)獨享/共享的角度:

  • 獨享:程式計數器、虛拟機棧、本地方法棧
  • 共享:堆、方法區

2)error的角度:

  • 程式計數器:不會溢出,比較特殊,其他都會
  • 兩個棧:可能會發生兩種溢出,一是深度超了,報StackOverflowError,空間不足:OutOfMemoryError
  • 堆:隻會在空間不足時,報OutOfMemoryError,會提示heapSpace
  • 方法區:空間不足時,報OutOfMemoryError,提示不同,1.6是permspace,1.8是元空間,和它在什麼地方有關

3)歸屬:

  • 計數器、虛拟機棧、本地方法棧:線程建立必須申請配套,真正的實體空間
  • 堆:真正的實體空間,但是内部結構的劃分有變動,1.6有永久代,1.8被幹掉
  • 方法區:最沒歸屬感的一塊,原因就是它是一個邏輯概念。1.6被放在了堆的永久代,1.8被拆分,一部分在元空間,一部分(方法區的運作時常量池裡面的類對象,包括字元串常量,被設計放在了堆裡)
  • 直接記憶體:這塊實際上不屬于運作時資料區的一部分,而是直接操作實體記憶體。在nio操作裡DirectByteBuffer類可以對native操作,避免流在堆内外的拷貝。我們下一步的調優不會涉及到它,了解即可。