一、運(yùn)行時(shí)數(shù)據(jù)區(qū)域
Java虛擬機(jī)在運(yùn)行Java程序時(shí)會(huì)將他管理的內(nèi)存分為若干個(gè)不同的數(shù)據(jù)區(qū)域,有的在Java虛擬機(jī)進(jìn)程中一直存在有的依賴線程的啟動(dòng)和介紹而建立和銷毀。包括:程序計(jì)數(shù)器,Java虛擬機(jī)棧,本地方法棧,Java堆,方法區(qū),運(yùn)行時(shí)常量池,直接內(nèi)存

1.1程序計(jì)數(shù)器
可以看做是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)的指示器;
每條線程都有自己獨(dú)立的程序計(jì)數(shù)器,用來記錄當(dāng)前線程正在執(zhí)行的虛擬機(jī)字節(jié)碼指令地址(當(dāng)前操作的是Java方法,如果是本地方法計(jì)數(shù)器的值為空)
原因:多線程的情況下,存在頻繁的上下文切換(一個(gè)處理器同一時(shí)刻只能處理一個(gè)線程),所以需要記錄當(dāng)前線程執(zhí)行的位置
各線程之間的計(jì)數(shù)器互不影響,存在于線程的“私有內(nèi)存”中
1.2Java虛擬機(jī)棧
1.線程私有
2.虛擬機(jī)棧描述的是Java執(zhí)行過程中線程的內(nèi)存模型:每個(gè)方法被執(zhí)行的時(shí)候,Java虛擬機(jī)都會(huì)同步創(chuàng)建一個(gè)棧幀用于存儲(chǔ) 局部變量,操作數(shù)棧,動(dòng)態(tài)連接,方法出口等信息。每個(gè)方法被調(diào)用直至執(zhí)行完畢的過程,就對(duì)應(yīng)一個(gè)棧幀在虛擬機(jī)棧中從入棧到出棧(數(shù)據(jù)結(jié)構(gòu) 因?yàn)榉椒ㄊ菍蛹?jí)調(diào)用)的過程。
2.存放基本數(shù)據(jù)類型和對(duì)象引用,對(duì)象實(shí)例存放在堆中
如何理解棧和棧幀的區(qū)別?
Java虛擬機(jī)啟動(dòng)的時(shí)候會(huì)指定棧和堆的大小 好比棧的大小是1024k
每個(gè)線程都能1024K大小的??臻g
線程每進(jìn)一個(gè)方法會(huì)創(chuàng)建一個(gè)棧幀,好比進(jìn)入了很多方法創(chuàng)建的棧幀超過了棧的大小就會(huì)棧溢出
在可以接受的范圍內(nèi),棧的大小設(shè)置的越小方法返回的速度越快
如何確定java方法棧(棧幀)的大???
其他的暫時(shí)還不知道,先說一下局部變量
局部變量表存放編譯期可知的基本數(shù)據(jù)類型,引用數(shù)據(jù)類型,returnAddress(指向了一條字節(jié)碼指令的地址)
局部變量表所需的空間在編譯期就完成了分配,運(yùn)行時(shí)當(dāng)進(jìn)入一個(gè)方法時(shí)需要在棧幀中分配多大的局部變量空間是確定的
HotSpot 虛擬機(jī)棧幀 的大小是不能動(dòng)態(tài)擴(kuò)展的
猜想創(chuàng)建棧幀大小時(shí)應(yīng)該是有個(gè)讀取的依據(jù)
設(shè)置堆棧大小? ? ? 棧溢出這個(gè)棧的大小是什么時(shí)候規(guī)定的
對(duì)于Java方法棧這塊內(nèi)存區(qū)域可能出現(xiàn)的異常:
StackOverflowError棧溢出,如果線程請(qǐng)求的棧的深度大于虛擬機(jī)允許的深度就會(huì)拋出這個(gè)異常
OutOfMemoryError堆溢出,如果虛擬機(jī)棧大小可以動(dòng)態(tài)擴(kuò)展(有的虛擬機(jī)可以像classic),當(dāng)無法申請(qǐng)到足夠的內(nèi)存就會(huì)拋這個(gè)異常
可能出現(xiàn)棧溢出的場(chǎng)景:方法的循環(huán)調(diào)用(循環(huán)中一直在調(diào)用就一直創(chuàng)建棧幀,循環(huán)中所有的方法一直沒有釋放就一直不會(huì)出棧,所以很容易就棧溢出了)
1.3本地方法棧
和Java方法棧和相似,只不過一個(gè)執(zhí)行的是Java方法服務(wù)一個(gè)是本地方法服務(wù)
和虛擬機(jī)棧一樣,本地方法棧也會(huì)在深度溢出和擴(kuò)展失敗是拋出StackOverflowError和OutOfMemoryError
hotspot直接將這兩個(gè)方法棧合二為一了
1.4java堆
1存儲(chǔ)對(duì)象
幾乎所有的對(duì)象實(shí)例都在堆內(nèi)存當(dāng)中,堆內(nèi)存區(qū)域所有線程共享
堆中具體存放那些對(duì)象?
全局普通變量是否存在堆中
2細(xì)化
垃圾回收的對(duì)象也是堆
現(xiàn)代垃圾回收器大部分是基于分代收集理論設(shè)計(jì),所以堆又被劃分為新生代、老年代、永久代
這種區(qū)域劃分并非堆的固有設(shè)計(jì)而是垃圾回收器的特性
也有不采用分代回收的垃圾回收器
3其他特性
可以在物理上處于不連續(xù)的內(nèi)存空間
堆的大小上可以設(shè)置成固定的也可以設(shè)置成可擴(kuò)展的 -Xmx -Xms (可擴(kuò)展的當(dāng)無法擴(kuò)展時(shí)就會(huì)內(nèi)存溢出)
1.5方法區(qū)(別名 非堆)
1.線程共享
2.存放已被虛擬機(jī)加載的類信息(類名,訪問修飾符、字段描述、方法描述)、常量、靜態(tài)變量、即時(shí)編譯期編譯后的代碼緩存等數(shù)據(jù)
3.方法區(qū)的垃圾回收
常量池的回收和類的卸載
Hotspot中方法區(qū)和“永久代”的關(guān)系及演變
最早是使用永久代來實(shí)現(xiàn)方法區(qū),相當(dāng)于永久代就是方法區(qū)
方法區(qū)物理上存在堆里,而且是在堆的持久代里面;但在邏輯上,方法區(qū)和堆是獨(dú)立的。
一般說堆的持久代就是說方法區(qū),因?yàn)橐坏㎎VM把方法區(qū)(類信息,常量,靜態(tài)靜態(tài)變量)加載進(jìn)內(nèi)存以后,這些內(nèi)存一般是不會(huì)被回收的了。
壞處:這種方式更容易內(nèi)存溢出,永久代有內(nèi)存大小限制,其他虛擬機(jī)方法區(qū)可以使用進(jìn)程最大內(nèi)存
Hotspot中的演變:jdk7 將永久代中的字符串常量池,靜態(tài)變量移出到堆中
jdk8廢棄了永久代的概念,將剩下的類信息等全部放到元空間
1.6運(yùn)行時(shí)常量池
1.是方法區(qū)的一部分,存放class文件的常量池表里的數(shù)據(jù)和運(yùn)行時(shí)生成的常量數(shù)據(jù)
1.7直接內(nèi)存
不是虛擬機(jī)運(yùn)行時(shí)內(nèi)存區(qū)域,其內(nèi)存大小的設(shè)置會(huì)影響到j(luò)vm內(nèi)存
如果 其他內(nèi)存 + jvm內(nèi)存 > 總內(nèi)存? 容易觸發(fā)jvm內(nèi)存溢出
直接內(nèi)存也可以通過參數(shù)設(shè)置,如果不設(shè)置默認(rèn)和堆大小一致
1.8普通Java對(duì)象的創(chuàng)建過程
1.8.1創(chuàng)建過程
1.根據(jù)指令參數(shù)在常量池中看能否找到對(duì)應(yīng)的類的符號(hào)引用
2.根據(jù)符號(hào)引用看這個(gè)類是否已經(jīng)被加載、解析、初始化了
3.沒有,則執(zhí)行類的初始化過程
4.在堆中為新對(duì)象分配內(nèi)存空間(“指針碰撞”、”空閑列表“)(對(duì)象的內(nèi)存大小在類加載完就確定了)5.設(shè)置對(duì)象頭信息(偏向鎖信息,對(duì)象是哪個(gè)類的實(shí)例,GC分代年齡)
6.執(zhí)行Class的<init>方法,對(duì)對(duì)象進(jìn)行初始化
7.對(duì)象引用入棧

