廣州開(kāi)發(fā)區(qū)醫(yī)院南崗院區(qū)莆田seo推廣公司
背景介紹
當(dāng)JVM類(lèi)加載器加載完字節(jié)碼文件之后,會(huì)交給執(zhí)行引擎執(zhí)行,在執(zhí)行的過(guò)程中會(huì)有一塊JVM內(nèi)存區(qū)域來(lái)存放程序運(yùn)行過(guò)程中的數(shù)據(jù),也就是我們圖中放的運(yùn)行時(shí)數(shù)據(jù)區(qū),那這一塊運(yùn)行時(shí)數(shù)據(jù)區(qū)究竟幫我們做了哪些工作?我們常說(shuō)的線上內(nèi)存泄漏和內(nèi)存溢出是因?yàn)槭裁?#xff1f;我們今兒來(lái)揭開(kāi)看看它神秘的面紗。
過(guò)程
在講述運(yùn)行時(shí)數(shù)據(jù)區(qū)有哪些部分之前先討論以下對(duì)象的創(chuàng)建流程
一、對(duì)象創(chuàng)建流程
在上一篇文章中已經(jīng)講述了類(lèi)加載的過(guò)程,虛擬機(jī)需要給new的新對(duì)象分配內(nèi)存空間,重點(diǎn)來(lái)說(shuō)說(shuō)它的下一步——分配內(nèi)存
在分配內(nèi)存的時(shí)候有兩種方式:
1、指針碰撞
假設(shè)Java堆內(nèi)存規(guī)整,所有的內(nèi)存占用是連續(xù)空間,通過(guò)一個(gè)指針將已經(jīng)使用和未使用的空間隔開(kāi),指針作為臨界。
2、空閑列表
假設(shè)Java堆內(nèi)存不規(guī)整,內(nèi)存占用是零散的,此時(shí)JVM通過(guò)一個(gè)空閑列表(Free List)維護(hù)空閑內(nèi)存信息,里面記錄了哪些內(nèi)存空間是可用的。再次分配新對(duì)象的時(shí)候直接從空閑列表中找哪塊空間可以使用進(jìn)行分配即可
但是在并發(fā)情況下可能會(huì)出現(xiàn)線程安全的問(wèn)題,對(duì)象1和對(duì)象2同時(shí)拿到指針,對(duì)象1在分配完內(nèi)存空間之后對(duì)象2也會(huì)分配內(nèi)存空間,對(duì)象2就會(huì)把對(duì)象1的空間覆蓋,導(dǎo)致數(shù)據(jù)被覆蓋。那怎么解決這個(gè)問(wèn)題呢?
兩種方案解決線程不安全:
1、CAS:自旋鎖不斷重試
2、TLAB:在堆內(nèi)存給每個(gè)線程預(yù)先分配一塊內(nèi)存,線程中每次要開(kāi)辟空間就在預(yù)分配的內(nèi)存中開(kāi)辟
下面就進(jìn)入我們的正題——內(nèi)存管理
- 線程共有:堆、方法區(qū)
- 線程私有:程序計(jì)數(shù)器、Java虛擬機(jī)棧、本地方法棧
二、內(nèi)存管理
1、 程序計(jì)數(shù)器(PC)
上官方百度百科介紹:
作用:存放當(dāng)前線程執(zhí)行的下一條指令地址。
在多線程環(huán)境下線程之間會(huì)涉及到線程切換問(wèn)題,為了保證線程切換之后還能繼續(xù)按照上一次切換的位置繼續(xù)執(zhí)行,PC會(huì)進(jìn)行指令的記錄。PC是線程獨(dú)有的,不可共享
2、Java虛擬機(jī)棧
作用:
每個(gè)線程在創(chuàng)建時(shí)都會(huì)創(chuàng)建一個(gè)虛擬機(jī)棧,保存了一個(gè)一個(gè)的棧幀 。針對(duì)棧幀我們來(lái)具體說(shuō)說(shuō),下圖為Java虛擬機(jī)中棧幀的內(nèi)部結(jié)構(gòu),每執(zhí)行一個(gè)方法都會(huì)創(chuàng)建一個(gè)棧幀(一個(gè)方法對(duì)應(yīng)一個(gè)棧幀) ,而棧幀中包含了四部分:局部變量表、操作數(shù)棧、方法返回地址、動(dòng)態(tài)鏈接,而每一個(gè)棧幀執(zhí)行的過(guò)程也是入棧、出棧的過(guò)程。
包括:
- 操作數(shù)棧(Operand Stack):用來(lái)在執(zhí)行字節(jié)碼指令過(guò)程中用來(lái)計(jì)算的
- 局部變量表(LocalVariables):在方法執(zhí)行過(guò)程中實(shí)時(shí)記錄每個(gè)局部變量對(duì)應(yīng)的值
- 方法返回地址(Return Address):地址
- 動(dòng)態(tài)鏈接(Dynamic Linking):符號(hào)引用轉(zhuǎn)換為調(diào)用方法的直接引用
特點(diǎn):
- 線程私有
- 遵守棧FIFO規(guī)則,方法開(kāi)始執(zhí)行棧幀入棧,方法執(zhí)行完棧幀彈出,所以虛擬機(jī)不需要垃圾回收
存在的問(wèn)題:
- 如果線程太多了,但是沒(méi)有足夠空間創(chuàng)建虛擬機(jī)棧,會(huì)發(fā)生棧溢出
- 方法調(diào)用層次太多,可能出現(xiàn)StackOverflowError
3、本地方法棧(Native Method Stacks)
作用:存儲(chǔ)Native方法
4、方法區(qū)(Method Area)
作用:
存儲(chǔ)被Java虛擬機(jī)加載過(guò)后的class類(lèi)信息、常量、靜態(tài)變量、編譯后的代碼
不知道大家是否還記得上一篇分享中講到的類(lèi)加載過(guò)程,其中加載這一步會(huì)通過(guò)類(lèi)全限定名加載成class類(lèi)對(duì)象,其中就會(huì)把class信息、靜態(tài)變量、常量等信息加載到方法區(qū),看下面這張圖
5、堆(Heap)
所有線程共享的一塊內(nèi)存區(qū)域,在new對(duì)象的時(shí)候會(huì)在Heap分配內(nèi)存空間??梢约?xì)分為:
- 年輕代和老年代,對(duì)應(yīng)的比例為2:1 ,新生代存放朝生夕死的對(duì)象,老年代存放生命周期較長(zhǎng)的對(duì)象
- 年輕代又可以分為Eden、From Survivor、ToSurvivor三個(gè)區(qū),對(duì)應(yīng)的比例為:8:1:1(默認(rèn)情況下,可以通過(guò)-XX:SurvivorRatio來(lái)調(diào)整)
那三個(gè)區(qū)域中對(duì)象是如何進(jìn)行流轉(zhuǎn)的呢?我們具體來(lái)看一下
1、在最開(kāi)始講述對(duì)象創(chuàng)建流程中包含了一步是分配內(nèi)存,就是通過(guò)指針碰撞或空間散列在Heap中分配內(nèi)存,新new對(duì)象的對(duì)象JVM會(huì)默認(rèn)優(yōu)先分配在Eden區(qū),當(dāng)Eden區(qū)空間逐漸減少(可以默認(rèn)配置Eden空間容量)的時(shí)候,就會(huì)觸發(fā)Young GC來(lái)清理,Eden區(qū)對(duì)象就會(huì)放入Survivor區(qū);
2、Survivor區(qū)每次分配內(nèi)存只使用其中一塊,Eden和Survivor存活對(duì)象會(huì)復(fù)制到另一塊Survivor區(qū)中,Eden和原來(lái)的Survivor區(qū)對(duì)象會(huì)被清理掉。(這也是為什么圖中我只在其中一個(gè)Survivor區(qū)畫(huà)了對(duì)象,另一塊Survivor區(qū)沒(méi)畫(huà)的原因)
3、在對(duì)象頭中記錄了對(duì)象迭代的年齡(年齡計(jì)數(shù)器),當(dāng)進(jìn)入Survivor區(qū)開(kāi)始每YoungGC一次年齡就會(huì)+1,當(dāng)年齡達(dá)到15的時(shí)候就會(huì)進(jìn)入老年代
從圖上我們看到進(jìn)入老年代的條件遠(yuǎn)不止年齡>=15這一個(gè),對(duì)象會(huì)進(jìn)入老年代的方式共有四個(gè):
- 長(zhǎng)期存活:對(duì)象頭中記錄了對(duì)象迭代的年齡,每次迭代都會(huì)—+1,當(dāng)年齡達(dá)到15(默認(rèn))
- 超大對(duì)象:占用大量連續(xù)空間
- 動(dòng)態(tài)年齡判斷:servivor中相同年齡對(duì)象的總和>survivor空間一半
- 空間分配擔(dān)保:Young GC后,新生代有大量對(duì)象對(duì)象存活,需要老年代分配擔(dān)保
三、垃圾回收
垃圾回收是什么?
清理不再使用的對(duì)象,釋放內(nèi)存空間
為什么要進(jìn)行垃圾回收?
如果不清理這些垃圾對(duì)象,那么它們會(huì)一直占用著內(nèi)存,而不能給其他對(duì)象是用,最終垃圾對(duì)象越來(lái)越多,就會(huì)出現(xiàn)OOM
什么樣的對(duì)象是垃圾?
JVM沒(méi)有任何引用指向它的對(duì)象
如何判斷對(duì)象是垃圾?
1、引用計(jì)數(shù)法
每個(gè)對(duì)象都保存一個(gè)引用計(jì)數(shù)器屬性,用戶記錄對(duì)象被引用的次數(shù),每被引用一次計(jì)數(shù)器值就+1;當(dāng)引用失效時(shí)就-1。當(dāng)計(jì)數(shù)器為0則表示是垃圾對(duì)象
2、可達(dá)性分析
從GC Roots開(kāi)始,遍歷,一層一層的往下級(jí)找引用對(duì)象,找到的對(duì)象就是存活對(duì)象,沒(méi)找到的就是垃圾對(duì)象
垃圾回收的三種方式分別是哪些?
1、標(biāo)記-清除算法(Mark-Sweep)
標(biāo)記:標(biāo)記出未引用對(duì)象
清除:回收所有被標(biāo)記的未引用對(duì)象
問(wèn)題:
- 如果堆內(nèi)包含了大量的對(duì)象都是需要被回收,這時(shí)會(huì)執(zhí)行大量標(biāo)記和清除操作,導(dǎo)致執(zhí)行效率降低
- 內(nèi)存空間碎片化,標(biāo)記和清除后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,導(dǎo)致在之后在給對(duì)象分配內(nèi)存空間的時(shí)候因?yàn)閮?nèi)存不足而再次觸發(fā)Young GC
2、標(biāo)記-復(fù)制算法(Mark-Copying)
基于標(biāo)記-清除算法,解決碎片化問(wèn)題。上文中我也提到了新生代中Survivor分為了From、To兩個(gè)區(qū)域,每次只使用其中一塊,當(dāng)其中一塊內(nèi)存用完了,會(huì)將存活的對(duì)象復(fù)制到另一塊區(qū)域上,然后清理掉使用的survivor區(qū)域,保證內(nèi)存區(qū)域連續(xù)可用。
問(wèn)題:
空間一分為2,利用率低,空間浪費(fèi)
3、標(biāo)記-整理(Mark-Compact)
基于標(biāo)記-清除算法,解決內(nèi)存碎片和空間浪費(fèi)問(wèn)題。將存活的對(duì)象向一端移動(dòng),清除標(biāo)記的垃圾對(duì)象,保證區(qū)域連續(xù)可用
問(wèn)題:
內(nèi)存變動(dòng)大,當(dāng)對(duì)象位置移動(dòng)相應(yīng)的引用地址也會(huì)變動(dòng)
如何選用使用什么回收算法呢?
分代回收:基于heap各個(gè)區(qū)域?qū)ο笊芷趤?lái)看,每個(gè)區(qū)域采用不同的回收算法:
- 新生代:標(biāo)記-復(fù)制
- 老年代:標(biāo)記-清理/標(biāo)記-整理