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