農林漁牧網

您現在的位置是:首頁 > 漁業

深入分析Java虛擬機器堆和棧及OutOfMemory異常產生原因

2022-02-28由 全棧技術資源社群 發表于 漁業

out of memory是什麼意思

深入分析Java虛擬機器堆和棧及OutOfMemory異常產生原因

前言

JVM系列文章如無特殊說明,一些特性均是基於Hot Spot虛擬機器和JDK1。8版本講述。

下面這張圖我想對於每個學習Java的人來說再熟悉不過了,這就是整個JDK的關係圖:

深入分析Java虛擬機器堆和棧及OutOfMemory異常產生原因

從上圖我們可以看到,JavaVirtualMachine位於最底層,所有的Java應用都是基於JVM來執行的,所以學習JVM對任何一個想要深入瞭解Java的人是必不可少的。

Java的口號是:Write once,run anywhere(一次編寫,到處執行)。這就是因為JVM的存在,JVM幫我們處理好了不同平臺的相容性問題,只要我們安裝對應系統的JDK,就可以執行,而無需關心其他問題。

什麼是JVM

JVM全稱Java Virtual Machine,即Java虛擬機器,是一種抽象計算機。與真正的計算機一樣,它有一個指令集,並在執行時操作各種記憶體區域。虛擬機器有很多種,不同的廠商提供了不同的實現,只要遵循虛擬機器規範即可。目前我們常說的虛擬機器一般都指的是Hot Spot。

JVM對Java程式語言一無所知,只知道一種特定的二進位制格式,即類檔案格式。類檔案包含Java虛擬機器指令(或位元組碼)和符號表,以及其他輔助資訊。也就是說,我們寫好的程式最終交給JVM執行的時候會被編譯成為二進位制格式。

注意:Java虛擬機器只認二進位制格式檔案,所以,任何語言,只要編譯之後的格式符合要求,都可以在Java虛擬機器上執行,如Kotlin,Groovy等。

Java程式執行流程

從我們寫好的。java檔案到最終在JVM上執行時,大致是如下一個流程:

深入分析Java虛擬機器堆和棧及OutOfMemory異常產生原因

一個java類在經過編譯和類載入機制之後,會將載入後得到的資料放到執行時資料區內,這樣我們在執行程式的時候直接從JVM記憶體中讀取對應資訊就可以了。

執行時資料區

執行時資料區:Run-Time Data Areas。Java虛擬機器定義了在程式執行期間使用的各種執行時資料區域。其中一些資料區域是在Java虛擬機器啟動時建立的,只在Java虛擬機器退出時銷燬,這些區域是所有執行緒共享的,所以會有執行緒不安全的問題發生。而有一些資料區域為每個執行緒獨佔的,每個執行緒獨佔資料區域線上程建立時建立,線上程退出時銷燬,執行緒獨佔的資料區就不會有安全性問題。

Run-Time Data Areas主要包括如下部分:pc暫存器,堆,方法區,虛擬機器棧,本地方法棧。

PC(program counter) Register(程式計數器)

PC Register是每個執行緒獨佔的空間。

Java虛擬機器可以支援同時執行多個執行緒,而在任何一個確定的時刻,一個處理器只會執行一個執行緒中的一個指令,又因為執行緒具有隨機性,作業系統會一直切換執行緒去執行不同的指令,所以為了切換執行緒之後能回到原先執行的位置,每個JVM執行緒都必須要有自己的pc(程式計數器)暫存器來獨立儲存執行資訊,這樣才能繼續之前的位置往後執行。

在任何時候,每個Java虛擬機器執行緒都在執行單個方法的程式碼,即該執行緒的當前方法。如果該方法不是Native方法,則pc暫存器會記錄當前正在執行的Java虛擬機器指令的地址。如果執行緒當前執行的方法是本地的,那麼Java虛擬機器的pc暫存器的值是Undefined。

Heap(堆)

堆是Java虛擬機器所管理記憶體中最大的一塊,在虛擬機器啟動時建立,被所有執行緒共享。

堆在虛擬機器啟動時建立,用於儲存所有的物件例項和陣列(在某些特殊情況下不是)。

堆中的物件永遠不會顯式地釋放,必須由GC自動回收。所以GC也主要是回收堆中的物件例項,我們平常討論垃圾回收主要也是回收堆記憶體。

堆可以處於物理上不連續的記憶體空間,可以固定大小,也可以動態擴充套件,透過引數-Xms和Xmx兩個引數來控制堆記憶體的最小和最大值。

堆可能存在如下異常情況:

如果計算需要的堆比自動儲存管理系統提供的堆多,將丟擲OutOfMemoryError錯誤。

模擬堆內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 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。

Method Area(方法區)

