寫在前面:為了更加深入的了解java虛擬機便斥,就看了一下《深入理解java虛擬機》這本書国夜,一方面為了總結(jié)一下自己的認(rèn)識米愿,另一方面就是想與各位分享厦凤,如果有什么不對的地方,歡迎指正
在進行java開發(fā)的時候育苟,開發(fā)人員一般都不需要關(guān)注內(nèi)存的請求较鼓,釋放等過程,那么jvm是怎樣幫我們完成的呢
深入理解java虛擬機(二)垃圾收集器與內(nèi)存分配策略
java內(nèi)存區(qū)域與內(nèi)存溢出異常
1违柏、運行時數(shù)據(jù)區(qū)
- 程序計數(shù)器(Program Counter Register)
用于保存程序的當(dāng)前執(zhí)行的指令地址博烂,當(dāng)cpu執(zhí)行指令時候,會從程序計數(shù)器中獲取當(dāng)前指定指令所在位置的存單元的地址漱竖,然后根據(jù)地址獲取到指令禽篱,執(zhí)行,然后指向下一條馍惹,循環(huán)直至結(jié)束躺率,jvm是通過多線程來完成指令的,為了線程切換后能夠恢復(fù)之前的狀態(tài)万矾,所以每個線程都有私有的程序計數(shù)器悼吱,唯一一個不會發(fā)生OutOfMemoryError的地方 - Java棧(VM Stack)
java棧中存儲的是棧幀,每個棧幀對應(yīng)一個被調(diào)用的方法良狈,棧幀中包含局部變量表(七大數(shù)據(jù)類型后添,對象引用,)们颜,操作數(shù)棧吕朵,動態(tài)鏈接猎醇,方法出口等,努溃,指向當(dāng)前類所屬的常量池的引用硫嘶,和方法返回地址,當(dāng)線程調(diào)用一個方法時梧税,會創(chuàng)建對應(yīng)的棧幀沦疾,然后進棧,執(zhí)行完成之后第队,出棧哮塞,所以說運行的方法在棧頂,遞歸容易出現(xiàn)內(nèi)存溢出的現(xiàn)象凳谦,棧不用程序員自己管理內(nèi)存 (java有自己的垃圾回收機制)忆畅,棧區(qū)是線程私有的,因為每個線程執(zhí)行的方法不同尸执,容易混家凯,
棧中的異常
當(dāng)線程請求棧深度大于虛擬機所允許的深度的時候,會拋出stackOverflowError異常如失,虛擬機棸砘澹可以動態(tài)擴展,如果在擴展的時候無法申請到足夠的內(nèi)存褪贵,就會拋出outofmemoryError異常
- 本地方法棧(Native Method Stack)
可與java棧放在一起說掂之,區(qū)別就是,本地運行的是nactive的方法,也會發(fā)生oom的異常 - 方法區(qū)(Method Area)
所有線程共享脆丁,存儲已被虛擬機加載的類信息世舰,常量,靜態(tài)變量偎快,即時編譯器編譯后的代碼等數(shù)據(jù)冯乘。這個區(qū)域的內(nèi)存回收目標(biāo)主要是針對常量池的對象的回收和對類型的卸載。 也會拋出oom的異常 - 堆(Heap)
JAVA 堆晒夹,也稱 GC 堆,所有線程共享姊氓,存放對象的實例和數(shù)組丐怯, JAVA 堆是垃圾收集器管理的主要區(qū)域。當(dāng)申請內(nèi)存不夠的時候也會拋出oom異常 - 運行時常量池
屬于方法區(qū)翔横,Class文件的信息读跷,存放編譯期生成的各種字面量和符號引用,這部分內(nèi)容將在類加載后進入方法區(qū)的運行時常量池禾唁,也會有oom異常 - 直接內(nèi)存
并不是虛擬機運行時數(shù)據(jù)區(qū)的一部分效览,也不是java虛擬機規(guī)范中定義的內(nèi)存區(qū)域无切,引入NIO之后,引入了一種基于通道(channel)與緩沖區(qū)(buffer)的IO方式丐枉,它可以使用native函數(shù)庫直接分配堆外內(nèi)存哆键,然后通過一個存儲在java堆中的DirectByteBuffer對象作為這塊內(nèi)存的引用進行操作,這樣能在一些場景中提高性能瘦锹,因為避免了在java堆和native堆中來回復(fù)制數(shù)據(jù)籍嘹。也有oom異常
2、hotspot虛擬機對象探秘
1弯院、對象的創(chuàng)建
當(dāng)在new一個對象的時候辱士,首先會去常量池(存放類的信息,屬于方法區(qū))定位這個類的信息听绳,查看是否有加載這個類颂碘,如果沒有這個類,先執(zhí)行類加載椅挣,類加載完成之后凭涂,給這個對象在堆中分配內(nèi)存,對象的內(nèi)存大小在類加載完成之后就會確定贴妻,假如這個堆中的內(nèi)存是整齊的切油,占用的在一邊,空閑的在一邊名惩,中間放著一個指針作為分界點的指示器澎胡,分配內(nèi)存就是將指針向空閑區(qū)域移動對象內(nèi)存大小,這種分配叫做指針碰撞娩鹉,假如堆中的內(nèi)存不是整齊的攻谁,那么虛擬機就會維護一個列表,用于記錄內(nèi)存的使用情況弯予,在分配內(nèi)存的時候戚宦,會在列表中尋找一個足夠大的內(nèi)存分配給這個對象,這種方式叫做空閑列表锈嫩,使用哪種分配方式由java虛擬機堆是否規(guī)整決定受楼,是否規(guī)整由采用的垃圾采集器決定,使用serial呼寸,parnew等帶有compact過程的收集器時候艳汽,系統(tǒng)采用的分配算法是指針碰撞,使用cms這種基于mark-sweep算法的收集器的時候对雪,采用空閑列表河狐,
new對象是很頻繁的事,在并發(fā)下不是安全的,再給a分配內(nèi)存的時候馋艺,指針還未修改栅干,對象b使用l這個指針分配內(nèi)存,解決方案:一種是對分配內(nèi)存空間的動作進行同步處理捐祠,虛擬機采用CAS配上失敗重試的方式保證更新操作的原子性碱鳞,另一種是把內(nèi)存分配的動作按照線程劃分在不同的空間之中進行,
內(nèi)存分配完成之后雏赦,虛擬機需要將分配到的內(nèi)存空間都初始化為0劫笙,不包括對象頭,虛擬機對對象進行必要的設(shè)置星岗,例如這個對象是哪個類的實例填大,如何得到類的元數(shù)據(jù)信息,對象的哈希嗎俏橘,對象的GC分代年齡信息等允华,這些信息放在對象的對象頭中
2、對象的內(nèi)存分配
對象在內(nèi)存中存儲寥掐,分為三個部分靴寂,對象頭,實例數(shù)據(jù)召耘,對齊方式
- 對象頭
對象頭分為兩個部分百炬,一是存儲對象的運行時數(shù)據(jù),GC年齡污它,哈希碼剖踊,鎖狀態(tài)標(biāo)志,線程持有的鎖衫贬,偏向線程ID德澈,偏向時間戳等,另一部分是存放他的類元數(shù)據(jù)指針固惯,虛擬機通過這個確定這個實例是哪個類的實例梆造,也并不是所有的虛擬機實現(xiàn)都必須在對象數(shù)據(jù)上保留類型指針,如果對象是數(shù)組葬毫,對象頭中還需要有一塊記錄數(shù)組長度的數(shù)據(jù)镇辉,因為虛擬機可以通過普通java對象的元數(shù)據(jù)確定java對象的大小,但是從數(shù)組的元數(shù)據(jù)中卻無法確定數(shù)組的大小 - 實例數(shù)據(jù)
用于存儲對象真正的信息供常,定義的各種字段摊聋,相同寬度的字段會被分配到一起,所以子類和父類的字段有可能會在一起 - 對齊填充
由于對象的大小必須是8的倍數(shù)栈暇,對象頭正好是8的倍數(shù),而實例數(shù)據(jù)并不一定箍镜,所以需要這個來站位源祈,補充
3煎源、對象的訪問定位
如何訪問到堆中的實例呢,在棧中存放著實例的引用reference香缺,有兩種方式手销,一種是句柄,一種是指針引用
-
句柄
在堆中會分配一個句柄池图张,存放實例的地址锋拖,而reference中存放的就是對象的句柄地址,
-
指針訪問
reference直接存儲對象在堆中的地址
句柄的好處祸轮,當(dāng)實例需要被挪位置的時候兽埃,垃圾回收的時候會有,我們只需要改變句柄中的對象地址适袜,不要改引用柄错, 而使用指針訪問,就需要改變reference苦酱,但是好處是少了一次指針尋找售貌,速度快
3、實戰(zhàn)OutOfMemoryError
1疫萤、堆內(nèi)存溢出
當(dāng)我們指定了一定大小的堆內(nèi)存颂跨,并一直new 對象,就會發(fā)生堆內(nèi)存溢出的錯誤扯饶,因為對象不能被回收恒削,內(nèi)存不夠
2、Stack Overflow
當(dāng)我們不停的遞歸調(diào)用方法帝际,造成棧的深度不夠蔓同,即會發(fā)生此錯誤
更多內(nèi)容請看后續(xù),
QQ交流群:552113611