Java的JVM可以自動(dòng)管理內(nèi)存,包括內(nèi)存動(dòng)態(tài)分配和垃圾收集等。
簡(jiǎn)介
JVM在執(zhí)行Java程序的過程中會(huì)把它所管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域。這些區(qū)域都有各自的用途,以及創(chuàng)建和銷毀的時(shí)間,有的區(qū)域隨著JVM進(jìn)程的啟動(dòng)而存在,有些區(qū)域則依賴用戶線程的啟動(dòng)和結(jié)束而建立和銷毀。
先看看JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)包括哪幾個(gè)部分:
可以看出JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)包括:堆、虛擬機(jī)棧、本地方法棧、方法區(qū)、程序計(jì)數(shù)器和運(yùn)行時(shí)常量池。其中,運(yùn)行時(shí)常量池在方法區(qū)里。
接下來對(duì)這幾個(gè)區(qū)域一一介紹。
Java堆
Java堆(Java Heap)是JVM所管理的內(nèi)存中最大的一塊。
堆的作用是用來存放對(duì)象實(shí)例,所有的對(duì)象實(shí)例和數(shù)組都在堆上分配內(nèi)存。
堆也是垃圾收集器管理的主要區(qū)域,因此也被稱為“GC堆”。因?yàn)槔占髦饕脕硎占瘜?duì)象,對(duì)象在堆上分配,所以自然堆是垃圾收集器管理的主要區(qū)域。
堆被所有的線程共享,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建,物理上可在不連續(xù)的內(nèi)存空間中,跟磁盤空間一樣。
Java堆溢出
什么情況下會(huì)堆溢出
當(dāng)創(chuàng)建新對(duì)象,堆上內(nèi)存不夠時(shí)就會(huì)產(chǎn)生堆溢出。
只要不斷創(chuàng)建對(duì)象,并且保證GC Roots到對(duì)象之間有可達(dá)路徑來避免垃圾回收機(jī)制清除這些對(duì)象,那么當(dāng)堆上的內(nèi)存不夠創(chuàng)建新對(duì)象時(shí)就會(huì)產(chǎn)生內(nèi)存溢出異常。
制造堆溢出
/**
* -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM {
static class OOMObject{
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
long length=0;
while (true){
try {
list.add(new OOMObject());
length += 1;
} catch (Throwable e){
System.out.println("number of obj: "+length);
throw e;
}
}
}
}
輸出:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid39193.hprof ...
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
Heap dump file created [27917334 bytes in 0.227 secs]
at java.util.Arrays.copyOf(Arrays.java:3210)
number of obj: 810325
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.lbd.jvm.HeapOOM.main(HeapOOM.java:20)
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:497)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
Process finished with exit code 1
解決堆溢出
重點(diǎn)確認(rèn)內(nèi)存中的對(duì)象是否是必要的,也就是確認(rèn)到底是出現(xiàn)了內(nèi)存泄露(Memory Leak)還是內(nèi)存溢出(Memory Overflow)。
方法:通過內(nèi)存映像分析工具(如Eclipse Memory Analyzer)對(duì)Dump出來的堆轉(zhuǎn)儲(chǔ)快照進(jìn)行分析。
如果是內(nèi)存泄露:
進(jìn)一步通過工具查看泄露對(duì)象到GC Roots的引用鏈,這樣就能找到泄露對(duì)象是通過怎樣的路徑與GC Roots相關(guān)聯(lián)并導(dǎo)致垃圾收集器無法自動(dòng)回收它們的,這樣就可以比較準(zhǔn)確地定位出泄露代碼的位置
如果不是內(nèi)存泄露:
也就是內(nèi)存中的對(duì)象確實(shí)還必須存活這,那就應(yīng)該檢查虛擬機(jī)的堆參數(shù)(-Xms和-Xmx),看看是否可以調(diào)大一些。
方法區(qū)
方法區(qū)與Java堆一樣,是各個(gè)線程共享的內(nèi)存區(qū)域,用于存儲(chǔ)已被虛擬機(jī)加載的類信息(類的版本、字段、方法、接口等描述信息)、常量、靜態(tài)變量、即使編譯器編譯后的代碼等數(shù)據(jù)。
方法區(qū)的內(nèi)存回收主要針對(duì)常量池的回收和堆類型的卸載。垃圾收集行為在該區(qū)域比較少出現(xiàn)。
在HotSpot虛擬機(jī)中,方法區(qū)又被稱為“永久代”(Permanent Generation),這樣HotSpot的垃圾收集器可以像管理Java堆一樣管理這部分內(nèi)存,就不用專門為方法區(qū)編寫內(nèi)存管理代碼了。
方法區(qū)溢出
如果產(chǎn)生大量的類或者大量的字符串常量(運(yùn)行時(shí)常量池溢出)可能導(dǎo)致方法區(qū)溢出。
Java SE API可以動(dòng)態(tài)產(chǎn)生類,如反射時(shí)的GeneratedConstructorAccessor和動(dòng)態(tài)代理等。
運(yùn)行時(shí)常量池
運(yùn)行時(shí)常量池是方法區(qū)的一部分,用于存放編譯期生成的各種字面量和符號(hào)引用,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放。
運(yùn)行時(shí)常量池溢出
什么時(shí)候會(huì)運(yùn)行時(shí)常量池溢出
產(chǎn)生大量的字符串常量。
制造運(yùn)行時(shí)常量池溢出
注:以下代碼只在JDK1.6及之前的版本才會(huì)產(chǎn)生運(yùn)行時(shí)常量池溢出異常,因?yàn)樵谶@些版本中常量池分配在永久代內(nèi),可以通過-XX:PermSize=1M -XX:MaxPermSize=1M來限制方法區(qū)的大小,從而間接限制其中常量池的容量。
/**
* -XX:PermSize=1M -XX:MaxPermSize=1M
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
int i = 0;
while (true){
list.add(String.valueOf(i++).intern());
}
}
}
虛擬機(jī)棧
虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個(gè)方法在執(zhí)行的時(shí)候都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。每一個(gè)方法從調(diào)用直至執(zhí)行完成的過程,就對(duì)應(yīng)者一個(gè)棧幀在虛擬機(jī)棧中入棧到出棧的過程。
虛擬機(jī)棧是線程私有的。為Java方法服務(wù)。
虛擬機(jī)棧中最重要的是局部變量表。局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型和對(duì)象引用類型。是在編譯期確定的。
虛擬機(jī)棧溢出
什么情況下會(huì)Java虛擬機(jī)棧溢出
- 如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的最大深度(一般是遞歸),將拋出StackOverflowError異常。
- 如果虛擬機(jī)在擴(kuò)展棧時(shí)無法申請(qǐng)到足夠的內(nèi)存空間,則拋出OutOfMemoryError異常。
制造虛擬機(jī)棧溢出(StackOverflowError)
遞歸。因?yàn)檫f歸需要用到棧。設(shè)置棧容量為256k(-Xss256k)。
/**
* -Xss256k
*/
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak(){
stackLength++;
stackLeak();
}
public static void main(String[] args) {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e){
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}
輸出:
Exception in thread "main" java.lang.StackOverflowError
stack length:2789
at com.lbd.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:10)
at com.lbd.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:10)
at com.lbd.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:10)
at com.lbd.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:10)
at com.lbd.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:10)
當(dāng)調(diào)用了2789次后出現(xiàn)了棧溢出StackOverflowError。
制造虛擬機(jī)棧溢出(OutOfMemoryError)
創(chuàng)建足夠多的線程,當(dāng)擴(kuò)展棧時(shí)無法申請(qǐng)到足夠的內(nèi)存空間,就會(huì)拋出OutOfMemoryError異常。
/**
* -Xss2M
* dangerous,don't run this program!
*/
public class JavaVMStackOOM {
private void dontStop() {
while (true);
}
public void stackLeakByThread () {
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args) {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}
本地方法棧
本地方法棧與虛擬機(jī)棧所發(fā)揮的作用很相似。區(qū)別在于:虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧為虛擬機(jī)使用到的Native方法服務(wù)。
程序計(jì)數(shù)器
程序計(jì)數(shù)器用來記錄正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址。程序計(jì)數(shù)器是線程私有的。是唯一一個(gè)沒有規(guī)定任何OutOfMemoryError情況的區(qū)域。
因?yàn)镴ava虛擬機(jī)的多線程是通過線程輪流切換并分配CPU執(zhí)行時(shí)間的方式來實(shí)現(xiàn)的。為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每個(gè)線程需要有一個(gè)獨(dú)立的程序計(jì)數(shù)器,各線程之間計(jì)數(shù)器互不影響,獨(dú)立存儲(chǔ)。
直接內(nèi)存
直接內(nèi)存并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域。
JDK1.4中新加入了NIO(New Input/Ouput)類,引入了一種基于通道(Channel)和緩沖區(qū)(Buffer)的I/O方式,它可以使用Native函數(shù)庫直接分配堆外內(nèi)存,然后通過一個(gè)存儲(chǔ)在Java堆中的DirectByteBufer對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。