方法區是各個執行緒共享的記憶體區域,在虛擬機器啟動時建立。它儲存每個類的結構,比如:執行時常量池、屬性和方法資料,以及方法和建構函式的程式碼,包括在類和例項初始化以及介面初始化中使用的特殊方法。

方法區在邏輯上是堆的一部分,但是它卻又一個別名叫做Non-Heap(非堆),目的是與Java堆區分開來。

方法區域可以是固定大小,也可以根據計算的需要進行擴充套件,如果不需要更大的方法區域,則可以收縮。方法區域的記憶體不需要是連續的。

方法區中可能出現如下異常:

如果方法區域中的記憶體無法滿足分配請求時,將丟擲OutOfMemoryError錯誤。

Run-Time Constant Pool(執行時常量池)

執行時常量池是方法區中的一部分,用於儲存編譯生成的字面量和符號引用。類或介面的執行時常量池是在Java虛擬機器建立類或介面時構建的。

字面量

在計算機科學中,字面量(literal)是用於表達原始碼中一個固定值的表示法(notation)。幾乎所有計算機程式語言都具有對基本值的字面量表示,諸如:整數、浮點數以及字串等。在Java中常用的字面量就是基本資料型別或者被final修飾的常量或者字串等。

String字串去哪了

字串這裡值得拿出來單獨解釋一下,在jdk1。6以及之前的版本,Java中的字串就是放在方法區中的執行時常量池內,但是在jdk1。7和jdk1。8版本(jdk1。8之後本人沒有深入去了解過,所以不討論),將字串常量池拿出來放到了堆(heap)裡。

我們來透過一個例子來演示一下區別:

package com。zwx;

