虛擬機整體結(jié)構(gòu)
我們先來張圖看看虛擬機的整體結(jié)構(gòu)
我們可以從上圖看出敢辩,JVM大概可以分為以下幾部分內(nèi)容:類加載器、內(nèi)存空間逛万、執(zhí)行引擎泳猬、本地方法接口、本地方法庫宇植。接下來我們在本篇文章中著重分析內(nèi)存空間得封。
JVM內(nèi)存空間
JVM內(nèi)存空間大致分為程序計數(shù)器、堆指郁、虛擬機棧忙上、本地方法棧、方法區(qū)闲坎、直接內(nèi)存幾部分疫粥。
程序計數(shù)器
程序計數(shù)器是一塊較小的內(nèi)存空間,可以看作是當前線程所執(zhí)行的字節(jié)碼的行號指示器腰懂。分支手形、循環(huán)、跳轉(zhuǎn)悯恍、異常處理库糠、線程恢復等基礎(chǔ)功能都需要依賴這個計數(shù)器來完成
在任何一個確定的時刻,一個處理器(對于多核處理器來說是一個內(nèi)核)只會執(zhí)行一條線程中的指令涮毫。因此瞬欧,為了線程切換后能恢復到正確的執(zhí)行位置,每條線程都需要有一個獨立的程序計數(shù)器罢防,我們稱這類內(nèi)存區(qū)域為“線程私有”的內(nèi)存艘虎。
此內(nèi)存區(qū)域是唯一一個在Java 虛擬機規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域
虛擬機棧
線程私有,它的生命周期與線程相同咒吐。虛擬機棧描述的是Java 方法執(zhí)行的內(nèi)存模型:每個方法被執(zhí)行的時候都會同時創(chuàng)建一個棧幀野建、用于存儲局部變量表属划、操作棧、動態(tài)鏈接候生、方法出口等信息同眯。
每個在虛擬機中運行的程序也是由許多的幀的切換產(chǎn)生的結(jié)果,只是這些幀里面存放的是方法的局部變量唯鸭,操作數(shù)棧须蜗,動態(tài)鏈接,方法返回地址和一些額外的附加信息組成目溉。每一個方法被調(diào)用直至執(zhí)行完成的過程明肮,就對應(yīng)著一個棧幀在虛擬機棧中從入棧到出棧的過程。
我們來簡單看下局部變量表和動態(tài)鏈接:
局部變量表
一組變量值存儲空間缭付,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量
動態(tài)連接
虛擬機運行的時候,運行時常量池會保存大量的符號引用柿估,這些符號引用可以看成是每個方法的間接引用。如果代表棧幀A的方法想調(diào)用代表棧幀B的方法陷猫,那么這個虛擬機的方法調(diào)用指令就會以B方法的符號引用作為參數(shù)秫舌,但是因為符號引用并不是直接指向代表B方法的內(nèi)存位置,所以在調(diào)用之前還必須要將符號引用轉(zhuǎn)換為直接引用烙丛,然后通過直接引用才可以訪問到真正的方法。
如果符號引用是在類加載階段或者第一次使用的時候轉(zhuǎn)化為直接應(yīng)用羔味,那么這種轉(zhuǎn)換成為靜態(tài)解析河咽,如果是在運行期間轉(zhuǎn)換為直接引用,那么這種轉(zhuǎn)換就成為動態(tài)連接赋元。
本地方法棧
與虛擬機棧所發(fā)揮的作用是非常相似的忘蟹,其區(qū)別不過是虛擬機棧為虛擬機執(zhí)行Java 方法(也就是字節(jié)碼)服務(wù),而本地方法棧則是為虛擬機使用到的Native 方法服務(wù)搁凸。
與虛擬機棧一樣媚值,本地方法棧區(qū)域也會拋出StackOverflowError和OutOfMemoryError異常。
方法區(qū)
方法區(qū)在一個jvm實例的內(nèi)部护糖,類型信息被存儲在一個稱為方法區(qū)的內(nèi)存邏輯區(qū)中褥芒。類型信息是由類加載器在類加載時從類文件中提取出來的。類(靜態(tài))變量也存儲在方法區(qū)中嫡良。
存儲已被虛擬機加載的類信息锰扶、常量、靜態(tài)變量寝受、即時編譯器編譯后的代碼等數(shù)據(jù)坷牛,如字面量及符號引用,類的全限定名很澄,繼承的父類全限定名京闰,實現(xiàn)的接口全限定名颜及,訪問修飾符,方法訪問修飾符蹂楣、參數(shù)列表俏站、返回值類型 、方法名捐迫,字段訪問修飾符乾翔、字段名稱、字段類型施戴,除了常量以外的所有類(靜態(tài))變量反浓,還有一個指向ClassLoader的指針,一個指向Class對象的指針赞哗, 常量池(常量數(shù)據(jù)以及對其他類型的符號引用)等
直接內(nèi)存
JDk1.4中新加入了NIO類雷则,引入了一種基于通道與緩沖區(qū)的I/O方式,它可以使用Native函數(shù)直接分配堆外內(nèi)存肪笋,這樣能夠在一些場景中顯著提高性能月劈,因為避免了在Java堆和Native堆中來回復制數(shù)據(jù)。
堆
堆是Java 虛擬機所管理的內(nèi)存中最大的一塊藤乙。Java 堆是被所有線程共享的一塊內(nèi)存區(qū)域猜揪,在虛擬機啟動時創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對象實例坛梁,幾乎所有的對象實例都在這里分配內(nèi)存而姐。
堆分為Eden,Survivor to,Survivor from,Old等區(qū)域
內(nèi)存分配過程
1划咐、JVM 會試圖為相關(guān)Java對象在Eden Space中初始化一塊內(nèi)存區(qū)域拴念。
2、當Eden空間足夠時褐缠,內(nèi)存申請結(jié)束政鼠;否則到下一步。
3队魏、JVM 試圖釋放在Eden中所有不活躍的對象(這屬于1或更高級的垃圾回收)公般。釋放后若Eden空間仍然不足以放入新對象,則試圖將部分Eden中活躍對象放入Survivor區(qū)胡桨。
4俐载、Survivor區(qū)被用來作為Eden及Old的中間交換區(qū)域,當Old區(qū)空間足夠時登失,Survivor區(qū)的對象會被移到Old區(qū)遏佣,否則會被保留在Survivor區(qū)。
5揽浙、當Old區(qū)空間不夠時状婶,JVM 會在Old區(qū)進行完全的垃圾收集(0級)意敛。
6、完全垃圾收集后膛虫,若Survivor及Old區(qū)仍然無法存放從Eden復制過來的部分對象草姻,導致JVM無法在Eden區(qū)為新對象創(chuàng)建內(nèi)存區(qū)域,則出現(xiàn)“outofmemory”錯誤稍刀。
對象內(nèi)存布局
對象內(nèi)存中布局可以分為3塊:對象頭撩独,實例數(shù)據(jù)和對齊填充
對象頭
對象頭分為兩部分:
1.運行時數(shù)據(jù),如哈希碼账月、GC分代年齡综膀、鎖狀態(tài)標志、線程持有的鎖局齿、偏向線程ID剧劝、偏向時間戳等
2.類型指針,即對象指向它的類元數(shù)據(jù)指針抓歼,虛擬機通過這個指針來確定屬于哪個類的實例讥此。
對象訪問
對象訪問在Java 語言中無處不在,是最普通的程序行為谣妻,但即使是最簡單的訪問萄喳,也會卻涉及Java 棧、Java 堆蹋半、方法區(qū)這三個最重要內(nèi)存區(qū)域之間的關(guān)聯(lián)關(guān)系他巨,如下面的這句代碼:
Object obj = newObject();
假設(shè)這句代碼出現(xiàn)在方法體中,那“Object obj”這部分的語義將會反映到Java 棧的局部變量表中湃窍,作為一個reference 類型數(shù)據(jù)出現(xiàn)闻蛀。而“new Object()”這部分的語義將會反映到Java 堆中匪傍,形成一塊存儲了Object 類型所有實例數(shù)據(jù)值(Instance Data您市,對象中各個實例字段的數(shù)據(jù))的結(jié)構(gòu)化內(nèi)存,根據(jù)具體類型以及虛擬機實現(xiàn)的對象內(nèi)存布局(Object Memory Layout)的不同役衡,這塊內(nèi)存的長度是不固定的茵休。另外,在Java 堆中還必須包含能查找到此對象類型數(shù)據(jù)(如對象類型手蝎、父類榕莺、實現(xiàn)的接口、方法等)的地址信息棵介,這些類型數(shù)據(jù)則存儲在方法區(qū)中钉鸯。
由于reference 類型在Java 虛擬機規(guī)范里面只規(guī)定了一個指向?qū)ο蟮囊茫]有定義這個引用應(yīng)該通過哪種方式去定位邮辽,以及訪問到Java 堆中的對象的具體位置唠雕,因此不同虛擬機實現(xiàn)的對象訪問方式會有所不同贸营,主流的訪問方式有兩種:使用句柄和直接指針。
句柄訪問方式
Java 堆中將會劃分出一塊內(nèi)存來作為句柄池岩睁,reference中存儲的就是對象的句柄地址钞脂,而句柄中包含了對象實例數(shù)據(jù)和類型數(shù)據(jù)各自的具體地址信息。
直接指針訪問方式
reference變量中直接存儲的就是對象的地址捕儒,而java堆對象一部分存儲了對象實例數(shù)據(jù)冰啃,另外一部分存儲了對象類型數(shù)據(jù)。
4種情況必須立即對類進行初始化
1刘莹、遇到new(使用new關(guān)鍵字實例化對象)阎毅、getstatic(獲取一個類的靜態(tài)字段,final修飾符修飾的靜態(tài)字段除外)栋猖、putstatic(設(shè)置一個類的靜態(tài)字段净薛,final修飾符修飾的靜態(tài)字段除外)和invokestatic(調(diào)用一個類的靜態(tài)方法)這4條字節(jié)碼指令時,如果類還沒有初始化蒲拉,則必須首先對其初始化
2肃拜、使用java.lang.reflect包中的方法對類進行反射調(diào)用時,如果類還沒有初始化雌团,則必須首先對其初始化
3燃领、當初始化一個類時,如果其父類還沒有初始化锦援,則必須首先初始化其父類
4猛蔽、當虛擬機啟動時,需要指定一個主類(main方法所在的類)灵寺,虛擬機會首選初始化這個主類
除了上面這4種方式曼库,所有引用類的方式都不會觸發(fā)初始化,稱為被動引用略板。如:通過子類引用父類的靜態(tài)字段毁枯,不會導致子類初始化;通過數(shù)組定義來引用類叮称,不會觸發(fā)此類的初始化种玛;引用類的靜態(tài)常量不會觸發(fā)定義常量的類的初始化,因為常量在編譯階段已經(jīng)被放到常量池中了瓤檐。
常見異常及實例代碼
內(nèi)存溢出和內(nèi)存泄漏
內(nèi)存溢出 out of memory赂韵,是指程序在申請內(nèi)存時,沒有足夠的內(nèi)存空間供其使用挠蛉,出現(xiàn)out of memory祭示;比如申請了一個integer,但給它存了long才能存下的數(shù)谴古,那就是內(nèi)存溢出质涛。
內(nèi)存泄露 memory leak悄窃,是指程序在申請內(nèi)存后,無法釋放已申請的內(nèi)存空間蹂窖,一次內(nèi)存泄露危害可以忽略轧抗,但內(nèi)存泄露堆積后果很嚴重,無論多少內(nèi)存,遲早會被占光瞬测。
memory leak會最終會導致out ofmemory横媚。
Java 堆內(nèi)存的OutOfMemoryError異常是實際應(yīng)用中最常見的內(nèi)存溢出異常情況。出現(xiàn)Java 堆內(nèi)存溢出時月趟,異常堆棧信息“java.lang.OutOfMemoryError”會跟著進一步提示“Java heapspace”灯蝴。
要解決這個區(qū)域的異常,一般的手段是首先通過內(nèi)存映像分析工具(如Eclipse Memory Analyzer)對dump 出來的堆轉(zhuǎn)儲快照進行分析孝宗,重點是確認內(nèi)存中的對象是否是必要的穷躁,也就是要先分清楚到底是出現(xiàn)了內(nèi)存泄漏(Memory Leak)還是內(nèi)存溢出(Memory Overflow)。
如果是內(nèi)存泄漏因妇,可進一步通過工具查看泄漏對象到GC Roots 的引用鏈问潭。于是就能找到泄漏對象是通過怎樣的路徑與GC Roots 相關(guān)聯(lián)并導致垃圾收集器無法自動回收它們的。掌握了泄漏對象的類型信息婚被,以及GC Roots 引用鏈的信息狡忙,就可以比較準確地定位出泄漏代碼的位置。
如果不存在泄漏址芯,換句話說就是內(nèi)存中的對象確實都還必須存活著灾茁,那就應(yīng)當檢查虛擬機的堆參數(shù)(-Xmx 與-Xms),與機器物理內(nèi)存對比看是否還可以調(diào)大谷炸,從代碼上檢查是否存在某些對象生命周期過長北专、持有狀態(tài)時間過長的情況,嘗試減少程序運行期的內(nèi)存消耗旬陡。
棧溢出
導致各個內(nèi)存區(qū)域溢出異常實例代碼:
STACKOVERFLOW異常
package exceptiontest;
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length: " + oom.stackLength);
throw e;
}
}
}
運行結(jié)果:
stack length: 19792
Exception in thread “main” java.lang.StackOverflowError
at exceptiontest.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:9)
堆內(nèi)存溢出代碼
package exceptiontest;
import java.util.ArrayList;
import java.util.List;
public class HeapOOM {
static class OOMObject {}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
long i = 1;
while (true) {
list.add(new OOMObject());
}
}
}
方法區(qū)溢出
package exceptiontest;
import java.lang.reflect.Method;
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() {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
}
static class OOMObject {
}
}
常量池溢出
package exceptiontest;
import java.util.ArrayList;
import java.util.List;
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}
直接內(nèi)存溢出
package exceptiontest;
import java.util.ArrayList;
import java.util.List;
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}
/*public String intern()
返回字符串對象的規(guī)范化表示形式拓颓。
一個初始時為空的字符串池,它由類 String 私有地維護季惩。
當調(diào)用 intern 方法時录粱,如果池已經(jīng)包含一個等于此 String 對象的字符串(該對象由 equals(Object) 方法確定)腻格,則返回池中的字符串画拾。否則,將此 String 對象添加到池中菜职,并且返回此 String 對象的引用青抛。
它遵循對于任何兩個字符串 s 和 t,當且僅當 s.equals(t) 為 true 時酬核,s.intern() == t.intern() 才為 true蜜另。
所有字面值字符串和字符串賦值常量表達式都是內(nèi)部的适室。*/
jdk常用命令
對于一些常見的問題,我們可以使用jdk提供的一些工具來分析我們的jvm內(nèi)存狀況举瑰。從而跟蹤和分析問題科盛,如下是一些常用的命令娱俺。
jps 查看虛擬機進程
jstat 用于監(jiān)控虛擬機各種運行狀態(tài)信息的命令行工具
jinfo 實時查看和調(diào)整虛擬機各項參數(shù)
jmap 用于生成堆轉(zhuǎn)儲快照
jhat 虛擬機堆轉(zhuǎn)儲快照分析工具
jstack java堆棧跟蹤工具
具體用法大家可以根據(jù)需求去查,后續(xù)我會慢慢填充這一塊。
判斷對象是否可回收翩剪?
- 引用計數(shù)法
類似C++智能指針,但是當兩個對象互相引用的時候喳资,可能造成無法回收的情況檀头。在c++當中是根據(jù)實際業(yè)務(wù)情況用弱引用來強制回收分配內(nèi)存。 - 可達性分析算法
算法的基本思想就是通過一系列成為”GC Roots”的對象作為起始點坎怪,從這些節(jié)點開始向下搜索罢坝,搜索走過的路徑成為引用鏈,當一個對象到GC Roots沒有任何引用鏈的時候搅窿,則證明此對象是不可用的嘁酿。
在Java中,可作為引用鏈的GC Roots對象包括下面幾種- 虛擬機棧(本地變量表)中引用的對象男应。
- 方法區(qū)中靜態(tài)變量引用的對象痹仙。
- 方法區(qū)中常量引用的對象。
- 本地方法棧中JNI引用的對象殉了。
垃圾收集算法:
標記-清除法 優(yōu)缺點:速度慢开仰,碎片多
復制法 優(yōu)缺點:速度快,但有一半內(nèi)存無法利用 主要用于在新生代垃圾回收
標記-整理法 優(yōu)缺點:速度慢薪铜,內(nèi)存利用率高 主要用于老年代垃圾回收
關(guān)于垃圾回收更多內(nèi)容可參考這篇文章
引用概念:
強引用: 類似”O(jiān)bject obj = new Object()”众弓,只要引用還在,垃圾收集器就無法回收
軟引用:內(nèi)存溢出之前隔箍,才會對這些對象進行回收谓娃。
弱引用:實例化到下次GC就會被回收
虛引用:虛引用主要用來跟蹤對象被垃圾回收器回收的活動。虛引用與軟引用和弱引用的一個區(qū)別在于:虛引用必須和引用隊列 (ReferenceQueue)聯(lián)合使用蜒滩。當垃圾回收器準備回收一個對象時滨达,如果發(fā)現(xiàn)它還有虛引用,就會在回收對象的內(nèi)存之前俯艰,把這個虛引用加入到與之 關(guān)聯(lián)的引用隊列中捡遍。