1.8.2對(duì)象的內(nèi)存布局
對(duì)象頭:
對(duì)象運(yùn)行時(shí)數(shù)據(jù): 哈希嗎、GC年齡分代、線程持有的鎖、偏向鎖的線程ID、偏向鎖時(shí)間戳
類型指針:指向類的元數(shù)據(jù)的指針,元數(shù)據(jù)在方法區(qū)中(Hotspot里的在元空間)
如果對(duì)象是數(shù)組還有塊空間記錄數(shù)組大小
實(shí)例數(shù)據(jù):
記錄父類、子類中各種定義的類型的字段內(nèi)容
對(duì)齊填充:
虛擬機(jī)要求對(duì)象的大小必須是8字節(jié)的整數(shù)倍,如果對(duì)象不是需要對(duì)齊填充來補(bǔ)充,沒有實(shí)際意義
為什么要是8字節(jié)的倍數(shù)?
棧中存放的對(duì)象引用是什么?
對(duì)象在堆中的地址值
or對(duì)象在句柄池中的地址,句柄存放的是對(duì)象實(shí)例數(shù)據(jù)指針和對(duì)象元數(shù)據(jù)指針
二、內(nèi)存溢出
除了程序計(jì)數(shù)器之外其他的內(nèi)存區(qū)域都有可能內(nèi)存溢出
內(nèi)存泄漏和內(nèi)存溢出的區(qū)別
2.1堆溢出
2.1.1堆溢出的原因
堆溢出是由于沒被引用的對(duì)象(垃圾)過多造成JVM沒有及時(shí)回收,造成堆的溢出。如果出現(xiàn)這種現(xiàn)象可行代碼排查:
一)是否App中的類中和引用變量過多使用了Static修飾 如public staitc Student s;在類中的屬性中使用 static修飾的最好只用基本類型或字符串。如public static int i = 0; //public static String str;
二)是否App中使用了大量的遞歸或無限遞歸(遞歸中用到了大量的建新的對(duì)象)
三)是否App中使用了大量循環(huán)或死循環(huán)(循環(huán)中用到了大量的新建的對(duì)象)
四)檢查App中是否使用了向數(shù)據(jù)庫查詢所有記錄的方法。即一次性全部查詢的方法,如果數(shù)據(jù)量超過10萬多條了,就可能會(huì)造成內(nèi)存溢出。所以在查詢時(shí)應(yīng)采用“分頁查詢”。
五)檢查是否有數(shù)組,List,Map中存放的是對(duì)象的引用而不是對(duì)象,因?yàn)檫@些引用會(huì)讓對(duì)應(yīng)的對(duì)象不能被釋放。會(huì)大量存儲(chǔ)在內(nèi)存中。
六)檢查是否使用了“非字面量字符串進(jìn)行+”的操作。因?yàn)镾tring類的內(nèi)容是不可變的,每次運(yùn)行"+"就會(huì)產(chǎn)生新的對(duì)象,如果過多會(huì)造成新String對(duì)象過多,從而導(dǎo)致JVM沒有及時(shí)回收而出現(xiàn)內(nèi)存溢出。
2.2.2堆溢出的排查方式
1.設(shè)置參數(shù)-XX:+HeapDumpOnOutOfMemoryError 堆溢出前將堆轉(zhuǎn)儲(chǔ)快照
2.根據(jù)快照文件排查是內(nèi)存泄漏還是內(nèi)存溢出
3.如果是內(nèi)存泄漏找到內(nèi)存泄漏的代碼
4.如果是內(nèi)存溢出,根據(jù)快照文件找到內(nèi)存溢出代碼是否有生命周期過長,持有時(shí)間過長,存儲(chǔ)結(jié)構(gòu)不合理等情況,同時(shí)優(yōu)化堆的配置
2.2棧溢出
2.2.1棧溢出原因
1.棧的內(nèi)存過小,虛擬機(jī)請(qǐng)求的棧的深度超過了所允許的最大深度
2.命名了大量對(duì)象,超過了本地變量表長度
2.3方法區(qū)溢出
2.3.1方法區(qū)溢出原因
1.程序中有生成大量動(dòng)態(tài)類的場(chǎng)景
2.有大量jsp或動(dòng)態(tài)生成jsp的應(yīng)用(jsp第一次運(yùn)行需要編譯為Java類)
3.存在OSGI的應(yīng)用(不同的方式加載生成不同的類)
總之,運(yùn)行時(shí)生成了大量類的信息,導(dǎo)致溢出
2.4本機(jī)直接內(nèi)存溢出
本機(jī)直接內(nèi)存如不設(shè)置默認(rèn)和堆大小一致,NIO的操作容易導(dǎo)致直接內(nèi)存溢出