JVM系列文章如無特殊說明,一些特性均是基於Hot Spot虛擬機和JDK1.8版本講述。java
下面這張圖我想對於每一個學習Java的人來講再熟悉不過了,這就是整個JDK的關係圖:
web
從上圖咱們能夠看到,Java Virtual Machine位於最底層,全部的Java應用都是基於JVM來運行的,因此學習JVM對任何一個想要深刻了解Java的人是必不可少的。編程
Java的口號是:Write once,run anywhere(一次編寫,處處運行)。之因此能實現這個口號的緣由就是由於JVM的存在,JVM幫咱們處理好了不一樣平臺的兼容性問題,只要咱們安裝對應系統的JDK,就能夠運行,而無需關心其餘問題。api
JVM全稱Java Virtual Machine,即Java虛擬機,是一種抽象計算機。與真正的計算機同樣,它有一個指令集,並在運行時操做各類內存區域。虛擬機有不少種,不一樣的廠商提供了不一樣的實現,只要遵循虛擬機規範便可。目前咱們常說的虛擬機通常都指的是Hot Spot。數組
JVM對Java編程語言一無所知,只知道一種特定的二進制格式,即類文件格式。類文件包含Java虛擬機指令(或字節碼)和符號表,以及其餘輔助信息。也就是說,咱們寫好的程序最終交給JVM執行的時候會被編譯成爲二進制格式。安全
注意:Java虛擬機只認二進制格式文件,因此,任何語言,只要編譯以後的格式符合要求,均可以在Java虛擬機上運行,如Kotlin,Groovy等。app
從咱們寫好的.java文件到最終在JVM上運行時,大體是以下一個流程:
框架
一個java類在通過編譯和類加載機制以後,會將加載後獲得的數據放到運行時數據區內,這樣咱們在運行程序的時候直接從JVM內存中讀取對應信息就能夠了。jvm
運行時數據區:Run-Time Data Areas。Java虛擬機定義了在程序執行期間使用的各類運行時數據區域。其中一些數據區域是在Java虛擬機啓動時建立的,只在Java虛擬機退出時銷燬,這些區域是全部線程共享的,因此會有線程不安全的問題發生。而有一些數據區域爲每一個線程獨佔的,每一個線程獨佔數據區域在線程建立時建立,在線程退出時銷燬,線程獨佔的數據區就不會有安全性問題。編程語言
Run-Time Data Areas主要包括以下部分:pc寄存器,堆,方法區,虛擬機棧,本地方法棧。
PC Register是每一個線程獨佔的空間。
Java虛擬機能夠支持同時執行多個線程,而在任何一個肯定的時刻,一個處理器只會執行一個線程中的一個指令,又由於線程具備隨機性,操做系統會一直切換線程去執行不一樣的指令,因此爲了切換線程以後能回到原先執行的位置,每一個JVM線程都必需要有本身的pc(程序計數器)寄存器來獨立存儲執行信息,這樣才能繼續以前的位置日後運行。
在任什麼時候候,每一個Java虛擬機線程都在執行單個方法的代碼,即該線程的當前方法。若是該方法不是Native方法,則pc寄存器會記錄當前正在執行的Java虛擬機指令的地址。若是線程當前執行的方法是本地的,那麼Java虛擬機的pc寄存器的值是Undefined。
堆是Java虛擬機所管理內存中最大的一塊,在虛擬機啓動時建立,被全部線程共享。
堆在虛擬機啓動時建立,用於存儲全部的對象實例和數組(在某些特殊狀況下不是)。
堆中的對象永遠不會顯式地釋放,必須由GC自動回收。因此GC也主要是回收堆中的對象實例,咱們日常討論垃圾回收主要也是回收堆內存。
堆能夠處於物理上不連續的內存空間,能夠固定大小,也能夠動態擴展,經過參數-Xms和Xmx兩個參數來控制堆內存的最小和最大值。
堆可能存在以下異常狀況:
若是計算須要的堆比自動存儲管理系統提供的堆多,將拋出OutOfMemoryError錯誤。
爲了方便模擬,咱們把堆固定一下大小,設置爲:
-Xms20m -Xmx20m
而後新建一個測試類來測試一下:
package com.zwx.jvm.oom; import java.util.ArrayList; import java.util.List; public class Heap { public static void main(String[] args) { List<Integer> list = new ArrayList<>(); while (true){ list.add(99999); } } }
輸出結果爲(後面的Java heap space,表示堆空間溢出):
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:3210) at java.util.Arrays.copyOf(Arrays.java:3181)
注意:堆不能設置的過小,過小的話會啓動失敗,如上咱們把參數大小都修改成2m,會出現下面的錯誤:
Error occurred during initialization of VM GC triggered before VM initialization completed. Try increasing NewSize, current value 1536K.
方法區是各個線程共享的內存區域,在虛擬機啓動時建立。它存儲每一個類的結構,好比:運行時常量池、屬性和方法數據,以及方法和構造函數的代碼,包括在類和實例初始化以及接口初始化中使用的特殊方法。
方法區在邏輯上是堆的一部分,可是它卻又一個別名叫作Non-Heap(非堆),目的是與Java堆區分開來。
方法區域能夠是固定大小,也能夠根據計算的須要進行擴展,若是不須要更大的方法區域,則能夠收縮。方法區域的內存不須要是連續的。
方法區中可能出現以下異常:
若是方法區域中的內存沒法知足分配請求時,將拋出OutOfMemoryError錯誤。
運行時常量池是方法區中的一部分,用於存儲編譯生成的字面量和符號引用。類或接口的運行時常量池是在Java虛擬機建立類或接口時構建的。
在計算機科學中,字面量(literal)是用於表達源代碼中一個固定值的表示法(notation)。幾乎全部計算機編程語言都具備對基本值的字面量表示,諸如:整數、浮點數以及字符串等。在Java中經常使用的字面量就是基本數據類型或者被final修飾的常量或者字符串等。
字符串這裏值得拿出來單獨解釋一下,在jdk1.6以及以前的版本,Java中的字符串就是放在方法區中的運行時常量池內,可是在jdk1.7和jdk1.8版本(jdk1.8以後本人沒有深刻去了解過,因此不討論),將字符串常量池拿出來放到了堆(heap)裏。
咱們來經過一個例子來演示一下區別:
package com.zwx; public class demo { public static void main(String[] args) { String str1 = new String("lonely") + new String("wolf"); System.out.println(str1==str1.intern()); } }
這個語句的運行結果在不一樣的JDK版本中輸出的結果會不同:
JDK1.6中會輸出false:
JDK1.7中輸出true:
JDK1.8中也會輸出true:
jdk1.6及以前的版本中:
調用String.intern()方法,會先去常量池檢查是否存在當前字符串,若是不存在,則會在方法區中建立一個字符串,而new String("")方法建立的字符串在堆裏面,兩個字符串的地址不相等,故而返回false。
在jdk1.7及1.8版本中:
字符串常量池從方法區中的運行時常量池移到了堆內存中,而intern()方法也隨之作了改變。調用String.intern()方法,首先仍是會去常量池中檢查是否存在,若是不存在,那麼就會建立一個常量,並將引用指向堆,也就是說不會再從新建立一個字符串對象了,二者都會指向堆中的對象,因此返回true。
不過有一點仍是須要注意,咱們把上面的構造字符串的代碼改造一下:
String str1 = new String("ja") + new String("va"); System.out.println(str1==str1.intern()); 12
這時候在jdk1.7和jdk1.8中也會返回false。
這個差別在《深刻理解Java虛擬機》一書中給出的解釋是java這個字符串已經存在常量池了,因此我我的的推測是可能初始化的時候jdk自己須要使用到java字符串,因此常量池中就提早已經建立好了,若是理解錯了,還請你們指正,感謝!
上面的例子中我用了兩個new String(「lonely」)和new String(「wolf」)相加,而若是去掉其中一個new String()語句的話,那麼實際上jdk1.7和jdk1.8中返回的也會是false,而不是true。
這是爲何?看下面(
咱們假設一開始字符串常量池沒有任何字符串
):
只執行一個new String(「lonely」)會產生2個對象,1個在堆,1個在字符串常量池
這時候執行了String.intern()方法,String.intern()會去檢查字符串常量池,發現字符串常量池存在longly字符串,因此會直接返回,不論是jdk1.6仍是jdk1.7和jdk1.8都是檢查到字符串存在就會直接返回,因此str1==str1.intern()獲得的結果就是都是false,由於一個在堆,一個在字符串常量池。
執行new String(「lonely」)+new String(「wolf」)會產生5個對象,3個在堆,2個在字符串常量池
好了,這時候執行String.intern()方法會怎麼樣呢,若是在jdk1.7和jdk1.8會去檢查字符串常量池,發現沒有lonelywolf字符串,因此會建立一個指向堆中的字符串放到字符串常量池:
而若是是jdk1.6中,不會指向堆,會從新建立一個lonelywolf字符串放到字符串常量池,因此纔會產生不一樣的運行結果。
注意:+號的底層執行的是new StringBuild().append()語句,因此咱們再看下面一個例子:
String s1 = new StringBuilder("aa").toString(); System.out.println(s1==s1.intern()); String s2 = new StringBuilder("aa").append("bb").toString(); System.out.println(s2==s2.intern());//1.6返回false,1.7和1.8返回true
這個在jdk1.6版本所有返回false,而在jdk1.7和jdk1.8中一個返回false,一個返回true。多了一個append至關於上面的多了一個+號,原理是同樣的。
符號引用在下篇講述類加載機制的時候會進行解釋,這裏暫不作解釋,
**感興趣的能夠關注我,留意個人JVM系列下一篇文章**
。
方法區是Java虛擬機規範中的規範,可是具體如何實現並無規定,因此虛擬機廠商徹底能夠採用不一樣的方式實現方法區的。
在HotSpot虛擬機中:
jdk1.7及以前版本
方法區採用永久代(Permanent Generation)的方式來實現,方法區的大小咱們能夠經過參數-XX:PermSize和-XX:MaxPermSize來控制方法區的大小和所能容許最大值。
jdk1.8版本
移除了永久代,採用元空間(Metaspace)來實現方法區,因此在jdk1.8中關於永久代的參數-XX:PermSize和-XX:MaxPermSize已經被廢棄卻代之的是參數-XX:MetaspaceSize和-XX:MaxMetaspaceSize。元空間和永久代的一個很大的區別就是元空間已經不在jvm內存在,而是直接存儲到了本地內存中。
以下,咱們再jdk1.8中設置-XX:PermSize和-XX:MaxPermSize會給出警告:
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize1m; support was removed in 8.0 Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize1m; support was removed in 8.0
由於jdk1.7及以前都是永久代來實現方法區,因此咱們能夠經過設置永久代參數來模擬內存溢出:
設置永久代最大爲2M:
-XX:PermSize=2m -XX:MaxPermSize=2m
而後執行以下代碼:
package com.zwx; import java.util.ArrayList; import java.util.List; public class demo { public static void main(String[] args) { List<String> list = new ArrayList<>(); int i = 0; while (true){ list.add(String.valueOf(i++).intern()); } } }
最後報錯OOM:PermGen space(永久代溢出)。
Error occurred during initialization of VM java.lang.OutOfMemoryError: PermGen space at sun.misc.Launcher$ExtClassLoader.getExtClassLoader(Launcher.java:141) at sun.misc.Launcher.<init>(Launcher.java:71) at sun.misc.Launcher.<clinit>(Launcher.java:57)
jdk1.8版本,由於永久代被取消了,因此模擬方式會不同。
首先引入asm字節碼框架依賴(前面介紹動態代理的時候提到cglib動態代理也是利用了asm框架來生成字節碼,因此也能夠直接cglib的api來生成):
<dependency> <groupId>asm</groupId> <artifactId>asm</artifactId> <version>3.3.1</version> </dependency>
建立一個工具類去生成class文件:
package com.zwx.jvm.oom; import jdk.internal.org.objectweb.asm.ClassWriter; import jdk.internal.org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import java.util.ArrayList; import java.util.List; public class MetaspaceUtil extends ClassLoader { public static List<Class<?>> createClasses() { List<Class<?>> classes = new ArrayList<Class<?>>(); for (int i = 0; i < 10000000; ++i) { ClassWriter cw = new ClassWriter(0); cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null); MethodVisitor mw = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null); mw.visitVarInsn(Opcodes.ALOAD, 0); mw.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V"); mw.visitInsn(Opcodes.RETURN); mw.visitMaxs(1, 1); mw.visitEnd(); MetaspaceUtil test = new MetaspaceUtil(); byte[] code = cw.toByteArray(); Class<?> exampleClass = test.defineClass("Class" + i, code, 0, code.length); classes.add(exampleClass); } return classes; } }
設置元空間大小
-XX:MetaspaceSize=5M -XX:MaxMetaspaceSize=5M 1
而後運行測試類模擬:
package com.zwx.jvm.oom; import java.util.ArrayList; import java.util.List; public class MethodArea { public static void main(String[] args) { //jdk1.8 List<Class<?>> list=new ArrayList<Class<?>>(); while(true){ list.addAll(MetaspaceUtil.createClasses()); } } }
拋出以下異常OOM:Metaspace:
每一個Java虛擬機線程都有一個與線程同時建立的私有Java虛擬機棧。
Java虛擬機棧存儲棧幀(Frame)。每一個被調用的方法就會產生一個棧幀,棧幀中保存了一個方法的狀態信息,如:局部變量,操做棧幀,方出出口等。
調用一個方法,就會產生一個棧幀,並壓入棧內;一個方法調用完成,就會把該棧幀從棧中彈出,大體調用過程以下圖所示:
Java虛擬機棧中可能有下面兩種異常狀況:
若是線程執行所需棧深度大於Java虛擬機棧深度,則會拋出StackOverflowError。
上圖能夠知道,其實方法的調用就是入棧和出棧的過程,若是一直入棧而不出棧就容易發生異常(如遞歸)。
若是Java虛擬機棧能夠動態地擴展,可是擴展大小的時候沒法申請到足夠的內存,則會拋出一個OutOfMemoryError。
大部分Java虛擬機棧都是支持動態擴展大小的,也容許設置固定大小(在Java虛擬機規範中兩種都是能夠的,具體要看虛擬機的實現)。
注:咱們常常說的JVM中的棧,通常指的就是Java虛擬機棧。
下面是一個簡單的遞歸方法,沒有跳出遞歸條件:
package com.zwx.jvm.oom; public class JMVStack { public static void main(String[] args) { test(); } static void test(){ test(); } }
輸出結果爲:
Exception in thread "main" java.lang.StackOverflowError at com.zwx.jvm.oom.JMVStack.test(JMVStack.java:15) at com.zwx.jvm.oom.JMVStack.test(JMVStack.java:15) .....
本地方發棧相似於Java虛擬機棧,區別就是本地方法棧存儲的是Native方法。本地方發棧和Java虛擬機棧在有的虛擬機中是合在一塊兒的,並無分開,如:Hot Spot虛擬機。
本地方法棧可能出現以下異常:
若是線程執行所需棧深度大於本地方法棧深度,則會拋出StackOverflowError。
若是能夠動態擴展本地方法棧,可是擴展大小的時候沒法申請到足夠的內存,則會拋出OutOfMemoryError。
本文主要介紹了jvm運行時數據區的構造,以及每部分區域到底都存了哪些數據,而後去模擬了一下常見異常的產生方式,固然,模擬異常的方式不少,關鍵要知道每一個區域存了哪些東西,模擬的時候對應生成就能夠。
本文主要從整體上介紹運行時數據區,主要是有一個概念上的認識,下一篇,將會介紹類加載機制,以及雙親委派模式,介紹類加載模式的同時會對運行時數據區作更詳細的介紹。
**請關注我,一塊兒學習進步**