運作資料區
位元組碼隻是一個二進制檔案存放在那裡。要想在jvm裡跑起來,先得有個運作的記憶體環境。
也就是我們所說的jvm運作時資料區。
1)運作時資料區的位置
運作時資料區是jvm中最為重要的部分,執行引擎頻繁操作的就是它。類的初始化,以及後面我們講的對象空間的配置設定、垃圾的回收都是在這塊區域發生的。
2)區域劃分
根據《Java虛拟機規範》中的規定,在運作時資料區将記憶體細分為幾個部分
線程私有的:Java虛拟機棧(Java Virtual Machine Stack)、程式計數器(Program Counter Register)、本地方法棧(Native Method Stacks)
大家共享的:方法區(Method Area)、Java堆區(Java Heap)
接下來我們分塊詳細來解讀,每一塊是做什麼的,如果溢出了會發生什麼事情
1.1 程式計數器
1.1.1 概述
程式計數器(Program Counter Register)
- 每個線程一個。是一塊較小的記憶體空間,它表示目前線程執行的位元組碼指令的位址。
- 位元組碼解釋器工作時,通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,是以整個程式無論是分支、循環、跳轉、異常處理、線程恢複等基礎功能都需要依賴這個計數器來完成。
- 由于線程是多條并行執行的,互相之間執行到哪條指令是不一樣的,是以每條線程都需要有一個獨立的程式計數器,各條線程之間計數器互不影響,獨立存儲,我們稱這類記憶體區域為“線程私有”的記憶體。
- 如果是native方法,這裡為空
1.1.2 溢出異常
沒有!
在虛拟機規範中,沒有對這塊區域設定記憶體溢出規範,也是唯一一個不會溢出的區域
1.1.3 案例
因為它不會溢出,是以我們沒有辦法給它造一個,但是從class類上可以找到痕迹。
回顧上面javap的反彙編,其中code所對應的編号就可以了解為計數器中所記錄的執行編号。
1.2 虛拟機棧
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方法
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)棧結構:
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的記憶體模型。
- 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
由上圖可以看出,jdk1.8的記憶體模型是由2部分組成,年輕代 + 年老代。永久代被幹掉,換成了Metaspace(中繼資料空間)
年輕代:Eden + 2*Survivor (不變)
年老代:OldGen (不變)
元空間:原來的perm區 (重點!)
需要特别說明的是:Metaspace所占用的記憶體空間不是在虛拟機内部,而是在本地記憶體空間中,這也是與1.7的永久代最大的差別所在。
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)啟動
注意啟動時,指定一下堆的大小:
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)原理
在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)建立啟動環境
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)配置運作環境
3)控制台資訊
不會抛出異常,隻要你jvm堆記憶體夠,理論上可以一直打下去
4)為什麼呢?
永久代我們加了限制,結果沒意義,因為1.8裡已經沒有這貨了
元空間也加了限制,同樣沒意義,那說明字元串常量池它不在元空間裡!
那麼,它在哪裡呢?
jdk1.8以後,字元串常量池被移到了堆空間,和其他對象一樣,接受堆的控制。
其他的運作時的類資訊、基本資料類型等在元空間。
我們可以驗證一下,對上面的運作時參數再加一個堆上限限制:
-Xms10m-Xmx10m
運作環境如下:
運作沒多久,你會得到以下異常:
……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)運作設定
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會先将這個Bootstrap.class 資訊加載到記憶體中的方法區
- 接着,主線程開辟一塊記憶體空間,準備好程式計數器pc,虛拟機棧、本地方法棧
- 然後,JVM會在Heap堆上為Bootstrap.class 建立一個Bootstrap.class 的類執行個體
- JVM開始執行main方法,這時在虛拟機棧裡為main方法建立一個棧幀
- main方法在執行的過程之中,調用了greeting方法,則JVM會為greeting方法再建立一個棧幀,推到虛拟機棧頂,在main的上面,每次隻有一個棧幀處于活動狀态,目前為greeting
- 當greeting方法運作完成後,則greeting方法出棧,目前活動幀指向main,方法繼續往下運作
1.7 歸納總結
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操作,避免流在堆内外的拷貝。我們下一步的調優不會涉及到它,了解即可。