記得有位大佬曾經(jīng)說(shuō)過(guò)這樣一句話:
如欲征服java,必須征服java虛擬機(jī)甚垦,如欲征服java虛擬機(jī)茶鹃,需先征服java虛擬機(jī)內(nèi)存模型涣雕。
java虛擬機(jī)內(nèi)存艰亮,是java虛擬機(jī)進(jìn)行對(duì)象內(nèi)存空間分配、垃圾回收的活動(dòng)室挣郭,只有先了解java虛擬機(jī)內(nèi)存才能在此基礎(chǔ)上進(jìn)一步了解對(duì)象內(nèi)存分配迄埃、垃圾回收等活動(dòng)。有別于真實(shí)物理機(jī)硬盤兑障、主存侄非、緩存、寄存器的存儲(chǔ)模型流译,java虛擬機(jī)內(nèi)存模型按照其存儲(chǔ)模塊負(fù)責(zé)的數(shù)據(jù)類型將其劃分為如下圖所示的模型:
堆
堆是各個(gè)線程共享的內(nèi)存區(qū)域逞怨,是java對(duì)象內(nèi)存分配和垃圾回收的主戰(zhàn)場(chǎng),幾乎所有的對(duì)象都是在堆中創(chuàng)建的福澡。根據(jù)Java虛擬機(jī)規(guī)范(Java Virtual Machine Specification) 的規(guī)則叠赦,Java堆可以處于物理上不連續(xù)的內(nèi)存空間中,只要邏輯上是連續(xù)的即可革砸。如果在堆中沒有內(nèi)存空間完成Java對(duì)象的內(nèi)存分配時(shí)除秀,將會(huì)拋出OutOfMemoryError(一下簡(jiǎn)稱OOM)。
關(guān)于堆的最常見虛擬機(jī)參數(shù):
- -Xms :表示虛擬機(jī)堆的最小值算利,如 -Xms10M 表示堆的最小值為10MB
- -Xmx :表示虛擬機(jī)堆的最大值册踩,如果 -Xmx100M 表示堆的最大值為100MB
/**
* 設(shè)置虛擬機(jī)參數(shù)為:-Xms5M -Xmx5M
*/
public class HeapOOM {
public static void main(String[] args) {
ArrayList<Byte[]> bytes = new ArrayList<>();
for (; ; ) {
Byte[] _1M = new Byte[1024 * 1024];
bytes.add(_1M);
}
}
}
執(zhí)行結(jié)果:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at jvm.post1.HeapOOM.main(HeapOOM.java:15)
“Java heap space”類型的OOM表示堆中沒有可用的內(nèi)存空間,具體到本例子中就是在大小為5M的堆中沒有可用空間分配給大小為1M的數(shù)組對(duì)象效拭。再來(lái)看一個(gè)例子:
/**
* 虛擬機(jī)參數(shù) -Xms5M -Xmx5M
*/
public class HeapOOM1 {
public static void main(String[] args) {
ArrayList<Object> heapOOM1s = new ArrayList<>();
for (; ; ) {
heapOOM1s.add(new Object());
}
}
}
執(zhí)行結(jié)果:
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at jvm.post1.HeapOOM1.main(HeapOOM1.java:14)
“GC overhead limit exceeded” 類型的OOM是在jdk6后引入的一種新的錯(cuò)誤類型暂吉。發(fā)生錯(cuò)誤的原因是虛擬機(jī)用了大量的時(shí)間進(jìn)行GC但是只釋放了較小的空間,這是虛擬機(jī)的一種保護(hù)機(jī)制缎患。具體到本例子中就是虛擬機(jī)在GC時(shí)沒有能回收內(nèi)存空間借笙,浪費(fèi)了時(shí)間卻沒有收獲,所以就拋出了這個(gè)錯(cuò)誤较锡∫导冢可以用 -XX:-UseGCOverheadLimit參數(shù)禁用這個(gè)檢查,但解決不了內(nèi)存問(wèn)題蚂蕴,只是把錯(cuò)誤的信息延后低散,替換成 java.lang.OutOfMemoryError: Java heap space錯(cuò)誤俯邓。
方法區(qū)
方法區(qū)和堆一樣,也是各個(gè)線程共享的內(nèi)存區(qū)域熔号,它用來(lái)存儲(chǔ)已經(jīng)被虛擬機(jī)加載的類信息稽鞭、常量池、靜態(tài)變量等引镊。方法區(qū)是jdk5到j(luò)dk8變化較大的java虛擬機(jī)內(nèi)存區(qū)域朦蕴。在jdk5和jdk6時(shí),常量池是存在方法區(qū)的:
而從jdk7及其以后的版本弟头,常量池被放到了堆里面:
常量池就是java語(yǔ)言系統(tǒng)級(jí)別的緩存吩抓,目的是讓程序在運(yùn)行過(guò)程中速度更快,更節(jié)省內(nèi)存空間赴恨,java的8種基本數(shù)據(jù)類型外加String類型疹娶,共9種類型都有對(duì)應(yīng)的常量池。這些類型的對(duì)象不可能全都放到常量池中存儲(chǔ)伦连,因此不同的類型有不同的存儲(chǔ)策略雨饺,具體到String類型的對(duì)象來(lái)說(shuō),有如下三條規(guī)則:
- 用雙引號(hào)創(chuàng)建的對(duì)象放在常量池中惑淳,如 "Hello"额港,"Jvm"這種。
- 用雙引號(hào)創(chuàng)建的對(duì)象相加產(chǎn)生的對(duì)象放在常量池歧焦,如 String s = "Hello" + "Jvm";移斩,這里的s對(duì)象就是放在常量池中的。
- 調(diào)用String對(duì)象的intern方法會(huì)返回一個(gè)存放在常量池中的String對(duì)象,且兩個(gè)對(duì)象內(nèi)容相同倚舀。
再回到本篇的主題上叹哭,因?yàn)槌A砍匚恢玫淖兓诓煌膉dk版本下痕貌,下面代碼的執(zhí)行結(jié)果是不一樣的:
public class ConstantsPool {
public static void main(String[] args) {
String s = new String("Hello") + new String("Jvm"); //1
String s1 = s.intern(); //2
System.out.println(s == s1); //jdk5和jdk6中返回false风罩,jdk7及其以上版本返回true。
}
}
在jdk7之前舵稠,程序在執(zhí)行//2處代碼之前常量池中沒有"HelloJvm"這個(gè)字符串常量超升,//2處代碼執(zhí)行時(shí),程序會(huì)在常量池中創(chuàng)建一個(gè)"HelloJvm"的字符串對(duì)象s1并返回哺徊,而常量池是在方法區(qū)的室琢。那一個(gè)在堆中的s對(duì)象和方法區(qū)中的s1對(duì)象比較地址是否相同,當(dāng)然會(huì)得到false落追。
在jdk7及其以后的版本盈滴,程序在執(zhí)行//2出代碼時(shí),發(fā)現(xiàn)常量池中同樣沒有"HelloJvm"這個(gè)對(duì)象,但因?yàn)槌A砍匾呀?jīng)遷移到堆中巢钓,常量池不需要存儲(chǔ)一個(gè)對(duì)象了病苗,程序只是簡(jiǎn)單的把s這個(gè)對(duì)象的引用在常量池中存儲(chǔ)了,此時(shí)s和s1指向的是同一個(gè)對(duì)象症汹,結(jié)果當(dāng)然是true硫朦。
上面簡(jiǎn)單介紹了jdk7中常量池的變化,而在jdk8中方法整個(gè)方法區(qū)被放到了物理機(jī)的本地內(nèi)存,同時(shí)也更名為元空間(MetaSpace):
jdk8及其以后的版本背镇,元空間直接使用物理機(jī)的本地內(nèi)存咬展,在不加限制的情況下其最大值為本地內(nèi)存的最大可用值÷髡叮考慮到物理機(jī)上可能部署其它的應(yīng)用服務(wù)破婆,通常會(huì)給元空間加一個(gè)大小限制。
關(guān)于元空間最常見的虛擬機(jī)參數(shù)是:
- -XX:MetaspaceSize : 表示虛擬機(jī)元空間發(fā)生MetadataGC時(shí)的初始閾值,如 -XX:MetaspaceSize=10M 表示元空間在第一次到大10M時(shí)济瓢,會(huì)發(fā)生一次MetadataGC荠割。
- -XX:MaxMetaspaceSize : 表示虛擬機(jī)元空間的最大值為MaxMetaspaceSize妹卿,如 -XX:MaxMetaspaceSize=15M 表示元空間的最大值為15M旺矾,再大就會(huì)發(fā)生OOM異常。
關(guān)于元空間的的內(nèi)存溢出模擬夺克,我們需要借助CGLib來(lái)動(dòng)態(tài)的創(chuàng)建類箕宙,先引入如下maven依賴:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>3.3.0</version>
</dependency>
具體代碼如下:
/**
* 虛擬機(jī)參數(shù) -XX:MaxMetaspaceSize=10M
* @description 元空間內(nèi)存溢出
*/
public class MetaSpaceOOM {
public static void main(String[] args) {
BeanGenerator beanGenerator = new BeanGenerator();
List<Class> classes = new ArrayList<>();
for (int i=0; i<1000000000L;i++ ) {
beanGenerator.addProperty("id"+i, Integer.class);
Object aClass = beanGenerator.createClass();
classes.add((Class) aClass);
}
}
}
執(zhí)行結(jié)果為:
Exception in thread "main" java.lang.IllegalStateException: Unable to load cache item
at net.sf.cglib.core.internal.LoadingCache.createEntry(LoadingCache.java:79)
at net.sf.cglib.core.internal.LoadingCache.get(LoadingCache.java:34)
at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:119)
at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:294)
at net.sf.cglib.beans.BeanGenerator.createHelper(BeanGenerator.java:94)
at net.sf.cglib.beans.BeanGenerator.createClass(BeanGenerator.java:85)
at jvm.post1.MetaSpaceOOM.main(MetaSpaceOOM.java:19)
Caused by: java.lang.OutOfMemoryError: Metaspace
at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:348)
at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData$3.apply(AbstractClassGenerator.java:96)
at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData$3.apply(AbstractClassGenerator.java:94)
at net.sf.cglib.core.internal.LoadingCache$2.call(LoadingCache.java:54)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at net.sf.cglib.core.internal.LoadingCache.createEntry(LoadingCache.java:61)
... 6 more
可以看到,引起IllegalStateException異常的正是因?yàn)?Metaspace"類型的OOM錯(cuò)誤铺纽。具體原因?yàn)锽eanGenerator對(duì)象通過(guò)createClass方法不斷創(chuàng)建新的類柬帕,導(dǎo)致最大內(nèi)存為10MB的元空間沒辦法存儲(chǔ)類的信息而拋出異常。
虛擬機(jī)棧和本地方法棧
虛擬機(jī)棧和本地方法棧狡门,都是線程私有的陷寝,主要用來(lái)存儲(chǔ)在線程運(yùn)行過(guò)程中的局部變量、操作數(shù)棧其馏、方法出入口等信息凤跑,這些信息是以棧幀的形式存儲(chǔ)的,虛擬機(jī)棧和本地方法棧的區(qū)別就是一個(gè)存儲(chǔ)java方法運(yùn)行時(shí)的棧幀數(shù)據(jù)一個(gè)存儲(chǔ)本地方法(native 關(guān)鍵字修飾的方法)運(yùn)行時(shí)的棧幀數(shù)據(jù)叛复。由于都是存儲(chǔ)棧幀數(shù)據(jù)仔引,兩種棧的區(qū)別不是很大,甚至在HotSpot虛擬機(jī)中褐奥,直接把這兩個(gè)合二為一咖耘,所以本小節(jié)把這兩種棧合起來(lái)說(shuō)。java程序在運(yùn)行時(shí)的棧數(shù)據(jù)結(jié)構(gòu)如下圖:
在介紹堆時(shí)撬码,我們?cè)f(shuō)過(guò)幾乎所有的對(duì)象都是在堆中創(chuàng)建的儿倒,這幾乎中的特例就來(lái)自于棧眶掌,對(duì)象是可以在棧上創(chuàng)建距糖,我們稱為棧上分配乐埠。
/**
* 執(zhí)行棧上分配的虛擬機(jī)參數(shù) -XX:+DoEscapeAnalysis -XX:+EliminateAllocations -Xmx10M
* 不執(zhí)行棧上分配的虛擬機(jī)參數(shù) -XX:-DoEscapeAnalysis -XX:+EliminateAllocations -Xmx10M
*
* 參數(shù)說(shuō)明:
* DoEscapeAnalysis : 逃逸分析随静,對(duì)于本例來(lái)說(shuō)逃逸分析可以判斷出//1處創(chuàng)建的對(duì)象是否會(huì)被本方法外的方法獲取到。
* EliminateAllocations : 標(biāo)量替換慷吊,對(duì)于本例來(lái)說(shuō)袖裕,在逃逸分析的幫助下發(fā)現(xiàn)//1出的User對(duì)象不會(huì)逃逸出方法allo,那么消除User對(duì)象的堆內(nèi)存分配溉瓶,把它的字段改為一個(gè)個(gè)獨(dú)立的局部變量(本例中是int類型的標(biāo)量)存儲(chǔ)在線程的棧中急鳄。
* 要模擬棧上分配,需要逃逸分析和標(biāo)量替換兩個(gè)功能都是開啟的堰酿。
* @description 棧上分配
*/
public class StackAllocation {
static class User{
int i;
}
public static void allo() {
User user = new User(); //1
user.i = 4;
}
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
for (int i = 0; i < 100000000L; i++) {
allo();
}
long endTime = System.currentTimeMillis();
System.out.println(endTime - startTime);
}
}
用不同的虛擬機(jī)參數(shù)執(zhí)行上面的代碼時(shí)疾宏,會(huì)發(fā)現(xiàn)同樣執(zhí)行1億次方法調(diào)用,棧上分配的執(zhí)行時(shí)間明顯比非棧上分配的執(zhí)行時(shí)間短触创。簡(jiǎn)單的解釋就是1億個(gè)的User對(duì)象不是被分配在堆上坎藐,這樣就避免了頻繁的GC,對(duì)性能自然有很大提升哼绑。
與棧相關(guān)的虛擬機(jī)參數(shù)主要有:
- -Xss : 設(shè)置java線程棧的大小岩馍,如 -Xss100k 表示每個(gè)java線程棧的大小為100k。
線程棧是用來(lái)存方法的棧幀的抖韩。線程棧越大其能調(diào)用的方法深度越大蛀恩,運(yùn)行如下代碼可以印證此觀點(diǎn):
/**
* 虛擬機(jī)參數(shù) -Xss1000K
* @description 模擬棧內(nèi)存溢出
*/
public class StackOverFlowOOM {
private static int num = 0;
public static void loop(){
num++;
loop();
}
public static void main(String[] args) {
try {
loop();
} catch (Throwable e) {
e.printStackTrace();
System.out.println(num);
}
}
}
當(dāng)Xss的值越大時(shí),程序中的num變量在棧溢出異常時(shí)的值越大茂浮。jdk8中如果不指定Xss參數(shù)的大小双谆,那么其默認(rèn)值為1MB,這也從內(nèi)存角度印證線程是一種昂貴的資源席揽,即使簡(jiǎn)單的創(chuàng)建一個(gè)線程而不分配給其處理任務(wù)顽馋,其也要占用一些內(nèi)存空間。
程序計(jì)數(shù)器
程序計(jì)數(shù)器是一塊較小的內(nèi)存空間幌羞,它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器寸谜,因?yàn)椴僮飨到y(tǒng)會(huì)分配給各個(gè)線程一些時(shí)間片來(lái)運(yùn)行,當(dāng)時(shí)間片用完后新翎,就需要有程序計(jì)數(shù)器記錄線程執(zhí)行的位置程帕,用來(lái)在線程重新獲得時(shí)間片時(shí)能恢復(fù)到原來(lái)的執(zhí)行位置。從程序計(jì)數(shù)器的用途得知地啰,程序程序計(jì)數(shù)器也是線程私有的愁拭,而且也是唯一一個(gè)不會(huì)有OOM異常的虛擬機(jī)內(nèi)存區(qū)域。
篇尾小節(jié)
本篇主要簡(jiǎn)紹了java虛擬機(jī)在運(yùn)行時(shí)的各個(gè)內(nèi)存區(qū)域亏吝,簡(jiǎn)單介紹了它們的作用和內(nèi)存溢出的方式岭埠。
有任何不懂或者質(zhì)疑的地方,都?xì)g迎大家積極留言討論,留言必回惜论,一起學(xué)習(xí)進(jìn)步许赃。