前言
上一回我們了解了java的歷史背景和JVM的一些版本殿漠,這次我們要探索java的內(nèi)存區(qū)域和內(nèi)存溢出。
java和C++之間有一睹由內(nèi)存動(dòng)態(tài)分配和垃圾收集技術(shù)所圍成的高墻佩捞,墻外面的人想進(jìn)去,墻里面的人卻想出來(lái)蕾哟。java程序員將內(nèi)存控制的權(quán)力交給了java虛擬機(jī)一忱,一旦出現(xiàn)內(nèi)存泄露和溢出方面的問(wèn)題莲蜘,如果不了解虛擬機(jī)是怎樣使用內(nèi)存的,那么排查錯(cuò)誤將會(huì)成為一項(xiàng)異常艱難的工作帘营。
基本概念
- 程序計(jì)數(shù)器
線程隔離票渠,一塊較小的內(nèi)存空間,當(dāng)前程序所執(zhí)行的字節(jié)碼的行號(hào)指示器芬迄。唯一一個(gè)在java虛擬機(jī)規(guī)范中沒有指定任何OutOfMemoryError情況的區(qū)域 - java虛擬機(jī)棧
線程隔離问顷,生命周期和線程相同,每個(gè)方法執(zhí)行的同時(shí)都會(huì)產(chǎn)生一個(gè)棧幀用于存儲(chǔ)局部變量表禀梳、操作數(shù)棧杜窄、動(dòng)態(tài)鏈接、方法出口等信息算途。- 局部變量表存放編譯器可知的各種基本數(shù)據(jù)類型(boolean塞耕、byte、char嘴瓤、short扫外、int、float廓脆、long筛谚、double),對(duì)象的引用(reference類型停忿,不等同與對(duì)象本身刻获,有可能是指向起始地址的引用指針,也可能是一個(gè)代表對(duì)象的句柄活其他與對(duì)象相關(guān)的位置)和returnAddress類型(指向了一條字節(jié)碼指令的地址)
會(huì)出現(xiàn)的異常情況: StackOverflowError瞎嬉,線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度蝎毡;OutOfMemoryError,虛擬機(jī)椦踉妫可以動(dòng)態(tài)擴(kuò)展沐兵,擴(kuò)展無(wú)法申請(qǐng)到足夠內(nèi)存。
- 局部變量表存放編譯器可知的各種基本數(shù)據(jù)類型(boolean塞耕、byte、char嘴瓤、short扫外、int、float廓脆、long筛谚、double),對(duì)象的引用(reference類型停忿,不等同與對(duì)象本身刻获,有可能是指向起始地址的引用指針,也可能是一個(gè)代表對(duì)象的句柄活其他與對(duì)象相關(guān)的位置)和returnAddress類型(指向了一條字節(jié)碼指令的地址)
- 本地方法棧
線程隔離便监,虛擬機(jī)棧為虛擬機(jī)指向java方法服務(wù)扎谎,本地方法棧為虛擬機(jī)提供Native方法服務(wù)。Sun HotSpot虛擬機(jī)直接將本地方法棧和虛擬機(jī)棧合二為一烧董。 - java堆
線程內(nèi)存共享毁靶。虛擬機(jī)所管理內(nèi)存中最大的一塊,所有線程共享逊移。虛擬機(jī)規(guī)范中描述:所有對(duì)象實(shí)例以及數(shù)組都要在堆上分配预吆。JIT編譯器的發(fā)展后這個(gè)也不是絕對(duì)了GC的主要區(qū)域。收集器基本上采用分代收集算法-->java堆分為新生代和老年代胳泉。 - 方法區(qū)
線程內(nèi)存共享拐叉。存儲(chǔ)已被虛擬機(jī)加載的類信息岩遗、常量、靜態(tài)變量凤瘦、即時(shí)編譯器編譯的代碼等數(shù)據(jù)宿礁,java虛擬機(jī)任務(wù)是堆的一個(gè)邏輯部分,但是別名為非堆蔬芥。有人稱為永久代梆靖,其實(shí)并不等價(jià),只是HotSopt虛擬機(jī)將GC分代收集擴(kuò)展到了方法區(qū)笔诵,其他虛擬機(jī)并不存在永久代的概率返吻。目前JDK1.7的HotSpot中已經(jīng)將原本放在永久代的字符串常量池移出。JDK1.8剔除了永久代嗤放,取而代之的是元數(shù)據(jù)區(qū)(也稱為元空間)思喊。元數(shù)據(jù)區(qū)不屬于JVM內(nèi)存的一部分,它直接存儲(chǔ)于本機(jī)內(nèi)存次酌,而將常量池移到了堆中恨课。 - 運(yùn)行時(shí)常量池
線程內(nèi)存共享。方法區(qū)的一部分 - 直接內(nèi)存
并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分岳服,也不是java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域剂公。但這部分內(nèi)存也被頻繁使用。JDK1.4映入NIO吊宋,基于通道與緩沖區(qū)的I/O方式纲辽。利用Native函數(shù)庫(kù)直接分配堆外內(nèi)存,然后通過(guò)一個(gè)存儲(chǔ)在java堆中的DirectByteBuffer對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作璃搜。這樣能在一些場(chǎng)景中顯著提高性能拖吼,避免java堆和Natie堆中來(lái)回復(fù)制數(shù)據(jù)。
題目
以下結(jié)果在jdk1.6和jdk1.8中的結(jié)果
String str2 = new String("str")+new String("01");
str2.intern();
String str1 = "str01";
System.out.println(str2==str1);
jdk1.6:false
jdk1.8:true
分析原因:
intern()方法是當(dāng)調(diào)用 intern 方法時(shí)这吻,如果池已經(jīng)包含一個(gè)等于此 `String` 對(duì)象的字符串吊档,則返回池中的字符串。否則唾糯,將此 `String` 對(duì)象添加到池中怠硼,并返回此 `String` 對(duì)象的引用。
jdk1.6中常量池在永久代移怯,如果字符串在常量池中找不到會(huì)將字符串拷貝到常量池中,如果str2.intern();替換成str2=str2.intern();這個(gè)時(shí)候str2代表的是對(duì)象的引用香璃,則結(jié)果就是true了。
jdk1.8中常量池已經(jīng)移到了堆中舟误,如果字符串在常量池中找不到不再拷貝到常量池中葡秒,而會(huì)重新生成一個(gè)對(duì)原字符串的引用
補(bǔ)充概念
JVM內(nèi)存區(qū)域劃分Eden Space、Survivor Space、Tenured Gen同云,Perm Gen解釋
- Eden Space 伊甸園(新生的對(duì)象)
- Survivor Space 幸存者區(qū)
- Tenured Gen 老年代-養(yǎng)老區(qū)
- Perm Gen 永久代
HotSpot 虛擬機(jī)在java堆中對(duì)象分配糖权、布局和訪問(wèn)的全過(guò)程堵腹。
- 對(duì)象的分配
- new指令--檢查指令參數(shù)是否能在常量池中定位到類的符號(hào)引用,檢查這個(gè)符號(hào)代表的類是否已經(jīng)加載疚顷、解析旱易,初始化,沒有腿堤,執(zhí)行類加載過(guò)程
- 類加載檢查通過(guò)--為新生對(duì)象分配內(nèi)存--
- 堆內(nèi)存絕對(duì)規(guī)整阀坏,指針碰撞分配方式,用過(guò)的內(nèi)存放一邊笆檀,空閑內(nèi)存放一邊忌堂,中間放一個(gè)指針作為分界點(diǎn)指示器。
- 堆內(nèi)存不規(guī)整酗洒∈啃蓿空閑列表分配方式。
- 具體采用什么分配方式取決于java堆是否規(guī)整樱衷。java堆是否規(guī)準(zhǔn)由采取的垃圾收集器是否帶有壓縮整理功能決定棋嘲。
- Serial、ParNew等帶Compact過(guò)程的收集器矩桂,分配采用指針碰撞分配
- 使用CMS這種基于Mark-Sweep算法的收集器通常采用空閑列表沸移。
- 分配內(nèi)存并發(fā)情況,
- 動(dòng)作同步處理侄榴,虛擬機(jī)采用CAS配上失敗重試的方式保證更新操作的原子性雹锣。
- 內(nèi)存分配按照線程劃分在不同的空間進(jìn)行。即每個(gè)線程在java堆中預(yù)先分配一小塊內(nèi)存癞蚕,本地線程分配緩沖(TLAB)蕊爵,哪個(gè)線程需要分配內(nèi)存就在哪個(gè)線程的TLAB傷分配,只有TLAB用完并分配新的TLAB時(shí)涣达,才需要同步鎖定在辆。
- 內(nèi)存分配完后,虛擬機(jī)將分配的內(nèi)存空間初始化為零值度苔。使用TLAB匆篓,在TLAB分配時(shí)就直接進(jìn)行這步。保證對(duì)象實(shí)例字段在java代碼中不賦值就能直接使用寇窑。
- 設(shè)置對(duì)象鸦概。對(duì)象所屬哪個(gè)類的實(shí)例,如何查找類的元數(shù)據(jù)信息,對(duì)象的哈希碼窗市,對(duì)象的GC分代年齡等先慷,這些信息存放在對(duì)象頭中。
- 上面完成后咨察,虛擬機(jī)視角砍艾,一個(gè)新對(duì)象已經(jīng)產(chǎn)生了纬乍,java程序視角,創(chuàng)建才剛剛開始,init還沒執(zhí)行剑肯,所有字段還為零宁炫。
-
對(duì)象的訪問(wèn)
建立對(duì)象-->使用對(duì)象暴备。java程序通過(guò)棧上的reference數(shù)據(jù)來(lái)操作堆上的具體對(duì)象围小。java虛擬機(jī)規(guī)范規(guī)定reference類型指向一個(gè)對(duì)象。
句柄好處:reference中存儲(chǔ)的是句柄地址酣衷,在對(duì)象唄移動(dòng)時(shí)只會(huì)改變句柄中的實(shí)例指針交惯,而reference本身不修改
指針好處:速度更快,節(jié)省了一次指針定位的時(shí)間開銷穿仪。就Sun HotSpot而言席爽,它是使用第二種方式進(jìn)行對(duì)象訪問(wèn)的。但從整個(gè)軟件開發(fā)范圍來(lái)看牡借,句柄訪問(wèn)的情況也十分常見拳昌。
- 句柄。reference存儲(chǔ)句柄地址钠龙。句柄中包含對(duì)象實(shí)例數(shù)據(jù)與類型數(shù)據(jù)各自的具體地址
- 指針炬藤。java堆對(duì)象中必須考慮放置訪問(wèn)類型數(shù)據(jù)的相關(guān)信息。reference中存儲(chǔ)的是對(duì)象地址碴里。
實(shí)戰(zhàn)OutOfMemoryError
先設(shè)置VM args -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
- 堆內(nèi)存溢出程序
/**
* VM Args: -Xms20M -Xmx20M -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOP {
static class OOMObject{
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true){
list.add(new OOMObject());
}
}
}
異常信息
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:261)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
at java.util.ArrayList.add(ArrayList.java:458)
at com.zwq.heap.HeapOOP.main(HeapOOP.java:13)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
- 虛擬機(jī)棧和本地方法棧溢出
public class JavaVMStackSop {
private int stackLength = 1;
public void stackLeak(){
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable{
JavaVMStackSop oom = new JavaVMStackSop();
try {
oom.stackLeak();
}catch (Throwable e){
System.out.println("stack length:"+oom.stackLength);
throw e;
}
}
}
異常信息
Exception in thread "main" java.lang.StackOverflowError
stack length:11387
at com.zwq.heap.JavaVMStackSop.stackLeak(JavaVMStackSop.java:7)
at com.zwq.heap.JavaVMStackSop.stackLeak(JavaVMStackSop.java:8)
...
- 方法區(qū)和運(yùn)行時(shí)常量池溢出
/**
* 方法區(qū)和運(yùn)行時(shí)常量池溢出
* String.intern()是一個(gè)native方法沈矿,在字符串常量池有則直接返回,沒有則添加到常量池中
* JDK1.6及以前版本常量池在永久代內(nèi)咬腋,通過(guò)-XX:PermSize和-XX:MaxPermSize限制方法區(qū)大小羹膳,
*報(bào)錯(cuò)java.lang.outofmemoryerror:PermGen space
* 從而限制常量池容量
* 而JDK1.7后不會(huì)出現(xiàn)這個(gè)問(wèn)題,會(huì)一直執(zhí)行下去
*/
public class RunntimeConstantPoolOOM {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
int i=0;
while(true){
list.add(String.valueOf(i).intern());
}
}
}
/**
* 方法區(qū)內(nèi)存溢出
*/
public class JavaMethodAreaOp {
public static void main(String[] args) {
while(true){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o,args);
}
});
enhancer.create();
}
}
static class OOMObject{
}
}
需要配置cglib包引用
<!-- [https://mvnrepository.com/artifact/cglib/cglib](https://mvnrepository.com/artifact/cglib/cglib) -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.4</version>
</dependency>
附:由于jdk6.0及以下版本和JDK7.0以上版本存在差異根竿,以下代碼運(yùn)行結(jié)果不一致
public class RunntimeConstantPoolOOM {
public static void main(String[] args) {
String str1 = new StringBuffer().append("計(jì)算機(jī)").append("軟件").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuffer().append("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}
}
jdk1.6結(jié)果
false
false
jdk1.8結(jié)果
true
false
出現(xiàn)差異原因:String.intern()是一個(gè)Native方法陵像,作用:jdk1.6及以前版本,如果字符串常量池已經(jīng)包含一個(gè)等于此String對(duì)象的字符串,則返回代表池中這個(gè)字符串的String對(duì)象否則寇壳,將String對(duì)象包含的字符串添加到常量池中醒颖,并返回此String對(duì)象的引用,而new StringBuffer()對(duì)象存放在堆中壳炎,很明顯返回false泞歉。jdk1.7后intern()方法不會(huì)再?gòu)?fù)制實(shí)例,只是在常量池中記錄首次出現(xiàn)的實(shí)例引用,由于“java”之前就出現(xiàn)腰耙,不符合首次首先榛丢,所以返回false,“計(jì)算機(jī)軟件”首次出現(xiàn)挺庞,則返回true
- 本機(jī)直接內(nèi)存溢出
/**
* 本機(jī)直接內(nèi)存溢出
*/
public class DirectMemoryOOM {
private static final int _1MB = 1024*1024;
public static void main(String[] args) throws IllegalAccessException {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true){
unsafe.allocateMemory(_1MB);
}
}
}
異常信息
java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at com.zwq.heap.DirectMemoryOOM.main(DirectMemoryOOM.java:18)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
附:文中流程圖為原創(chuàng)晰赞,代碼來(lái)源周志明《java虛擬機(jī) Jvm高級(jí)特性和最佳實(shí)現(xiàn)》,代碼結(jié)果在idea上驗(yàn)證過(guò)挠阁。