public class demo {

String str1 = new String(“lonely”) + new String(“wolf”);

System。out。println(str1==str1。intern());

這個語句的執行結果在不同的JDK版本中輸出的結果會不一樣:

JDK1。6中會輸出false:

深入分析Java虛擬機器堆和棧及OutOfMemory異常產生原因

JDK1。7中輸出true:

深入分析Java虛擬機器堆和棧及OutOfMemory異常產生原因

JDK1。8中也會輸出true:

深入分析Java虛擬機器堆和棧及OutOfMemory異常產生原因

intern()方法

jdk1。6及之前的版本中:

呼叫String。intern()方法,會先去常量池檢查是否存在當前字串,如果不存在,則會在方法區中建立一個字串,而new String(“”)方法建立的字串在堆裡面,兩個字串的地址不相等,故而返回false。

在jdk1。7及1。8版本中:

字串常量池從方法區中的執行時常量池移到了堆記憶體中,而intern()方法也隨之做了改變。呼叫String。intern()方法,首先還是會去常量池中檢查是否存在,如果不存在,那麼就會建立一個常量,並將引用指向堆,也就是說不會再重新建立一個字串物件了,兩者都會指向堆中的物件,所以返回true。

不過有一點還是需要注意,我們把上面的構造字串的程式碼改造一下:

String str1 = new String(“ja”) + new String(“va”);

這時候在jdk1。7和jdk1。8中也會返回false。

這個差異在《深入理解Java虛擬機器》一書中給出的解釋是java這個字串已經存在常量池了,所以我個人的推測是可能初始化的時候jdk本身需要使用到java字串,所以常量池中就提前已經建立好了,如果理解錯了,還請大家指正,感謝!

new String(“lonely”)建立了幾個物件

上面的例子中我用了兩個new String(“lonely”)和new String(“wolf”)相加,而如果去掉其中一個new String()語句的話,那麼實際上jdk1。7和jdk1。8中返回的也會是false,而不是true。

這是為什麼?看下面(

我們假設一開始字串常量池沒有任何字串

):

只執行一個new String(“lonely”)會產生2個物件,1個在堆,1個在字串常量池

深入分析Java虛擬機器堆和棧及OutOfMemory異常產生原因

這時候執行了String。intern()方法,String。intern()會去檢查字串常量池,發現字串常量池存在longly字串,所以會直接返回,不管是jdk1。6還是jdk1。7和jdk1。8都是檢查到字串存在就會直接返回,所以str1==str1。intern()得到的結果就是都是false,因為一個在堆,一個在字串常量池。

執行newString(“lonely”)+newString(“wolf”)會產生5個物件,3個在堆,2個在字串常量池

深入分析Java虛擬機器堆和棧及OutOfMemory異常產生原因

好了,這時候執行String。intern()方法會怎麼樣呢,如果在jdk1。7和jdk1。8會去檢查字串常量池,發現沒有lonelywolf字串,所以會建立一個指向堆中的字串放到字串常量池:

深入分析Java虛擬機器堆和棧及OutOfMemory異常產生原因

而如果是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系列下一篇文章。

jdk1。7和1。8的實現方法區的差異

方法區是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

模擬方法區OutOfMemoryError

因為jdk1。7及之前都是永久代來實現方法區,所以我們可以透過設定永久代引數來模擬記憶體溢位:

設定永久代最大為2M:

-XX:PermSize=2m -XX:MaxPermSize=2m

深入分析Java虛擬機器堆和棧及OutOfMemory異常產生原因

然後執行如下程式碼:

List list = new ArrayList<>();

int i = 0;

list。add(String。valueOf(i++)。intern());

最後報錯OOM:PermGen space(永久代溢位)。

java。lang。OutOfMemoryError: PermGen space

at sun。misc。Launcher$ExtClassLoader。getExtClassLoader(Launcher。java:141)

at sun。misc。Launcher。(Launcher。java:71)

at sun。misc。Launcher。(Launcher。java:57)

jdk1。8

jdk1。8版本,因為永久代被取消了,所以模擬方式會不一樣。

首先引入asm位元組碼框架依賴(前面介紹動態代理的時候提到cglib動態代理也是利用了asm框架來生成位元組碼,所以也可以直接cglib的api來生成):

asm

asm

3。3。1

建立一個工具類去生成class檔案:

import jdk。internal。org。objectweb。asm。ClassWriter;

import jdk。internal。org。objectweb。asm。MethodVisitor;

import org。objectweb。asm。Opcodes;

public class MetaspaceUtil extends ClassLoader {

public static List> createClasses() {

List> classes = new ArrayList>();

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, “”,

“()V”, null, null);

mw。visitVarInsn(Opcodes。ALOAD, 0);

mw。visitMethodInsn(Opcodes。INVOKESPECIAL, “java/lang/Object”,

”, “()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

深入分析Java虛擬機器堆和棧及OutOfMemory異常產生原因

然後執行測試類模擬:

public class MethodArea {

//jdk1。8

List> list=new ArrayList>();

list。addAll(MetaspaceUtil。createClasses());

丟擲如下異常OOM:Metaspace:

深入分析Java虛擬機器堆和棧及OutOfMemory異常產生原因

Java Virtual Machine Stacks(Java虛擬機器棧)

每個Java虛擬機器執行緒都有一個與執行緒同時建立的私有Java虛擬機器棧。

Java虛擬機器棧儲存棧幀(Frame)。每個被呼叫的方法就會產生一個棧幀,棧幀中儲存了一個方法的狀態資訊,如:區域性變數,操作棧幀,方出出口等。

呼叫一個方法,就會產生一個棧幀,並壓入棧內;一個方法呼叫完成,就會把該棧幀從棧中彈出,大致呼叫過程如下圖所示:

深入分析Java虛擬機器堆和棧及OutOfMemory異常產生原因

Java虛擬機器棧中可能有下面兩種異常情況:

如果執行緒執行所需棧深度大於Java虛擬機器棧深度,則會丟擲StackOverflowError。

上圖可以知道,其實方法的呼叫就是入棧和出棧的過程,如果一直入棧而不出棧就容易發生異常(如遞迴)。

如果Java虛擬機器棧可以動態地擴充套件,但是擴充套件大小的時候無法申請到足夠的記憶體,則會丟擲一個OutOfMemoryError。

大部分Java虛擬機器棧都是支援動態擴充套件大小的,也允許設定固定大小(在Java虛擬機器規範中兩種都是可以的,具體要看虛擬機器的實現)。

注:我們經常說的JVM中的棧,一般指的就是Java虛擬機器棧。

模擬棧內StackOverflowError

下面是一個簡單的遞迴方法,沒有跳出遞迴條件:

public class JMVStack {

test();

static void test(){

輸出結果為:

Exception in thread “main” java。lang。StackOverflowError

at com。zwx。jvm。oom。JMVStack。test(JMVStack。java:15)

。。。。。

Native Method Stacks(本地方法棧)

本地方發棧類似於Java虛擬機器棧,區別就是本地方法棧儲存的是Native方法。本地方發棧和Java虛擬機器棧在有的虛擬機器中是合在一起的,並沒有分開,如:Hot Spot虛擬機器。

本地方法棧可能出現如下異常:

如果執行緒執行所需棧深度大於本地方法棧深度,則會丟擲StackOverflowError。

如果可以動態擴充套件本地方法棧,但是擴充套件大小的時候無法申請到足夠的記憶體,則會丟擲OutOfMemoryError。

總結

本文主要介紹了jvm執行時資料區的構造,以及每部分割槽域到底都存了哪些資料,然後去模擬了一下常見異常的產生方式,當然,模擬異常的方式很多,關鍵要知道每個區域存了哪些東西,模擬的時候對應生成就可以。

深入分析Java虛擬機器堆和棧及OutOfMemory異常產生原因