JVM內(nèi)存模型
1民效、程序計(jì)數(shù)器
描述:程序計(jì)數(shù)器是一塊較小的內(nèi)存空間,可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器炬守。分支牧嫉、循環(huán)、跳轉(zhuǎn)减途、異常處理酣藻、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個(gè)計(jì)數(shù)器來完成。
1)在線程創(chuàng)建時(shí)創(chuàng)建
2)當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器
3)執(zhí)行本地方法時(shí)观蜗,PC的值為undefined
4)每條線程都需要有一個(gè)獨(dú)立的程序計(jì)數(shù)器,各條線程之間的計(jì)數(shù)器互不影響衣洁,獨(dú)立存儲(chǔ)墓捻,我們稱這類內(nèi)存區(qū)域?yàn)?code>線程私有的內(nèi)存
5)此內(nèi)存區(qū)域是唯一一個(gè)在Java 虛擬機(jī)規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域
2、虛擬機(jī)棧
描述:線程私有坊夫,它的生命周期與線程相同砖第。虛擬機(jī)棧描述的是Java 方法執(zhí)行的內(nèi)存模型:每個(gè)方法被執(zhí)行的時(shí)候都會(huì)同時(shí)創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表、操作棧环凿、動(dòng)態(tài)鏈接梧兼、方法出口等信息。
設(shè)置大兄翘:-Xss2M
1)線程私有
羽杰,生命周期和線程相同
2)棧由一系列幀組成(因此Java棧也叫做幀棧)
3)幀保存方法參數(shù)渡紫,方法的局部變量、操作數(shù)棧考赛、常量池
惕澎,指針
4)每一次方法調(diào)用創(chuàng)建一個(gè)幀,并壓棧
3颜骤、方法區(qū)
描述:方法區(qū)在一個(gè)jvm實(shí)例的內(nèi)部唧喉,類型信息被存儲(chǔ)在一個(gè)稱為方法區(qū)的內(nèi)存邏輯區(qū)中。類型信息是由類加載器在類加載時(shí)從類文件中提取出來的忍抽。類(靜態(tài))變量
也存儲(chǔ)在方法區(qū)中八孝。
簡(jiǎn)單說方法區(qū)用來存儲(chǔ)類型的元數(shù)據(jù)信息
設(shè)置大小:-XX:PermSize=10M -XX:MaxPermSize=10M
1)保存裝載的類信息
存儲(chǔ)
常量池
存儲(chǔ)static變量
鸠项,方法信息(方法名,返回類型,參數(shù)列表,方法的修飾符)通常和
永久區(qū)(Perm)
關(guān)聯(lián)在一起可以通過-XX:PermSize和-XX:MaxPermSize參數(shù)限制方法區(qū)的大小
5)方法區(qū)是線程安全的
干跛。由于所有的線程共享
方法區(qū),所以锈锤,方法區(qū)里的數(shù)據(jù)訪問必須被設(shè)計(jì)成線程安全的
- 方法區(qū)也可被垃圾收集驯鳖,當(dāng)某個(gè)類不在被使用(不可觸及)時(shí),JVM將卸載這個(gè)類久免,進(jìn)行垃圾收集
4浅辙、方法區(qū)內(nèi):運(yùn)行時(shí)常量池
描述:運(yùn)行時(shí)常量池是方法區(qū)的一部分.Class文件中除了有類的版本,字段阎姥,方法记舆,接口等描述信息外還有一項(xiàng)信息是常量池
,用于存放編譯期生成的各種字面量和符號(hào)引用呼巴,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)運(yùn)行時(shí)常量池中存放
運(yùn)行時(shí)常量池相對(duì)于Class 文件常量池的另外一個(gè)重要特征是具備動(dòng)態(tài)性泽腮,Java 語
言并不要求常量一定只能在編譯期產(chǎn)生,也就是并非預(yù)置入Class 文件中常量池的內(nèi)容
才能進(jìn)入方法區(qū)運(yùn)行時(shí)常量池衣赶,運(yùn)行期間也可能將新的常量放入池中诊赊,這種特性被開發(fā)
人員利用得比較多的便是String 類的intern() 方法
。
5府瞄、堆(heap)內(nèi)存
描述:堆是Java 虛擬機(jī)所管理的內(nèi)存中最大的一塊碧磅。Java 堆是被所有線程共享
的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建遵馆。此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例鲸郊,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存
設(shè)置大小:Xms2g -Xmx2g -Xmn1g-Xss128k
-Xms2G 虛擬機(jī)內(nèi)存最小2G
-Xms2G 虛擬機(jī)內(nèi)存最大2G
-Xmn1G 年輕代內(nèi)存1G
-Xss128 每個(gè)線程堆棧大小
Java 堆是被所有線程共享的
存放對(duì)象實(shí)例货邓,幾乎所有對(duì)象的實(shí)例都在這里分配內(nèi)存
3)堆是垃圾收集器管理的主要區(qū)域秆撮,因此很多時(shí)候也被稱做“GC 堆”。
4)堆的大小可以通過-Xms(最小值)和-Xmx(最大值)參數(shù)設(shè)置换况,-Xms為JVM啟動(dòng)時(shí)申請(qǐng)的最小內(nèi)存职辨,默認(rèn)為操作系統(tǒng)物理內(nèi)存的1/64但小于1G盗蟆,-Xmx為JVM可申請(qǐng)的最大內(nèi)存,默認(rèn)為物理內(nèi)存的1/4但小于1G
默認(rèn)當(dāng)空余堆內(nèi)存小于40%時(shí)拨匆,JVM會(huì)增大Heap到-Xmx指定的大小姆涩,可通過-XX:MinHeapFreeRation=來指定這個(gè)比列;當(dāng)空余堆內(nèi)存大于70%時(shí)惭每,JVM會(huì)減小heap的大小到-Xms指定的大小骨饿,可通過XX:MaxHeapFreeRation=來指定這個(gè)比列
從內(nèi)存回收的角度看,由于現(xiàn)在收集器基本都是采用的分代收集算法台腥,所以Java 堆中還可以細(xì)分為:新生代和老年代
新生代:程序新創(chuàng)建的對(duì)象都是從新生代分配內(nèi)存宏赘,新生代由Eden Space和兩塊相同大小的Survivor Space(通常又稱S0和S1或From和To)構(gòu)成,可通過-Xmn參數(shù)來指定新生代的大小黎侈,也可以通過-XX:SurvivorRation來調(diào)整Eden Space及SurvivorSpace的大小
.
-
-- 新生代
- -- Eden Space
- -- Survivor Space (S0/From)
- -- Survivor Space (S1/To)
-- 老年代
老年代:用于存放經(jīng)過多次新生代GC仍然存活的對(duì)象察署,例如緩存對(duì)象,新建的對(duì)象也有可能直接進(jìn)入老年代峻汉,主要有兩種情況:
1贴汪、大對(duì)象,可通過啟動(dòng)參數(shù)設(shè)置-XX:PretenureSizeThreshold=1024(單位為字節(jié)休吠,默認(rèn)為0)來代表超過多大時(shí)就不在新生代分配扳埂,而是直接在老年代分配。
2瘤礁、大的數(shù)組對(duì)象阳懂,且數(shù)組中無引用外部對(duì)象。
老年代所占的內(nèi)存大小為-Xmx對(duì)應(yīng)的值減去-Xmn對(duì)應(yīng)的值柜思。
6岩调、本地方法棧
描述:本地方法棧(Native MethodStacks)與虛擬機(jī)棧所發(fā)揮的作用是非常相似的,其區(qū)別不過是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java 方法(也就是字節(jié)碼)服務(wù)赡盘,而本地方法棧則是為虛擬機(jī)使用到的Native 方法服務(wù)
号枕。虛擬機(jī)規(guī)范中對(duì)本地方法棧中的方法使用的語言、使用方式與數(shù)據(jù)結(jié)構(gòu)并沒有強(qiáng)制規(guī)定陨享,因此具體的虛擬機(jī)可以自由實(shí)現(xiàn)本地方法棧葱淳。
7、堆外內(nèi)存:直接內(nèi)存
- NIO的Buffer提供了一個(gè)可以不經(jīng)過JVM內(nèi)存直接訪問系統(tǒng)物理內(nèi)存的類——DirectBuffer霉咨。
- DirectBuffer類繼承自ByteBuffer蛙紫,但和普通的ByteBuffer不同拍屑,普通的ByteBuffer仍在JVM堆上分配內(nèi)存途戒,其最大內(nèi)存受到最大堆內(nèi)存的限制;
- DirectBuffer直接分配在物理內(nèi)存中僵驰,并不占用堆空間喷斋,其可申請(qǐng)的最大內(nèi)存受操作系統(tǒng)限制唁毒。
- 直接內(nèi)存的讀寫操作比普通Buffer快,但它的創(chuàng)建星爪、銷毀比普通Buffer慢浆西。
- 直接內(nèi)存使用于需要大內(nèi)存空間且頻繁訪問的場(chǎng)合,不適用于頻繁申請(qǐng)釋放內(nèi)存的場(chǎng)合顽腾。
(Note:DirectBuffer并沒有真正向OS申請(qǐng)分配內(nèi)存近零,其最終還是通過調(diào)用Unsafe的allocateMemory()
來進(jìn)行內(nèi)存分配。
不過JVM對(duì)Direct Memory可申請(qǐng)的大小也有限制抄肖,可用-XX:MaxDirectMemorySize=1M設(shè)置久信,這部分內(nèi)存不受JVM垃圾回收管理
。)
直接內(nèi)存(堆外內(nèi)存)與堆內(nèi)存比較
1漓摩、 直接內(nèi)存申請(qǐng)空間耗費(fèi)更高的性能裙士,當(dāng)頻繁申請(qǐng)到一定量時(shí)尤為明顯
2、直接內(nèi)存IO讀寫的性能要優(yōu)于普通的堆內(nèi)存管毙,在多次讀寫操作的情況下差異明顯
代碼驗(yàn)證:
package com.xnccs.cn.share;
import java.nio.ByteBuffer;
/**
* 直接內(nèi)存 與 堆內(nèi)存的比較
*/
public class ByteBufferCompare {
public static void main(String[] args) {
allocateCompare(); //分配比較
operateCompare(); //讀寫比較
}
/**
* 直接內(nèi)存 和 堆內(nèi)存的 分配空間比較
*
* 結(jié)論: 在數(shù)據(jù)量提升時(shí)腿椎,直接內(nèi)存相比非直接內(nèi)的申請(qǐng),有很嚴(yán)重的性能問題
*
*/
public static void allocateCompare(){
int time = 10000000; //操作次數(shù)
long st = System.currentTimeMillis();
for (int i = 0; i < time; i++) {
//ByteBuffer.allocate(int capacity) 分配一個(gè)新的字節(jié)緩沖區(qū)夭咬。
ByteBuffer buffer = ByteBuffer.allocate(2); //非直接內(nèi)存分配申請(qǐng)
}
long et = System.currentTimeMillis();
System.out.println("在進(jìn)行"+time+"次分配操作時(shí)啃炸,堆內(nèi)存 分配耗時(shí):" + (et-st) +"ms" );
long st_heap = System.currentTimeMillis();
for (int i = 0; i < time; i++) {
//ByteBuffer.allocateDirect(int capacity) 分配新的直接字節(jié)緩沖區(qū)。
ByteBuffer buffer = ByteBuffer.allocateDirect(2); //直接內(nèi)存分配申請(qǐng)
}
long et_direct = System.currentTimeMillis();
System.out.println("在進(jìn)行"+time+"次分配操作時(shí)皱埠,直接內(nèi)存 分配耗時(shí):" + (et_direct-st_heap) +"ms" );
}
/**
* 直接內(nèi)存 和 堆內(nèi)存的 讀寫性能比較
*
* 結(jié)論:直接內(nèi)存在直接的IO 操作上肮帐,在頻繁的讀寫時(shí) 會(huì)有顯著的性能提升
*
*/
public static void operateCompare(){
int time = 1000000000;
ByteBuffer buffer = ByteBuffer.allocate(2*time);
long st = System.currentTimeMillis();
for (int i = 0; i < time; i++) {
// putChar(char value) 用來寫入 char 值的相對(duì) put 方法
buffer.putChar('a');
}
buffer.flip();
for (int i = 0; i < time; i++) {
buffer.getChar();
}
long et = System.currentTimeMillis();
System.out.println("在進(jìn)行"+time+"次讀寫操作時(shí),非直接內(nèi)存讀寫耗時(shí):" + (et-st) +"ms");
ByteBuffer buffer_d = ByteBuffer.allocateDirect(2*time);
long st_direct = System.currentTimeMillis();
for (int i = 0; i < time; i++) {
// putChar(char value) 用來寫入 char 值的相對(duì) put 方法
buffer_d.putChar('a');
}
buffer_d.flip();
for (int i = 0; i < time; i++) {
buffer_d.getChar();
}
long et_direct = System.currentTimeMillis();
System.out.println("在進(jìn)行"+time+"次讀寫操作時(shí)边器,直接內(nèi)存讀寫耗時(shí):" + (et_direct - st_direct) +"ms");
}
}
輸出:
在進(jìn)行10000000次分配操作時(shí)训枢,堆內(nèi)存 分配耗時(shí):12ms
在進(jìn)行10000000次分配操作時(shí),直接內(nèi)存 分配耗時(shí):8233ms
在進(jìn)行1000000000次讀寫操作時(shí)忘巧,非直接內(nèi)存讀寫耗時(shí):4055ms
在進(jìn)行1000000000次讀寫操作時(shí)恒界,直接內(nèi)存讀寫耗時(shí):745ms
可以自己設(shè)置不同的time 值進(jìn)行比較
從數(shù)據(jù)流的角度,來看
- 非直接內(nèi)存作用鏈: 本地IO –>直接內(nèi)存–>非直接內(nèi)存–>直接內(nèi)存–>本地IO
- 直接內(nèi)存作用鏈: 本地IO–>直接內(nèi)存–>本地IO
直接內(nèi)存使用場(chǎng)景
- 有很大的數(shù)據(jù)需要存儲(chǔ)砚嘴,它的生命周期很長(zhǎng)
- 適合頻繁的IO操作十酣,例如網(wǎng)絡(luò)并發(fā)場(chǎng)景
JVM核心參數(shù)設(shè)置
初始堆大小: -Xms
- 默認(rèn)值是物理內(nèi)存的1/64(<1GB)
- 空余堆內(nèi)存小于40%時(shí)际长,JVM會(huì)增大堆到-Xmx的最大限制
- 此值可以設(shè)置與-Xmx相同耸采,以避免每次垃圾回收完成后JVM重新分配內(nèi)存。
- 整改堆的大小=年輕代大小 + 年老代大小 + 持久代大小
- 增大年輕代后工育,將減少年老代大小.
最大堆大邢河睢: -Xmx
- 默認(rèn)值:物理內(nèi)存的1/4(<1GB)
- 空余堆內(nèi)存大于70%,JVM會(huì)減少直到-Xms的最小限制
年輕代大腥绯瘛:-Xmn
- 整改堆的大小=年輕代大小 + 年老代大小 + 持久代大小增大年輕代后嘱朽,將減少年老代大小旭贬,此值對(duì)系統(tǒng)的性能影響較大,
- 官方推薦配置:堆的3/8
設(shè)置年輕代大刑掠尽: -XX:NewSize
- 設(shè)置年輕代的初始值大小
- 建議設(shè)置為整個(gè)堆大小的1/3或者1/4
年輕代最大值: -XX:MaxNewSize
- 設(shè)置年輕代的最大值大小
- 建議設(shè)置為整個(gè)堆大小的1/3或者1/4
持久代的初始值: -XX:PermSize
設(shè)置持久代最大值: -XX:MaxPermSize
每個(gè)線程的棧大小: -Xss
- 在相同物理內(nèi)存下稀轨,減小這個(gè)值能生成更多的線程。但是操作系統(tǒng)對(duì)一個(gè)進(jìn)程內(nèi)的線程數(shù)還是有限制的岸军,不能無限生成奋刽,經(jīng)驗(yàn)值在3000~5000左右
線程棧大小:-XX:ThreadStackSize
代碼示例
堆溢出測(cè)試:
package com.yhj.jvm.memory.heap;
import java.util.ArrayList;
import java.util.List;
/**
* @Described:堆溢出測(cè)試
* @VM args:-verbose:gc -Xms20M -Xmx20M -XX:+PrintGCDetails
* @FileNmae com.yhj.jvm.memory.heap.HeapOutOfMemory.java
*/
public class HeapOutOfMemory {
public static void main(String[] args) {
List<TestCase> cases = new ArrayList<TestCase>();
while(true){
cases.add(new TestCase());
}
}
}
Java 堆內(nèi)存的OutOfMemoryError異常是實(shí)際應(yīng)用中最常見的內(nèi)存溢出異常情況艰赞。出現(xiàn)Java 堆內(nèi)
存溢出時(shí)杨名,異常堆棧信息“java.lang.OutOfMemoryError”會(huì)跟著進(jìn)一步提示“Java heap
space”。
要解決這個(gè)區(qū)域的異常猖毫,一般的手段是首先通過內(nèi)存映像分析工具(如Eclipse
Memory Analyzer)對(duì)dump 出來的堆轉(zhuǎn)儲(chǔ)快照進(jìn)行分析台谍,重點(diǎn)是確認(rèn)內(nèi)存中的對(duì)象是
否是必要的,也就是要先分清楚到底是出現(xiàn)了內(nèi)存泄漏(Memory Leak)還是內(nèi)存溢
出(Memory Overflow)吁断。圖2-5 顯示了使用Eclipse Memory Analyzer 打開的堆轉(zhuǎn)儲(chǔ)快
照文件趁蕊。
如果是內(nèi)存泄漏,可進(jìn)一步通過工具查看泄漏對(duì)象到GC Roots 的引用鏈仔役。于是就
能找到泄漏對(duì)象是通過怎樣的路徑與GC Roots 相關(guān)聯(lián)并導(dǎo)致垃圾收集器無法自動(dòng)回收
它們的掷伙。掌握了泄漏對(duì)象的類型信息,以及GC Roots 引用鏈的信息又兵,就可以比較準(zhǔn)確
地定位出泄漏代碼的位置任柜。
如果不存在泄漏,換句話說就是內(nèi)存中的對(duì)象確實(shí)都還必須存活著沛厨,那就應(yīng)當(dāng)檢查
虛擬機(jī)的堆參數(shù)(-Xmx 與-Xms)宙地,與機(jī)器物理內(nèi)存對(duì)比看是否還可以調(diào)大,從代碼上
檢查是否存在某些對(duì)象生命周期過長(zhǎng)逆皮、持有狀態(tài)時(shí)間過長(zhǎng)的情況宅粥,嘗試減少程序運(yùn)行期
的內(nèi)存消耗。
以上是處理Java 堆內(nèi)存問題的簡(jiǎn)略思路电谣,處理這些問題所需要的知識(shí)秽梅、工具與經(jīng)驗(yàn)
在后面的幾次分享中我會(huì)做一些額外的分析。
java棧溢出
package com.yhj.jvm.memory.stack;
/**
* @Described:棧層級(jí)不足探究
* @VM args:-Xss128k
* @FileNmae com.yhj.jvm.memory.stack.StackOverFlow.java
*/
public class StackOverFlow {
private int i ;
public void plus() {
i++;
plus();
}
public static void main(String[] args) {
StackOverFlow stackOverFlow = new StackOverFlow();
try {
stackOverFlow.plus();
} catch (Exception e) {
System.out.println("Exception:stack length:"+stackOverFlow.i);
e.printStackTrace();
} catch (Error e) {
System.out.println("Error:stack length:"+stackOverFlow.i);
e.printStackTrace();
}
}
}
常量池溢出(常量池都有哪些信息剿牺,我們?cè)诤罄m(xù)的JVM類文件結(jié)構(gòu)中詳細(xì)描述)
package com.yhj.jvm.memory.constant;
import java.util.ArrayList;
import java.util.List;
/**
* @Described:常量池內(nèi)存溢出探究
* @VM args : -XX:PermSize=10M -XX:MaxPermSize=10M
* @FileNmae com.yhj.jvm.memory.constant.ConstantOutOfMemory.java
*/
public class ConstantOutOfMemory {
public static void main(String[] args) throws Exception {
try {
List<String> strings = new ArrayList<String>();
int i = 0;
while(true){
strings.add(String.valueOf(i++).intern());
}
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
}
方法區(qū)溢出
package com.yhj.jvm.memory.methodArea;
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
/**
* @Described:方法區(qū)溢出測(cè)試
* 使用技術(shù) CBlib
* @VM args : -XX:PermSize=10M -XX:MaxPermSize=10M
* @FileNmae com.yhj.jvm.memory.methodArea.MethodAreaOutOfMemory.java
*/
public class MethodAreaOutOfMemory {
public static void main(String[] args) {
while(true){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(TestCase.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object arg0, Method arg1, Object[] arg2,
MethodProxy arg3) throws Throwable {
return arg3.invokeSuper(arg0, arg2);
}
});
enhancer.create();
}
}
}
直接內(nèi)存溢出
package com.yhj.jvm.memory.directoryMemory;
import java.lang.reflect.Field;
import sun.misc.Unsafe;
/**
* @Described:直接內(nèi)存溢出測(cè)試
* @VM args: -Xmx20M -XX:MaxDirectMemorySize=10M
* @FileNmae com.yhj.jvm.memory.directoryMemory.DirectoryMemoryOutOfmemory.java
*/
public class DirectoryMemoryOutOfmemory {
private static final int ONE_MB = 1024*1024;
private static int count = 1;
public static void main(String[] args) {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
while (true) {
unsafe.allocateMemory(ONE_MB);
count++;
}
} catch (Exception e) {
System.out.println("Exception:instance created "+count);
e.printStackTrace();
} catch (Error e) {
System.out.println("Error:instance created "+count);
e.printStackTrace();
}
}
}