描述的是java方法執行的內存模型:每個方法在執行的同時多會創建一個棧幀用於存儲局部變量表、操作數棧、動態鏈表、方法出口等信息。
每一個方法從調用直至完成的過程,就對應着一個棧幀在虛擬機中入棧到出棧的過程。
局部變量表存放了編譯期可知的各種基本數據類型和對象引用,所需內存空間在編譯期確定。
操作數棧是爲運行過程中、字節碼指令執行服務。
參數設置:-Xoss參數設置本地方法棧大小(對於HotSpot無效),-Xss參數設置棧容量 例: -Xss128k
(3)、本地方法棧
線程隔離的數據區:桶虛擬機棧作用類似。
主要用於虛擬機執行Java本地(Native)方法。Sun HotSpot虛擬機把本地方法棧和虛擬機棧合二爲一。
方法區它用於儲存已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據,除了和Java堆一樣不需要連續的內存和可以選擇固定大小或者可擴展外,還可以選擇不實現垃圾收集。這個區域的內存回收目標主要是針對常量池的回收和對類型的卸載。
直接內存並不是虛擬機運行時數據區的一部分。在NIO中,引入了一種基於通道和緩衝區的I/O方式,它可以使用native函數直接分配堆外內存,然後通過一個存儲在java堆中的DirectByteBuffer對象作爲這塊內存的引用進行操作。
參數設置:-XX:MaxDirectMemorySize設置最大值,默認與java堆最大值一樣。
2、對象創建與內存佈局
(1)、對象的創建
虛擬機遇到一條new指令時,首先將去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已經被加載、解析和初始化過。如果沒有,那必須先執行相應的類加載過程.
2) .分配內存
接下來將爲新生對象分配內存,爲對象分配內存空間的任務等同於把一塊確定的大小的內存從Java堆中劃分出來。假設Java堆中內存是絕對規整的,所有用過的內存放在一遍,空閒的內存放在另一邊,中間放着一個指針作爲分界點的指示器,那所分配內存就僅僅是把那個指針指向空閒空間那邊挪動一段與對象大小相等的距離,這個分配方式叫做“指針碰撞”。如果Java堆中的內存並不是規整的,已使用的內存和空閒的內存相互交錯,那就沒辦法簡單地進行指針碰撞了,虛擬機就必須維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄,這種分配方式成爲“空閒列表”。選擇那種分配方式由Java堆是否規整決定,而Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。
3).設置
內存分配完後,虛擬機將內存設置爲零,然後設置對象頭,具體包括設置對象類型元數據信息、對象哈希嗎,對象分帶年齡,以及鎖相關的信息。
4). Init
執行new指令之後會接着執行Init方法,進行構造函數初始化,按照程序員要求完成對象狀態設置,這樣一個對象纔算產生出來。
對象頭包括兩部分:
a) 儲存對象自身的運行時數據,如哈希碼、GC分帶年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳。
b) 另一部分是指類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是那個類的實例。
2). 對象的訪問定位
- 使用句柄訪問
Java堆中將會劃分出一塊內存來作爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址。
優勢 :reference中存儲的是穩點的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行爲)時只會改變句柄中的實例數據指針,而reference本身不需要修改
- 使用直接指針訪問
Java堆對象的佈局就必須考慮如何訪問類型數據的相關信息,而refreence中存儲的直接就是對象的地址。
優勢:速度更快,節省了一次指針定位的時間開銷,由於對象的訪問在Java中非常頻繁,因此這類開銷積少成多後也是一項非常可觀的執行成本
package com.xl.jvm;
import java.util.ArrayList;
import java.util.List;
/**
* VM Args:-Xms20m -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());
}
}
}
如果虛擬機在擴展棧時無法申請到足夠的內存空間,則拋出OutOfMemoryError異常。
在單線程下,無論由於棧幀太大還是虛擬機棧容量太小,當內存無法分配的時候,虛擬機拋出的都是StackOverflowError異常。
如果是多線程導致的內存溢出,與棧空間是否足夠大並不存在任何聯繫,這個時候每個線程的棧分配的內存越大,反而越容易產生內存溢出異常。解決的時候是在不能減少線程數或更換64爲的虛擬機的情況下,就只能通過減少最大堆和減少棧容量來換取更多的線程。
package com.xl.jvm;
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;
}
}
}
String.intern()是一個Native方法,它的作用是:如果字符串常量池中已經包含一個等於此String對象的字符串,則返回代表池中這個字符串的String對象;否則,將此String對象包含的字符串添加到常量池中,並且返回此String對象的引用。由於常量池分配在永久代中,可以通過-XX:PermSize和-XX:MaxPermSize限制方法區大小,從而間接限制其中常量池的容量。
Intern():JDK1.6 intern方法會把首次遇到的字符串實例複製到永久代,返回的也是永久代中這個字符串實例的引用,而由StringBuilder創建的字符串實例在Java堆上,所以必然不是一個引用。JDK1.7 intern()方法的實現不會再複製實例,只是在常量池中記錄首次出現的實例引用,因此intern()返回的引用和由StringBuilder創建的那個字符串實例是同一個。
實例:虛擬機參數:-XX:PermSize=10M -XX:MaxPermSize=10M
package com.xl.jvm;
import java.util.ArrayList;
import java.util.List;
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
// 使用List保持着常量池引用,避免Full GC回收常量池行爲
List<String> list = new ArrayList<String>();
// 10MB的PermSize在integer範圍內足夠產生OOM了
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}