运行时错误7内存溢出_JVM运行时内存数据区域
閱讀本文大概需要5分鐘
作者:AI喬治出處:https://my.oschina.net/u/3611782/blog/45305121 討論背景
周志明老師寫的《深入理解Java虛擬機》應(yīng)該很多程序員都讀過,第二章中闡述了Java虛擬機在執(zhí)行Java程序的過程中是如何管理內(nèi)存的,以及這些內(nèi)存是如何被劃分成更細的邏輯區(qū)域的。如下圖所示,按照書中的論述JVM運行時數(shù)據(jù)區(qū)域包含以下幾個數(shù)據(jù)區(qū)[1]。
按照《Java虛擬機規(guī)范(Java SE 7版)》,各區(qū)域的功能簡要介紹如下:
程序計數(shù)器:各線程私有。用于記錄每個線程下一條待執(zhí)行的字節(jié)碼指令以及相關(guān)信息。這是唯一的不會拋出OOM異常的區(qū)域。
Java虛擬機棧:各線程私有。虛擬機棧由一個個的棧幀組成,每個棧幀包含了對應(yīng)方法執(zhí)行所需要的信息,具體包括:局部變量表、操作數(shù)棧(類似于編譯型語言體系下的數(shù)據(jù)寄存器)、動態(tài)鏈接(某些接口符號可能會動態(tài)的指向不同的目標方法)、函數(shù)返回地址以及其他一些相關(guān)信息。理論上當函數(shù)調(diào)用鏈超過棧的深度時就會觸發(fā)StackOverflow,當該區(qū)域設(shè)置為動態(tài)擴展時,虛擬機無法為棧申請到更多內(nèi)存時就會觸發(fā)OOM。事實中基本上不管哪種情況,結(jié)果都很可能會是StackOverflow,因為棧容量和棧幀的大小決定了棧的深度(棧幀大小*深度<=棧容量),所以當OOM時,棧深度一定也已經(jīng)不夠用了,所以拋出StackOverflow異常也無可厚非。可以通過“-Xss”來配置虛擬機棧固定大小。
Java堆:各線程公有。虛擬機工作的主要內(nèi)存區(qū)域(大部分情況下也是最大的),絕大部分對象實例的內(nèi)存分配都在這里進行。Java 7和之前的Java堆細分為:新生代(伊甸區(qū)、存活區(qū)0、存活區(qū)1)、年老代和永久代。Java 8去除了永久代,替換以Metaspace。在JVM的運行中,大部分情況下,GC主要就發(fā)生在堆區(qū)域,
方法區(qū):各線程公有。用于存放類定義、常量池、靜態(tài)變量(static修飾)、編譯后的字節(jié)碼等。方法區(qū)實際上是從堆上劃分出來的一塊區(qū)域,但是其GC機制是單獨的,與堆不同,所以為了區(qū)分方法區(qū)和堆,通常又把方法區(qū)叫做“非堆”。方法區(qū)對應(yīng)了堆中的永久代。因此在Java8以及之后版本中,永久代被抹除了,方法區(qū)也移到了元數(shù)據(jù)空間(metaspace)中。
運行時常量池:各線程公有。用于存放類信息中的常量(字面量、符號引用等),每個類編譯后的信息中的都有一個常量池,可以通過javap -vebose xxxx.class命令來查看。
直接內(nèi)存:進程間公有。直接內(nèi)存不屬于Java虛擬機運行時數(shù)據(jù)區(qū)的一部分,它是指操作系統(tǒng)分配給虛擬機以及其他進程所運行的那塊內(nèi)存區(qū)域,之所以這么說,是因為很多服務(wù)器都是虛擬機(操作系統(tǒng)級別),對于物理機來說,這塊內(nèi)存就是指操作系統(tǒng)所管控的物理內(nèi)存。通過在堆中創(chuàng)建一個DirectByteBuffer實例來對直接內(nèi)存進行訪問。
很多讀者了解完這些后還是云里霧里,各論壇還是會出現(xiàn)各種沒有定論的問題,比如
字符串常量池屬于哪個數(shù)據(jù)區(qū)?書中對字符串常量池和運行時常量池描述的相當晦澀和模糊。
Java6、Java7和Java8的運行時內(nèi)存數(shù)據(jù)區(qū)域到底有何不一樣?
什么是字面量,什么又是字符串常量?
什么是本地內(nèi)存?他和直接內(nèi)存相同嘛?什么又是堆外內(nèi)存?
下面我們圍繞這幾個問題做一些討論和引申,從而幫助我們更好的理解運行時數(shù)據(jù)區(qū)域劃分。
2 字符串常量池
我們先來回答第一和第二個問題。
2.1 字符串常量池在哪
在不同的Java版本中,規(guī)范規(guī)定的字符串常量池的位置也不一樣。以下三張圖分別代表了Java6、Java7和Java8體系下的Java虛擬機與運行時數(shù)據(jù)區(qū)域劃分,哪些是線程私有,哪些是線程公有,哪些又是進程間公有都比較清晰了。
2.1.1 Java 6 虛擬機運行數(shù)據(jù)區(qū)
當我們聽到“字符串常量池也是方法區(qū)的一部分”的時候,我們要知道他大概暗指的是Java 6或者之前的版本。如上圖所示,在Java 6虛擬機規(guī)范中,字符串常量池確實是方法區(qū)的一部分,受永久代內(nèi)存區(qū)大小的限制。當頻繁使用Spring.intern()時,可能會引發(fā)OOM(PermGen space)。
2.1.2 Java 7 虛擬機運行數(shù)據(jù)區(qū)
從Java 7 開始,規(guī)范將字符串常量池遷移到了Java堆中,受Java堆大小的限制。當頻繁大量使用String.intern()時,可能會引發(fā)OOM(Java heap space)。
2.1.3 Java 8 虛擬機運行數(shù)據(jù)區(qū)
Java 8 虛擬機規(guī)范徹底移除了永久代(-XX:Permsize和-XX:MaxPermsize均已失效),替而代之的則是元空間(Metaspace)。字符串常量池仍然在Java堆中,但方法區(qū)已經(jīng)遷移到了元空間中。這時候由于濫用 String.intern()引發(fā)的OOM依舊在Java堆中。
2.2 字符串常量池是啥
那么字符串常量池的數(shù)據(jù)結(jié)構(gòu)是怎么實現(xiàn)的呢?答案是HashMap,每個字符串常量池對應(yīng)了一個StringTable的數(shù)據(jù)結(jié)構(gòu),其本質(zhì)并不是Table,而是一個HashMap。這個HashMap的容量是固定的(默認1009),可以通過-XX:StringTableSize來設(shè)置,注意這個值是指哈希表中桶的數(shù)量,不是占用內(nèi)存的大小。所以這個值最好是一個質(zhì)數(shù),并且要大于默認的1009[2]。
3 字面量和字符串常量
如以下代碼:
String str = "123";其中”123”就是我們經(jīng)常看到的“字面量”。字面量是隨著Class信息等在類被加載完畢后一起進入運行時常量池的。而
String str2 = str.intern();這句代碼則嘗試將str的值放入字符串常量池,然而”123”已經(jīng)在類信息的常量池中了,所以StringTable實際記錄的是類信息常量池中該字符串的引用。
對于語句:
String str = new StringBuilder("hello").append(" world").toString().intern();這會將新創(chuàng)建的“hello world”的堆內(nèi)對象引用(str)放入到字符串常量池中,因為這是第一次出現(xiàn),沒有其他地方存在該值的引用。
4 本地內(nèi)存和直接內(nèi)存
首先需要說明的是,本地內(nèi)存(Native Memory)和堆外內(nèi)存(Off-heap Memory)的含義是一樣的。而關(guān)于直接內(nèi)存和本地內(nèi)存的關(guān)系,StackOverflow上也沒有說清楚的帖子,第二部分中的三張圖已經(jīng)可以很好的說明直接內(nèi)存和本地內(nèi)存的關(guān)系了,所謂的本地內(nèi)存是操作系統(tǒng)分配給JVM虛擬機(作為一個進程)使用的內(nèi)存塊中除去堆的那一部分。而直接內(nèi)存則是所有進程共享的操作系統(tǒng)所控制的內(nèi)存。所以可以這么說:本地內(nèi)存和直接內(nèi)存的關(guān)系就像“蘋果”和“水果”的關(guān)系,蘋果屬于水果,是水果更具體的限定。Java8中的元空間就屬于本地內(nèi)存空間,而他們都是直接內(nèi)存的一部分。通過DirectByteBuffer分配的內(nèi)存區(qū)域一定在本地內(nèi)存中,它也受直接內(nèi)存大小的限制。本地內(nèi)存的大小也有限制,比如Window中對每個程序運行所需的內(nèi)存大小做了2G的默認限制,這只時候其上運行的JVM的本地內(nèi)存大小≈2G-JVM堆內(nèi)存大小。
5 字符串常量池所屬數(shù)據(jù)區(qū)的具體說明
下面我們舉2個例子討論下在Java6和Java7(含之后版本)下字符串常量池遷移帶來的變化
5.1 例子1
請給出以下代碼拋出異常的類型:
import java.util.ArrayList;import java.util.List;
public class Test {
public static void main(String[] args){
Listlist = new ArrayList();int i = 0;while(true) {list.add( String.valueOf(i++).intern());
}
}
}
然后啟動參數(shù)中我們加上:
-XX:PermSize=10M -XX:MaxPermSize=10M分析下這個代碼,其意圖在于不斷的產(chǎn)生新的字符串,并且放入字符串常量池中,試圖撐爆永久代。然而這只會在Java 6 中發(fā)生,對于Java7和Java8來說,字符串常量池已經(jīng)遷移到了Java堆中,如果這時候我們添加以下虛擬機參數(shù):
-Xms10M -Xmx10M則會引發(fā):java.lang.OutOfMemoryError: GC overhead limit exceeded 這樣的錯誤,這個異常的本質(zhì)與 OOM(Heap space)一直,都是堆內(nèi)存溢出。
5.2 例子2
以下代碼在Java6和Java7中輸出也不相同:
public class TestStringConstantPool {
public static String hello = "Hello Java";
public static void main(String[] args) {
String str1 = new StringBuilder("Hello ").append("World").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("Hello ").append("Java").toString();
System.out.println(str2.intern() == str2);
}
}
在Java6中會輸出:
falsefalse
在Java7中則輸出:
truefalse
首先我們分析下Java6中的場景,Java6中字符串常量池還是運行時常量池的一部分,所以使用String.intern()時,會把堆中的字符串復(fù)制到方法區(qū)中,返回的是方法區(qū)中的對象引用。所以不管如何,堆中對象和方法區(qū)中對象應(yīng)用都不會想等。而在Java7中,這個情況發(fā)生了變化,字符串常量池轉(zhuǎn)移到了堆中,對于str1來說,字符串常量池StringTable會記錄其在堆中的引用(即str1)。所以str1.intern() == str1成立。而str2情況則不一樣了,因為“Hello Java”字符串已經(jīng)存在于方法區(qū)的運行時常量池中,所以intern()返回的是方法區(qū)中的對象引用。所以str2.intern() == str2不成立。
國民程序員總結(jié)
以上是生活随笔為你收集整理的运行时错误7内存溢出_JVM运行时内存数据区域的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: uvm 形式验证_一种基于UVM的总线验
- 下一篇: endnotex7怎么导入中文文献_En