參考資料《深入理解java虛擬機(jī)》
java內(nèi)存區(qū)域
運(yùn)行時數(shù)據(jù)區(qū)域
-
程序計數(shù)器 :可以看成是當(dāng)前線程所執(zhí)行字節(jié)碼的行號指示器。
- 每個線程都需要一個獨(dú)立的程序計數(shù)器,所以是私有的。(java虛擬機(jī)多線程是通過線程輪流切換并分配處理器執(zhí)行時間的方式)
- 如果線程執(zhí)行的是java方法,計數(shù)器記錄的是字節(jié)碼指令的地址;如果是native方法,計數(shù)器為空(uundifined),是唯一沒有outOfMemoryError的區(qū)域。
native是本地方法,和平臺有關(guān),需要借助c語言。
-
虛擬機(jī)棧 :線程私有的,生命周期和線程相同。
- 描述的是java方法執(zhí)行的內(nèi)存模型。每個方法在執(zhí)行時都會創(chuàng)建一個棧幀,用來記錄局部變量、動態(tài)鏈接,方法出口等。每個方法的執(zhí)行 從開始到介紹就是一個棧幀從虛擬機(jī)棧入棧到出棧的過程。
什么是棧幀呢?棧幀可以理解為一個方法的運(yùn)行空間。它主要由兩部分構(gòu)成,一部分是局部變量表,方法中定義的局部變量以及方法的參數(shù)就存放在這張表中;另一部分是操作數(shù)棧,用來存放操作數(shù)。
- 這個區(qū)域規(guī)定了兩種異常
- 如果線程請求的棧深度大于虛擬機(jī)允許的棧深度,則拋出 stackOverFlowError,比如遞歸調(diào)用
- 如果虛擬機(jī)??梢詣討B(tài)擴(kuò)展,但是擴(kuò)展時申請不到足夠的內(nèi)存,則拋出OutOfMemoryError,比如這個線程運(yùn)行時創(chuàng)建大量的類。
- 描述的是java方法執(zhí)行的內(nèi)存模型。每個方法在執(zhí)行時都會創(chuàng)建一個棧幀,用來記錄局部變量、動態(tài)鏈接,方法出口等。每個方法的執(zhí)行 從開始到介紹就是一個棧幀從虛擬機(jī)棧入棧到出棧的過程。
- 本地方法棧 和虛擬機(jī)棧類似。區(qū)別是虛擬機(jī)棧為執(zhí)行java方法服務(wù),本地方法棧為運(yùn)行native服務(wù)。
-
java堆 重點(diǎn)來了~ 下面重點(diǎn)分析
- 是java虛擬機(jī)中內(nèi)存區(qū)域最大的一塊
- 是被所有線程共享的區(qū)域,虛擬機(jī)啟動時創(chuàng)建。
- 幾乎所有的對象實例都會在這分配空間
- 可以是物理上不連續(xù)的區(qū)域,只要是邏輯上連續(xù)即可
- 如果堆中沒有內(nèi)存可以分配,并且不能擴(kuò)展的話,拋出 OutOfMemoryError異常
-
方法區(qū) non-heap (非堆)
- 是各個線程的共享區(qū)域
- 用于存放虛擬機(jī)加載的類信息,常量,靜態(tài)變量、即時編譯器編譯的代碼等。
- 并不能完全等同于永久代(permanent generation)
- 垃圾回收在這比較少出現(xiàn),回收目標(biāo)是常量池和對類型的卸載
- 運(yùn)行時常量池,是方法區(qū)的一部分。
- class文件除了記錄類的版本,字段、方法等,還有常量池,用于存放編譯期生成的字面量和符號引用,在類的加載后進(jìn)入該區(qū)域。
- 具有動態(tài)性,運(yùn)行期間也可以將新的常量放入池中。比如string類中的 intern()方法
string.intern() : 如果字符串常量池中已經(jīng)包含一個等于string對象的字符串,則返回池中這個字符串的對象,否則,將此字符串對象包含的字符串放入常量池中,并返回次string對象的引用。
- 受到方法區(qū)的限制,當(dāng)不能申請內(nèi)存時,拋出OutOfMemoryError異常
- 直接內(nèi)存:不屬于虛擬機(jī)內(nèi)存,但是有可能導(dǎo)致OutOfMemoryError異常
虛擬機(jī)對象
……
OutOfMemoryError異常分析
堆溢出
* VM Args: -Xms(堆的最小值)20m -Xmx(堆的最大值)20m:都設(shè)置成20m 防止堆內(nèi)存自動擴(kuò)展
* - XX:+HeapDumpOnOutOfMemoryError oom時生成dump文件
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<HeapOOM.OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
}
運(yùn)行結(jié)果:
Exception in thread "main" Heap dump file created [2314620069 bytes in 32.813 secs]
java.lang.OutOfMemoryError: Java heap space
- 內(nèi)存泄露
- 查看泄露對象到gc root的引用鏈(不太會找 哈哈)
- 內(nèi)存溢出
- 檢查堆參數(shù) xms xmx,是否還能調(diào)大。檢查代碼是否存在對象生命周期過長等情況。
虛擬機(jī)棧和本地方法棧溢出
單線程
==Xss:設(shè)置每個線程的堆棧大小==
/**
* hotspot虛擬機(jī)不區(qū)分虛擬機(jī)棧和本地方法棧 所以只設(shè)置xss
* VM Args:-Xss128k
*/
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;
}
}
}
運(yùn)行結(jié)果:
stack length: 978
Exception in thread "main" java.lang.StackOverflowError
at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:10)
……
- 如果線程請求的棧深度大于虛擬機(jī)允許的棧深度,則拋出 stackOverFlowError
- 虛擬機(jī)棧擴(kuò)展棧時申請不到足夠的內(nèi)存,則拋出OutOfMemoryError
當(dāng)??臻g無法分配時,是已使用的??臻g太大,還是內(nèi)存空間太???不管是調(diào)用xss減少棧內(nèi)存容量,還是增大本方法中本地變量表的長度,當(dāng)內(nèi)存無法分配時,都拋出stackOverFlowError異常。
多線程
- 多線程下的內(nèi)存溢出,與??臻g是否大不存在任何聯(lián)系
- 同等物理內(nèi)存下,為棧每個??臻g分配的內(nèi)存越大,可以創(chuàng)建的線程就越少。
- 如果不能減少線程數(shù),就只能減少最大堆(增加虛擬機(jī)棧的內(nèi)存?)和減少棧容量來獲得更多線程。
public class JavaVMStackOOM {
private void dontStop() {
while(true) {
}
}
// 多線程方式造成棧內(nèi)存溢出 OutOfMemoryError
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();
}
}
運(yùn)行結(jié)果:
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
方法區(qū)和運(yùn)行時常量池溢出
方法區(qū)用于存放class的信息,如類名、修飾符、常量池、字段描述等。對于該區(qū)域的測試,==思路就是運(yùn)行時產(chǎn)生大量的類去填滿方法區(qū),直到溢出。==
- CGLib動態(tài)生成類導(dǎo)致的方法區(qū)溢出
/**
* -XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class JavaMethodAreaOOM {
public static void main(String[] args) {
while(true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method m, Object[] objs, MethodProxy proxy) throws Throwable {
// TODO Auto-generated method stub
return proxy.invokeSuper(obj, objs);
}
});
enhancer.create();
}
}
}
運(yùn)行結(jié)果:
Caused by: java.lang.OutOfMemoryError: PermGen space
at java.lang.ClassLoader.defineClass1(Native Method)
......
對程序的講解參考 CGLIB enhancer講解
在經(jīng)常動態(tài)生成大量class的應(yīng)用中,要特別注意類的回收情況,
-
CGLib:Code Generation Library
- CGLIB是一個強(qiáng)大的、高性能的代碼生成庫。其被廣泛應(yīng)用于AOP框架(Spring、dynaop)中,用以提供方法攔截操作
- 原理:動態(tài)生成一個要代理類的子類,之類要實現(xiàn)代理類的所有方法(除了final修飾的)。在之類中利用方法攔截技術(shù)攔截所有代理類的方法的調(diào)用,順勢織入橫切邏輯。
-
CGLIB和Java動態(tài)代理的區(qū)別
- Java動態(tài)代理只能夠?qū)涌谶M(jìn)行代理,不能對普通的類進(jìn)行代理(因為所有生成的代理類的父類為Proxy,Java類繼承機(jī)制不允許多重繼承);CGLIB能夠代理普通類;
- Java動態(tài)代理使用Java原生的反射API進(jìn)行操作,在生成類上比較高效;CGLIB使用ASM框架直接對字節(jié)碼進(jìn)行操作,在類的執(zhí)行過程中比較高效
本地內(nèi)存直接溢出(感覺不常用,略,有興趣可以參考《深入理解java虛擬機(jī) 2.4.4小節(jié)》)
垃圾收集器和內(nèi)存分配
對象還存活嗎?
- 判斷對象是否存活,并不是給對象添加一個引用計數(shù)器。盡管有時候效率還是很高,java虛擬機(jī)并沒有采用,因為沒辦法解決對象之間相互循環(huán)引用的問題。
- java中是采用可達(dá)性分析算法來判斷對象是否存活的。
-
算法思想:通過GC root的對象作為起點(diǎn),從這個節(jié)點(diǎn)向下搜索,搜索經(jīng)過的路徑叫做引用鏈,當(dāng)一個對象到GC root沒有任何引用鏈項鏈,就認(rèn)為對象是不可用的。
image - java中GC root對象包括以下幾種
- 虛擬機(jī)棧中引用的對象
- 方法區(qū)中類靜態(tài)屬性引用的對象
- 方法區(qū)中常量引用的對象
- 本地方法棧中native引用的對象
-
引用
- 強(qiáng)引用
- 程序代碼中普遍存在的,例如Object obj=new Object(),只要強(qiáng)引用存在,永遠(yuǎn)不會被回收
- 軟引用
- 有用但并非必需的 。在內(nèi)存溢出之前,會對這些對象列進(jìn)回收范圍進(jìn)行二次回收。如果回收后還沒有內(nèi)存,就會拋出內(nèi)存溢出異常。
- 弱引用
- 當(dāng)垃圾收集工作時,不管內(nèi)存是否足夠,都會被回收。
- 虛引用
- 為一個對象設(shè)置虛引用的唯一目的,就是在回收時會得到一個通知。
生存還是死亡?
- 如果對象在進(jìn)行可達(dá)性分析之后發(fā)現(xiàn)沒有GC root相連,那他將會進(jìn)行一次標(biāo)記,并進(jìn)行一次篩選,篩選的條件是有沒有必要執(zhí)行finalize()方法。
- 當(dāng)對象沒有覆蓋finalize()方法,或者虛擬機(jī)已經(jīng)執(zhí)行過finalize(),則判定為不執(zhí)行。
- 如果判定為執(zhí)行,會將對象放入一個F-Queue中,稍后去執(zhí)行。執(zhí)行時會進(jìn)行二次標(biāo)記,這個時候如果與引用鏈建立關(guān)聯(lián),就可以拯救自己了~~~~~~
回收方法區(qū)
- 回收效率低,而且回收條件非??量?。
- 主要回收廢棄常量和無用的類。
垃圾收集算法
標(biāo)記-清除(Mark-Sweep)
- 首先標(biāo)記出需要回收的對象,標(biāo)記完成后統(tǒng)一回收
- 缺點(diǎn):
- 效率不高:標(biāo)記和清除效率都不高
-
空間問題:會導(dǎo)致大量的內(nèi)存碎片 程序需要分配較大對象時,無法找到連續(xù)的內(nèi)存,不得不提前再進(jìn)行內(nèi)存回收。
標(biāo)記-清除
復(fù)制
- 將內(nèi)存氛圍兩份,每次只使用一份,當(dāng)這一塊用完了,就將還存活的對象復(fù)制到另一塊上,然后再把這一塊內(nèi)存清空。
- 好處
- 不會產(chǎn)生內(nèi)存碎片,只需移動堆頂指針,順序分配,操作簡單,運(yùn)行高效。
- 缺點(diǎn)
-
將內(nèi)存縮小一半,代價太大。
image
-
- 應(yīng)用中 并不是按照1:1的比例劃分內(nèi)存。而是把eden和survivor按照8:1分配?;厥諘r,將eden和from sur中存活的對象一次性的放到to sur中。當(dāng)survivor內(nèi)存不夠時,需要old gen進(jìn)行分配擔(dān)保。這些對象將直接進(jìn)入老年代區(qū)域。(==詳細(xì)的參考下面的內(nèi)存分配與回收==)
小問題,為啥要有兩個survivor?
當(dāng)你把eden和from sur復(fù)制到to sur中后,清除eden和from ?,F(xiàn)在只有to中有數(shù)據(jù)了。
to中有數(shù)據(jù) 怎么做下一次的minor gc呢? 所以 要把to中的數(shù)據(jù) 再復(fù)制到from中?。。。?/p>
標(biāo)記-整理
- 復(fù)制算法在對象存活率較高的情況的下,需要進(jìn)行大量的復(fù)制,效率不高。而且還需要額外的空間進(jìn)行擔(dān)保,所以老年代不用這種算法。
-
算法和 標(biāo)記-清除差不多,只不過是在標(biāo)記之后不是對回收對象進(jìn)行清除,而是讓存活對象向一端移動,然后直接清理掉端邊界以外的內(nèi)存。
image
分代收集
- 根據(jù)對象存活周期的不同講內(nèi)存分為幾塊。
- 一般java堆分為新生代和老生代
- 新生代大批對象死去,少量存活,就采用 復(fù)制算法。
- 老生代存活率高 就用標(biāo)記-清除 或者標(biāo)記-整理。
內(nèi)存分配與回收
- 對象優(yōu)先再Eden區(qū)分配
- 當(dāng)eden沒有足夠的內(nèi)存分配時,虛擬機(jī)講發(fā)起一次Minor GC
- 大對象直接進(jìn)入老年代
- 最典型的大對象就是那種很長的字符串或數(shù)組。
- 虛擬機(jī)提供參數(shù) -Xx:PretenureSizeThreshold 大于這個設(shè)置值的直接進(jìn)入老年代
- 目的:是為了避免eden和survivor之間發(fā)生大量的內(nèi)存復(fù)制
- 長期存活的對象直接進(jìn)入老年代
- 虛擬機(jī)給每個對象定義了一個對象年齡計算器
- 如果這個對象在eden出生,并經(jīng)過一次minor gc扔存活,并且能夠被survivor容納,年齡+1
- 對象在survivor中熬過一次minor gc,年齡+1
- 當(dāng)?shù)竭_(dá)默認(rèn)值(15),就晉升老年代??梢酝ㄟ^-Xx:MaxTenuringThreshold設(shè)置。
- 動態(tài)對象年齡判定
- 如果在survivor中相同年齡所有對象大小的總和大于survivor內(nèi)存的一半,年齡大于或等于該年齡的對象直接晉升老年代,不用非要限制上面那個參數(shù)設(shè)定的年齡。
- 空間分配擔(dān)保
- 只要老年代的連續(xù)內(nèi)存空間>新生代對象總大小 或者 歷次晉升的平均大小就會進(jìn)行minor gc,否則進(jìn)行full gc。