這一陣子閱讀了《深入理解Java虛擬機》第二章,寫的真的很好,所以決定記錄下來,以後沒事翻翻博客看看。?
Java虛擬機在運行java代碼時,將內存區域分爲
1.程序計數器
該區域是線程私有,字節碼解釋器工作時,就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,例如:循環,跳轉,異常處理,線程恢復等基礎功能。該內存區域是唯一一個在jvm裏面沒有規定任何OOM情況的區域
2.java虛擬機棧
該區域線程私有,生命週期與線程相同,java每個方法執行的時候都會創建一個棧楨用於存放局部變量表、操作棧楨、動態鏈接、方法出口等信息。每個方法從調用到執行完成都對應一個棧楨從虛擬機棧中入棧到出棧過程。其中局部變量表存放了編譯期可知的基本數據類型(boolean,byte,char,short,int,float,long,double)、對象引用類型。拋出異常爲StackOverFlow和OOM。
3.本地方法棧
該區域線程私有,與虛擬機棧發揮的作用十分相似。區別爲虛擬機棧爲虛擬機執行java方法服務,而本地方法棧則爲虛擬機使用的Native方法服務。本地方法棧也會拋出StackOverFlow和OOM。
4.java堆
該區域線程共享,java堆是java虛擬機所管理的內存中最大的一塊,虛擬機啓動時創建,用於存放對象實例,所有對象實例以及數組都要在堆上分配。且是垃圾收集器管理的主要區域。java堆還可以細分爲新生代和老年代,用於進行不同的垃圾收集算法GC。當前的主流虛擬機可以擴展,通過-Xmx和-Xms控制。如果堆沒法再擴展內存時,將會拋出OOM異常。
5.方法區
該區域線程共享,用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。java虛擬機規範把方法去描述爲堆的一個邏輯部分。拋出異常類型爲OOM。
6.運行時的常量池
這是方法區的一部分。用於存放編譯器生成的各種字面量和符號引用,這部分內容將在類加載後進入方法區的運行時常量池中存放。運行時常量池相對於Class文件常量池的另外一個重要特性是具備動態性,Java語言並不要求常量一定只有編譯器才能產生,也就是並非預置入Class文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,比較常見的就是String類的intern()方法。拋出異常類型爲OOM。
下面這張圖是對象的訪問定位
通過這張圖我們就能清楚的知道,發生的內存溢出,是哪一塊兒出來的,而我也會貼上書上寫的代碼,並分析這種溢出情況。
1.java堆溢出代碼
看了上面的內容,我們應該知道了,java堆是存放對象的地方(怪不得我單身這麼久,原來對象要在java堆裏面找啊),所以java堆想要溢出,只要不斷創建對象,並保證GC Roots根到對象之間可達-GC算法。當對象數量達到最大堆的容量時就會產生內存溢出異常。
/**
*VM Args: -Xms 20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while(true) {
list.add(new OOMObject); //list添加聲明的對象,保證GC Roots根可達
}
}
}
基本參數設置,這個程序執行後會發生OOM,並在後面提示是 Java heap space,道出是java堆的溢出。
2.虛擬機棧和本地方法棧溢出
一種是單線程下不斷遞歸
我們應該都知道,不斷遞歸的話是容易爆棧的,因爲每次遞歸,都需要入棧來存放中間變量和部分參數等一些信息。
/**
* VM Args: -Xss128k
*/
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;
}
}
}
內心os:這種畫圖方式彷彿你畫我猜,2333。 就類似這樣,因爲不斷地遞歸,不斷的入棧,卻沒有出棧的操作,彷彿貔貅一樣,只吃不吐,所以最終就會發生StackOverflow的異常。
另外一種是不斷創建線程。
我們知道虛擬機棧和本地方法棧是線程私有的,所以每當我們一個線程,就會開闢一個虛擬機棧。如圖1.1
這種情況下最終會發生內存溢出異常,原因爲:操作系統給每個進程分配的內存有限,而線程是在進程下的,所以當爲每個線程的棧分配的內存越大時,進程下擁有的線程數量就越小,當然也就越容易產生內存溢出異常。而如果不能減少線程數或者更換64位虛擬機的情況下,就只能通過減少最大堆和減少棧容量來換取更多的線程了。需要注意的是,運行上面代碼的時候要保存當前工作,因爲在windows平臺上的JVM,Java線程是映射到操作系統線程上的,可能會導致操作系統假死。運行結果:OOM:unable to create new native thread
3.方法區和運行時常量池溢出
很簡單,保持GC Roots根的引用,再不斷在常量池添加對象,我們用String.intern()的方法。
/**
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class RuntimeConstantPool100M {
public static void main(String[] args) {
//使用List保持着常量池引用,避免Full GC回收常量池行爲
List<String> list = new ArrayList<String>();
//10MB的PermSize在integerr範圍內足夠產生OOM了
int i = 0;
while(true) {
list.add(String.valueOf(i++).intern());
}
}
}
運行結果:OOM: PermGen space。“PermGen space”,說明運行時常量池屬於方法區的一部分。
完結,這次的第二章,目前我感覺能用的總結完了,小夥伴們如果看我的博客感覺哪一塊兒理解的有問題或者哪一塊兒的語言組織的有問題,可以提出來,大家一塊兒努力進步。
下圖是關於jvm結構的信息。圖片出處:https://blog.csdn.net/wo541075754/article/details/102623406