Java的JVM可以自動管理內(nèi)存构挤,包括內(nèi)存動態(tài)分配和垃圾收集等髓介。
簡介
JVM在執(zhí)行Java程序的過程中會把它所管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域。這些區(qū)域都有各自的用途筋现,以及創(chuàng)建和銷毀的時(shí)間唐础,有的區(qū)域隨著JVM進(jìn)程的啟動而存在,有些區(qū)域則依賴用戶線程的啟動和結(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ū)里瞒津。
接下來對這幾個(gè)區(qū)域一一介紹蝉衣。
Java堆
Java堆(Java Heap)是JVM所管理的內(nèi)存中最大的一塊。
堆的作用是用來存放對象實(shí)例巷蚪,所有的對象實(shí)例和數(shù)組都在堆上分配內(nèi)存病毡。
堆也是垃圾收集器管理的主要區(qū)域,因此也被稱為“GC堆”屁柏。因?yàn)槔占髦饕脕硎占瘜ο罄材ぃ瑢ο笤诙焉戏峙洌宰匀欢咽抢占鞴芾淼闹饕獏^(qū)域淌喻。
堆被所有的線程共享僧家,在虛擬機(jī)啟動時(shí)創(chuàng)建,物理上可在不連續(xù)的內(nèi)存空間中裸删,跟磁盤空間一樣八拱。
Java堆溢出
什么情況下會堆溢出
當(dāng)創(chuàng)建新對象,堆上內(nèi)存不夠時(shí)就會產(chǎn)生堆溢出烁落。
只要不斷創(chuàng)建對象乘粒,并且保證GC Roots到對象之間有可達(dá)路徑來避免垃圾回收機(jī)制清除這些對象,那么當(dāng)堆上的內(nèi)存不夠創(chuàng)建新對象時(shí)就會產(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)存中的對象是否是必要的灯萍,也就是確認(rèn)到底是出現(xiàn)了內(nèi)存泄露(Memory Leak)還是內(nèi)存溢出(Memory Overflow)。
方法:通過內(nèi)存映像分析工具(如Eclipse Memory Analyzer)對Dump出來的堆轉(zhuǎn)儲快照進(jìn)行分析每聪。
如果是內(nèi)存泄露:
進(jìn)一步通過工具查看泄露對象到GC Roots的引用鏈旦棉,這樣就能找到泄露對象是通過怎樣的路徑與GC Roots相關(guān)聯(lián)并導(dǎo)致垃圾收集器無法自動回收它們的,這樣就可以比較準(zhǔn)確地定位出泄露代碼的位置
如果不是內(nèi)存泄露:
也就是內(nèi)存中的對象確實(shí)還必須存活這药薯,那就應(yīng)該檢查虛擬機(jī)的堆參數(shù)(-Xms和-Xmx)绑洛,看看是否可以調(diào)大一些。
方法區(qū)
方法區(qū)與Java堆一樣童本,是各個(gè)線程共享的內(nèi)存區(qū)域真屯,用于存儲已被虛擬機(jī)加載的類信息(類的版本、字段穷娱、方法绑蔫、接口等描述信息)、常量泵额、靜態(tài)變量配深、即使編譯器編譯后的代碼等數(shù)據(jù)。
方法區(qū)的內(nèi)存回收主要針對常量池的回收和堆類型的卸載嫁盲。垃圾收集行為在該區(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可以動態(tài)產(chǎn)生類嗦董,如反射時(shí)的GeneratedConstructorAccessor和動態(tài)代理等母谎。
運(yùn)行時(shí)常量池
運(yùn)行時(shí)常量池是方法區(qū)的一部分,用于存放編譯期生成的各種字面量和符號引用京革,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放。
運(yùn)行時(shí)常量池溢出
什么時(shí)候會運(yùn)行時(shí)常量池溢出
產(chǎn)生大量的字符串常量幸斥。
制造運(yùn)行時(shí)常量池溢出
注:以下代碼只在JDK1.6及之前的版本才會產(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í)候都會創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲局部變量表、操作數(shù)棧经窖、動態(tài)鏈接坡垫、方法出口等信息。每一個(gè)方法從調(diào)用直至執(zhí)行完成的過程画侣,就對應(yīng)者一個(gè)棧幀在虛擬機(jī)棧中入棧到出棧的過程冰悠。
虛擬機(jī)棧是線程私有的。為Java方法服務(wù)配乱。
虛擬機(jī)棧中最重要的是局部變量表溉卓。局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型和對象引用類型。是在編譯期確定的搬泥。
虛擬機(jī)棧溢出
什么情況下會Java虛擬機(jī)棧溢出
- 如果線程請求的棧深度大于虛擬機(jī)所允許的最大深度(一般是遞歸)桑寨,將拋出StackOverflowError異常。
- 如果虛擬機(jī)在擴(kuò)展棧時(shí)無法申請到足夠的內(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í)無法申請到足夠的內(nèi)存空間芭碍,就會拋出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ú)立存儲烦磁。
直接內(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è)存儲在Java堆中的DirectByteBufer對象作為這塊內(nèi)存的引用進(jìn)行操作陨晶。