原文出處:segmentfault.com/a/1190000014395186
工作之余,總結(jié)一下JVM相關(guān)知識(shí)。
Java運(yùn)行時(shí)數(shù)據(jù)區(qū):Java虛擬機(jī)在執(zhí)行Java程序的過程中會(huì)將其管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域,這些區(qū)域有各自的用途、創(chuàng)建和銷毀的時(shí)間,有些區(qū)域隨虛擬機(jī)進(jìn)程的啟動(dòng)而存在,有些區(qū)域則是依賴用戶線程的啟動(dòng)和結(jié)束來(lái)建立和銷毀。Java虛擬機(jī)所管理的內(nèi)存包括以下幾個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū)域,如圖:
1、程序計(jì)數(shù)器:指向當(dāng)前線程正在執(zhí)行的字節(jié)碼指令。線程私有的。
2、虛擬機(jī)棧:虛擬機(jī)棧是Java執(zhí)行方法的內(nèi)存模型。每個(gè)方法被執(zhí)行的時(shí)候,都會(huì)創(chuàng)建一個(gè)棧幀,把棧幀壓人棧,當(dāng)方法正常返回或者拋出未捕獲的異常時(shí),棧幀就會(huì)出棧。 (1)棧幀:棧幀存儲(chǔ)方法的相關(guān)信息,包含局部變量數(shù)表、返回值、操作數(shù)棧、動(dòng)態(tài)鏈接 a、局部變量表:包含了方法執(zhí)行過程中的所有變量。局部變量數(shù)組所需要的空間在編譯期間完成分配,在方法運(yùn)行期間不會(huì)改變局部變量數(shù)組的大小。 b、返回值:如果有返回值的話,壓入調(diào)用者棧幀中的操作數(shù)棧中,并且把PC的值指向 方法調(diào)用指令 后面的一條指令地址。 c、操作數(shù)棧:操作變量的內(nèi)存模型。操作數(shù)棧的最大深度在編譯的時(shí)候已經(jīng)確定(寫入方法區(qū)code屬性的max_stacks項(xiàng)中)。操作數(shù)棧的的元素可以是任意Java類型,包括long和double,32位數(shù)據(jù)占用棧空間為1,64位數(shù)據(jù)占用2。方法剛開始執(zhí)行的時(shí)候,棧是空的,當(dāng)方法執(zhí)行過程中,各種字節(jié)碼指令往棧中存取數(shù)據(jù)。 d、動(dòng)態(tài)鏈接:每個(gè)棧幀都持有在運(yùn)行時(shí)常量池中該棧幀所屬方法的引用,持有這個(gè)引用是為了支持方法調(diào)用過程中的動(dòng)態(tài)鏈接。 (2)線程私有
3、本地方法棧: (1)調(diào)用本地native的內(nèi)存模型 (2)線程獨(dú)享。
4、方法區(qū):用于存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯后的代碼等數(shù)據(jù) (1)線程共享的 (2)運(yùn)行時(shí)常量池: A、是方法區(qū)的一部分 B、存放編譯期生成的各種字面量和符號(hào)引用 C、Class文件中除了存有類的版本、字段、方法、接口等描述信息,還有一項(xiàng)是常量池,存有這個(gè)類的 編譯期生成的各種字面量和符號(hào)引用,這部分內(nèi)容將在類加載后,存放到方法區(qū)的運(yùn)行時(shí)常量池中。
5、堆(Heap):Java對(duì)象存儲(chǔ)的地方 (1)Java堆是虛擬機(jī)管理的內(nèi)存中最大的一塊 (2)Java堆是所有線程共享的區(qū)域 (3)在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建 (4)此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例,幾乎所有對(duì)象實(shí)例都在這里分配內(nèi)存。存放new生成的對(duì)象和數(shù)組 (5)Java堆是垃圾收集器管理的內(nèi)存區(qū)域,因此很多時(shí)候稱為“GC堆” JMM Java內(nèi)存模型:1、 Java的并發(fā)采用“共享內(nèi)存”模型,線程之間通過讀寫內(nèi)存的公共狀態(tài)進(jìn)行通訊。多個(gè)線程之間是不能通過直接傳遞數(shù)據(jù)交互的,它們之間交互只能通過共享變量實(shí)現(xiàn)。
2、 主要目的是定義程序中各個(gè)變量的訪問規(guī)則。
3、 Java內(nèi)存模型規(guī)定所有變量都存儲(chǔ)在主內(nèi)存中,每個(gè)線程還有自己的工作內(nèi)存。 (1) 線程的工作內(nèi)存中保存了被該線程使用到的變量的拷貝(從主內(nèi)存中拷貝過來(lái)),線程對(duì)變量的所有操作都必須在工作內(nèi)存中執(zhí)行,而不能直接訪問主內(nèi)存中的變量。 (2) 不同線程之間無(wú)法直接訪問對(duì)方工作內(nèi)存的變量,線程間變量值的傳遞都要通過主內(nèi)存來(lái)完成。 (3) 主內(nèi)存主要對(duì)應(yīng)Java堆中實(shí)例數(shù)據(jù)部分。工作內(nèi)存對(duì)應(yīng)于虛擬機(jī)棧中部分區(qū)域。 4、Java線程之間的通信由內(nèi)存模型JMM(Java Memory Model)控制。 (1)JMM決定一個(gè)線程對(duì)變量的寫入何時(shí)對(duì)另一個(gè)線程可見。 (2)線程之間共享變量存儲(chǔ)在主內(nèi)存中 (3)每個(gè)線程有一個(gè)私有的本地內(nèi)存,里面存儲(chǔ)了讀/寫共享變量的副本。 (4)JMM通過控制每個(gè)線程的本地內(nèi)存之間的交互,來(lái)為程序員提供內(nèi)存可見性保證。
5、可見性、有序性: (1)當(dāng)一個(gè)共享變量在多個(gè)本地內(nèi)存中有副本時(shí),如果一個(gè)本地內(nèi)存修改了該變量的副本,其他變量應(yīng)該能夠看到修改后的值,此為可見性。 (2)保證線程的有序執(zhí)行,這個(gè)為有序性。(保證線程安全)
6、內(nèi)存間交互操作: (1)lock(鎖定):作用于主內(nèi)存的變量,把一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)占狀態(tài)。 (2)unlock(解鎖):作用于主內(nèi)存的變量,把一個(gè)處于鎖定狀態(tài)的變量釋放出來(lái),釋放后的變量才可以被其他線程鎖定。 (3)read(讀?。鹤饔糜谥鲀?nèi)存變量,把主內(nèi)存的一個(gè)變量讀取到工作內(nèi)存中。 (4)load(載入):作用于工作內(nèi)存,把read操作讀取到工作內(nèi)存的變量載入到工作內(nèi)存的變量副本中 (5)use(使用):作用于工作內(nèi)存的變量,把工作內(nèi)存中的變量值傳遞給一個(gè)執(zhí)行引擎。 (6)assign(賦值):作用于工作內(nèi)存的變量。把執(zhí)行引擎接收到的值賦值給工作內(nèi)存的變量。 (7)store(存儲(chǔ)):把工作內(nèi)存的變量的值傳遞給主內(nèi)存 (8)write(寫入):把store操作的值入到主內(nèi)存的變量中
6.1、注意: (1)不允許read、load、store、write操作之一單獨(dú)出現(xiàn) (2)不允許一個(gè)線程丟棄assgin操作 (3)不允許一個(gè)線程不經(jīng)過assgin操作,就把工作內(nèi)存中的值同步到主內(nèi)存中 (4)一個(gè)新的變量只能在主內(nèi)存中生成 (5)一個(gè)變量同一時(shí)刻只允許一條線程對(duì)其進(jìn)行l(wèi)ock操作。但lock操作可以被同一條線程執(zhí)行多次,只有執(zhí)行相同次數(shù)的unlock操作,變量才會(huì)解鎖 (6)如果對(duì)一個(gè)變量進(jìn)行l(wèi)ock操作,將會(huì)清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個(gè)變量前,需要重新執(zhí)行l(wèi)oad或者assgin操作初始化變量的值。 (7)如果一個(gè)變量沒有被鎖定,不允許對(duì)其執(zhí)行unlock操作,也不允許unlock一個(gè)被其他線程鎖定的變量 (8)對(duì)一個(gè)變量執(zhí)行unlock操作之前,需要將該變量同步回主內(nèi)存中 堆的內(nèi)存劃分: Java堆的內(nèi)存劃分如圖所示,分別為年輕代、Old Memory(老年代)、Perm(永久代)。其中在Jdk1.8中,永久代被移除,使用MetaSpace代替。
1、新生代: (1)使用復(fù)制清除算法(Copinng算法),原因是年輕代每次GC都要回收大部分對(duì)象。新生代里面分成一份較大的Eden空間和兩份較小的Survivor空間。每次只使用Eden和其中一塊Survivor空間,然后垃圾回收的時(shí)候,把存活對(duì)象放到未使用的Survivor(劃分出from、to)空間中,清空Eden和剛才使用過的Survivor空間。 (2)分為Eden、Survivor From、Survivor To,比例默認(rèn)為8:1:1 (3)內(nèi)存不足時(shí)發(fā)生Minor GC
2、老年代: (1)采用標(biāo)記-整理算法(mark-compact),原因是老年代每次GC只會(huì)回收少部分對(duì)象。
3、Perm:用來(lái)存儲(chǔ)類的元數(shù)據(jù),也就是方法區(qū)。 (1)Perm的廢除:在jdk1.8中,Perm被替換成MetaSpace,MetaSpace存放在本地內(nèi)存中。原因是永久代進(jìn)場(chǎng)內(nèi)存不夠用,或者發(fā)生內(nèi)存泄漏。 (2)MetaSpace(元空間):元空間的本質(zhì)和永久代類似,都是對(duì)JVM規(guī)范中方法區(qū)的實(shí)現(xiàn)。不過元空間與永久代之間最大的區(qū)別在于:元空間并不在虛擬機(jī)中,而是使用本地內(nèi)存。 4、堆內(nèi)存的劃分在JVM里面的示意圖: GC垃圾回收:一、 判斷對(duì)象是否要回收的方法:可達(dá)性分析法
1、 可達(dá)性分析法:通過一系列“GC Roots”對(duì)象作為起點(diǎn)進(jìn)行搜索,如果在“GC Roots”和一個(gè)對(duì)象之間沒有可達(dá)路徑,則稱該對(duì)象是不可達(dá)的。不可達(dá)對(duì)象不一定會(huì)成為可回收對(duì)象。進(jìn)入DEAD狀態(tài)的線程還可以恢復(fù),GC不會(huì)回收它的內(nèi)存。(把一些對(duì)象當(dāng)做root對(duì)象,JVM認(rèn)為root對(duì)象是不可回收的,并且root對(duì)象引用的對(duì)象也是不可回收的) 2、 以下對(duì)象會(huì)被認(rèn)為是root對(duì)象: (1) 虛擬機(jī)棧(棧幀中本地變量表)中引用的對(duì)象 (2) 方法區(qū)中靜態(tài)屬性引用的對(duì)象 (3) 方法區(qū)中常量引用的對(duì)象 (4) 本地方法棧中Native方法引用的對(duì)象
3、 對(duì)象被判定可被回收,需要經(jīng)歷兩個(gè)階段: (1) 第一個(gè)階段是可達(dá)性分析,分析該對(duì)象是否可達(dá) (2) 第二個(gè)階段是當(dāng)對(duì)象沒有重寫finalize()方法或者finalize()方法已經(jīng)被調(diào)用過,虛擬機(jī)認(rèn)為該對(duì)象不可以被救活,因此回收該對(duì)象。(finalize()方法在垃圾回收中的作用是,給該對(duì)象一次救活的機(jī)會(huì)) 4、 方法區(qū)中的垃圾回收: (1) 常量池中一些常量、符號(hào)引用沒有被引用,則會(huì)被清理出常量池 (2) 無(wú)用的類:被判定為無(wú)用的類,會(huì)被清理出方法區(qū)。判定方法如下: A、 該類的所有實(shí)例被回收 B、 加載該類的ClassLoader被回收 C、 該類的Class對(duì)象沒有被引用 5、 finalize(): (1) GC垃圾回收要回收一個(gè)對(duì)象的時(shí)候,調(diào)用該對(duì)象的finalize()方法。然后在下一次垃圾回收的時(shí)候,才去回收這個(gè)對(duì)象的內(nèi)存。 (2) 可以在該方法里面,指定一些對(duì)象在釋放前必須執(zhí)行的操作。 二、 發(fā)現(xiàn)虛擬機(jī)頻繁full GC時(shí)應(yīng)該怎么辦: (full GC指的是清理整個(gè)堆空間,包括年輕代和永久代) (1) 首先用命令查看觸發(fā)GC的原因是什么 jstat –gccause 進(jìn)程id (2) 如果是System.gc(),則看下代碼哪里調(diào)用了這個(gè)方法 (3) 如果是heap inspection(內(nèi)存檢查),可能是哪里執(zhí)行jmap –histo[:live]命令 (4) 如果是GC locker,可能是程序依賴的JNI庫(kù)的原因 三、常見的垃圾回收算法:
1、Mark-Sweep(標(biāo)記-清除算法): (1)思想:標(biāo)記清除算法分為兩個(gè)階段,標(biāo)記階段和清除階段。標(biāo)記階段任務(wù)是標(biāo)記出所有需要回收的對(duì)象,清除階段就是清除被標(biāo)記對(duì)象的空間。 (2)優(yōu)缺點(diǎn):實(shí)現(xiàn)簡(jiǎn)單,容易產(chǎn)生內(nèi)存碎片 2、Copying(復(fù)制清除算法): (1)思想:將可用內(nèi)存劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)進(jìn)行垃圾回收的時(shí)候了,把其中存活對(duì)象全部復(fù)制到另外一塊中,然后把已使用的內(nèi)存空間一次清空掉。 (2)優(yōu)缺點(diǎn):不容易產(chǎn)生內(nèi)存碎片;可用內(nèi)存空間少;存活對(duì)象多的話,效率低下。 3、Mark-Compact(標(biāo)記-整理算法): (1)思想:先標(biāo)記存活對(duì)象,然后把存活對(duì)象向一邊移動(dòng),然后清理掉端邊界以外的內(nèi)存。 (2)優(yōu)缺點(diǎn):不容易產(chǎn)生內(nèi)存碎片;內(nèi)存利用率高;存活對(duì)象多并且分散的時(shí)候,移動(dòng)次數(shù)多,效率低下 4、分代收集算法:(目前大部分JVM的垃圾收集器所采用的算法): 思想:把堆分成新生代和老年代。(永久代指的是方法區(qū))
(1) 因?yàn)樾律看卫厥斩家厥沾蟛糠謱?duì)象,所以新生代采用Copying算法。新生代里面分成一份較大的Eden空間和兩份較小的Survivor空間。每次只使用Eden和其中一塊Survivor空間,然后垃圾回收的時(shí)候,把存活對(duì)象放到未使用的Survivor(劃分出from、to)空間中,清空Eden和剛才使用過的Survivor空間。 (2) 由于老年代每次只回收少量的對(duì)象,因此采用mark-compact算法。 (3) 在堆區(qū)外有一個(gè)永久代。對(duì)永久代的回收主要是無(wú)效的類和常量 5、GC使用時(shí)對(duì)程序的影響? 垃圾回收會(huì)影響程序的性能,Java虛擬機(jī)必須要追蹤運(yùn)行程序中的有用對(duì)象,然后釋放沒用對(duì)象,這個(gè)過程消耗處理器時(shí)間
6、幾種不同的垃圾回收類型: (1)Minor GC:從年輕代(包括Eden、Survivor區(qū))回收內(nèi)存。 A、當(dāng)JVM無(wú)法為一個(gè)新的對(duì)象分配內(nèi)存的時(shí)候,越容易觸發(fā)Minor GC。所以分配率越高,內(nèi)存越來(lái)越少,越頻繁執(zhí)行Minor GC B、執(zhí)行Minor GC操作的時(shí)候,不會(huì)影響到永久代(Tenured)。從永久代到年輕代的引用,被當(dāng)成GC Roots,從年輕代到老年代的引用在標(biāo)記階段直接被忽略掉。
(2)Major GC:清理整個(gè)老年代,當(dāng)eden區(qū)內(nèi)存不足時(shí)觸發(fā)。 (3)Full GC:清理整個(gè)堆空間,包括年輕代和老年代。當(dāng)老年代內(nèi)存不足時(shí)觸發(fā) HotSpot 虛擬機(jī)詳解:1、 Java對(duì)象創(chuàng)建過程: (1)虛擬機(jī)遇到一條new指令時(shí),首先檢查這個(gè)指令的參數(shù)能否在常量池中定位到一個(gè)類的符號(hào)引用,并檢查這個(gè)符號(hào)引用代表的類是否已經(jīng)加載、連接和初始化。如果沒有,就執(zhí)行該類的加載過程。 (2)為該對(duì)象分配內(nèi)存。 A、假設(shè)Java堆是規(guī)整的,所有用過的內(nèi)存放在一邊,空閑的內(nèi)存放在另外一邊,中間放著一個(gè)指針作為分界點(diǎn)的指示器。那分配內(nèi)存只是把指針向空閑空間那邊挪動(dòng)與對(duì)象大小相等的距離,這種分配稱為“指針碰撞” B、假設(shè)Java堆不是規(guī)整的,用過的內(nèi)存和空閑的內(nèi)存相互交錯(cuò),那就沒辦法進(jìn)行“指針碰撞”。虛擬機(jī)通過維護(hù)一個(gè)列表,記錄哪些內(nèi)存塊是可用的,在分配的時(shí)候找出一塊足夠大的空間分配給對(duì)象實(shí)例,并更新表上的記錄。這種分配方式稱為“空閑列表“。 C、使用哪種分配方式由Java堆是否規(guī)整決定。Java堆是否規(guī)整由所采用的垃圾收集器是否帶有壓縮整理功能決定。 D、分配對(duì)象保證線程安全的做法:虛擬機(jī)使用CAS失敗重試的方式保證更新操作的原子性。(實(shí)際上還有另外一種方案:每個(gè)線程在Java堆中預(yù)先分配一小塊內(nèi)存,稱為本地線程分配緩沖,TLAB。哪個(gè)線程要分配內(nèi)存,就在哪個(gè)線程的TLAB上分配,只有TLAB用完并分配新的TLAB時(shí),才進(jìn)行同步鎖定。虛擬機(jī)是否使用TLAB,由-XX:+/-UseTLAB參數(shù)決定) (3)虛擬機(jī)為分配的內(nèi)存空間初始化為零值(默認(rèn)值) (4)虛擬機(jī)對(duì)對(duì)象進(jìn)行必要的設(shè)置,例如這個(gè)對(duì)象是哪個(gè)類的實(shí)例、如何才能找到對(duì)象的元數(shù)據(jù)信息、對(duì)象的Hash碼、對(duì)象的GC分代年齡等信息。這些信息存放在對(duì)象的對(duì)象頭中。 (5) 執(zhí)行<init>方法,把對(duì)象按照程序員的意愿進(jìn)行初始化。 2、 對(duì)象的定位訪問的方式(通過引用如何去定位到堆上的具體對(duì)象的位置): (1)句柄:使用句柄的方式,Java堆中將會(huì)劃分出一塊內(nèi)存作為作為句柄池,引用中存儲(chǔ)的就是對(duì)象的句柄的地址。而句柄中包含了對(duì)象實(shí)例數(shù)據(jù)和對(duì)象類型數(shù)據(jù)的地址。
(2)直接指針:使用直接指針的方式,引用中存儲(chǔ)的就是對(duì)象的地址。Java堆對(duì)象的布局必須必須考慮如何去訪問對(duì)象類型數(shù)據(jù)。
(3)兩種方式各有優(yōu)點(diǎn): A、使用句柄訪問的好處是引用中存放的是穩(wěn)定的句柄地址,當(dāng)對(duì)象被移動(dòng)(比如說垃圾回收時(shí)移動(dòng)對(duì)象),只會(huì)改變句柄中實(shí)例數(shù)據(jù)指針,而引用本身不會(huì)被修改。 B、使用直接指針,節(jié)省了一次指針定位的時(shí)間開銷。 3、HotSpot的GC算法實(shí)現(xiàn): (1)HotSpot怎么快速找到GC Root? HotSpot使用一組稱為OopMap的數(shù)據(jù)結(jié)構(gòu)。在類加載完成的時(shí)候,HotSpot就把對(duì)象內(nèi)什么偏移量上是什么類型的數(shù)據(jù)計(jì)算出來(lái),在JIT編譯過程中,也會(huì)在棧和寄存器中哪些位置是引用。這樣子,在GC掃描的時(shí)候,就可以直接知道哪些是可達(dá)對(duì)象了。 (2)安全點(diǎn): A、HotSpot只在特定的位置生成OopMap,這些位置稱為安全點(diǎn)。 B、程序執(zhí)行過程中并非所有地方都可以停下來(lái)開始GC,只有在到達(dá)安全點(diǎn)是才可以暫停。 C、安全點(diǎn)的選定基本上以“是否具有讓程序長(zhǎng)時(shí)間執(zhí)行“的特征選定的。比如說方法調(diào)用、循環(huán)跳轉(zhuǎn)、異常跳轉(zhuǎn)等。具有這些功能的指令才會(huì)產(chǎn)生Safepoint。 (3)中斷方式: A、搶占式中斷:在GC發(fā)生時(shí),首先把所有線程中斷,如果發(fā)現(xiàn)有線程不在安全點(diǎn)上,就恢復(fù)線程,讓它跑到安全點(diǎn)上。 B、主動(dòng)式中斷:GC需要中斷線程時(shí),不直接對(duì)線程操作,僅僅設(shè)置一個(gè)標(biāo)志,各個(gè)線程執(zhí)行時(shí)主動(dòng)去輪詢這個(gè)標(biāo)志,當(dāng)發(fā)現(xiàn)中斷標(biāo)記為真就自己中斷掛起。輪詢標(biāo)記的地方和安全點(diǎn)是重合的。
(5)安全區(qū)域:一段代碼片段中,對(duì)象的引用關(guān)系不會(huì)發(fā)生變化,在這個(gè)區(qū)域中任何地方開始GC都是安全的。在線程進(jìn)入安全區(qū)域時(shí),它首先標(biāo)志自己已經(jīng)進(jìn)入安全區(qū)域,在這段時(shí)間里,當(dāng)JVM發(fā)起GC時(shí),就不用管進(jìn)入安全區(qū)域的線程了。在線程將要離開安全區(qū)域時(shí),它檢查系統(tǒng)是否完成了GC過程,如果完成了,它就繼續(xù)前行。否則,它就必須等待直到收到可以離開安全區(qū)域的信號(hào)。 4、 GC時(shí)為什么要停頓所有Java線程? 因?yàn)镚C先進(jìn)行可達(dá)性分析。可達(dá)性分析是判斷GC Root對(duì)象到其他對(duì)象是否可達(dá),假如分析過程中對(duì)象的引用關(guān)系在不斷變化,分析結(jié)果的準(zhǔn)確性就無(wú)法得到保證。 5、 CMS收集器: (1)一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器。 (2)一般用于互聯(lián)網(wǎng)站或者B/S系統(tǒng)的服務(wù)端 (3)基于標(biāo)記-清除算法的實(shí)現(xiàn),不過更為復(fù)雜,整個(gè)過程為4個(gè)步驟: A、初始標(biāo)記:標(biāo)記GC Root能直接引用的對(duì)象 B、并發(fā)標(biāo)記:利用多線程對(duì)每個(gè)GC Root對(duì)象進(jìn)行tracing搜索,在堆中查找其下所有能關(guān)聯(lián)到的對(duì)象。 C、重新標(biāo)記:為了修正并發(fā)標(biāo)記期間,用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)志產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄。 D、并發(fā)清除:利用多個(gè)線程對(duì)標(biāo)記的對(duì)象進(jìn)行清除
(4)由于耗時(shí)最長(zhǎng)的并發(fā)標(biāo)記和并發(fā)清除操作都是用戶線程一起工作,所以總體來(lái)說,CMS的內(nèi)存回收工作是和用戶線程一起并發(fā)執(zhí)行的。 (5)缺點(diǎn): A、對(duì)CPU資源占用比較多??赡芤?yàn)檎加靡徊糠諧PU資源導(dǎo)致應(yīng)用程序響應(yīng)變慢。 B、CMS無(wú)法處理浮動(dòng)垃圾。在并發(fā)清除階段,用戶程序繼續(xù)運(yùn)行,可能產(chǎn)生新的內(nèi)存垃圾,這一部分垃圾出現(xiàn)在標(biāo)記過程之后,因此,CMS無(wú)法清除。這部分垃圾稱為“浮動(dòng)垃圾“ C、需要預(yù)留一部分內(nèi)存,在垃圾回收時(shí),給用戶程序使用。 D、基于標(biāo)記-清除算法,容易產(chǎn)生大量?jī)?nèi)存碎片,導(dǎo)致full GC(full GC進(jìn)行內(nèi)存碎片的整理)
6、 對(duì)象頭部分的內(nèi)存布局:HotSpot的對(duì)象頭分為兩部分,第一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),比如哈希碼、GC分代年齡等。另外一部分用于指向方法區(qū)對(duì)象類型數(shù)據(jù)的指針。 7、 偏向鎖:偏向鎖偏向于第一個(gè)獲取它的線程,如果在接下來(lái)的執(zhí)行過程,沒有其他線程獲取該鎖,則持有偏向鎖的線程永遠(yuǎn)不需要同步。(當(dāng)一個(gè)線程獲取偏向鎖,它每次進(jìn)入這個(gè)鎖相關(guān)的同步塊,虛擬機(jī)不在進(jìn)行任何同步操作。當(dāng)有另外一個(gè)線程嘗試獲取這個(gè)鎖時(shí),偏向模式宣告結(jié)束) JVM優(yōu)化:1、一般來(lái)說,當(dāng)survivor區(qū)不夠大或者占用量達(dá)到50%,就會(huì)把一些對(duì)象放到老年區(qū)。通過設(shè)置合理的eden區(qū),survivor區(qū)及使用率,可以將年輕對(duì)象保存在年輕代,從而避免full GC,使用-Xmn 設(shè)置年輕代的大小 2、對(duì)于占用內(nèi)存比較多的大對(duì)象,一般會(huì)選擇在老年代分配內(nèi)存。如果在年輕代給大對(duì)象分配內(nèi)存,年輕代內(nèi)存不夠了,就要在eden區(qū)移動(dòng)大量對(duì)象到老年代,然后這些移動(dòng)的對(duì)象可能很快消亡,因此導(dǎo)致full GC。通過設(shè)置參數(shù):-XX:PetenureSizeThreshold=1000000 ,單位為B,標(biāo)明對(duì)象大小超過1M時(shí),在老年代(tenured)分配內(nèi)存空間。 3、一般情況下,年輕對(duì)象放在eden區(qū),當(dāng)?shù)谝淮蜧C后,如果對(duì)象還存活,放到survivor區(qū),此后,每GC一次,年齡增加1,當(dāng)對(duì)象的年齡達(dá)到閾值,就被放到tenured老年區(qū)。這個(gè)閾值可以同構(gòu)-XX:MaxTenuringThreshold 設(shè)置。如果想讓對(duì)象留在年輕代,可以設(shè)置比較大的閾值。 4、設(shè)置最小堆和最大堆:-Xmx 和-Xms 穩(wěn)定的堆大小堆垃圾回收是有利的,獲得一個(gè)穩(wěn)定的堆大小的方法是設(shè)置-Xms和-Xmx的值一樣,即最大堆和最小堆一樣,如果這樣子設(shè)置,系統(tǒng)在運(yùn)行時(shí)堆大小理論上是恒定的,穩(wěn)定的堆空間可以減少GC次數(shù),因此,很多服務(wù)端都會(huì)將這兩個(gè)參數(shù)設(shè)置為一樣的數(shù)值。穩(wěn)定的堆大小雖然減少GC次數(shù),但是增加每次GC的時(shí)間,因?yàn)槊看蜧C要把堆的大小維持在一個(gè)區(qū)間內(nèi)。 5、一個(gè)不穩(wěn)定的堆并非毫無(wú)用處。在系統(tǒng)不需要使用大內(nèi)存的時(shí)候,壓縮堆空間,使得GC每次應(yīng)對(duì)一個(gè)較小的堆空間,加快單次GC次數(shù)?;谶@種考慮,JVM提供兩個(gè)參數(shù),用于壓縮和擴(kuò)展堆空間。 (1)-XX:MinHeapFreeRatio 參數(shù)用于設(shè)置堆空間的最小空閑比率。默認(rèn)值是40,當(dāng)堆空間的空閑內(nèi)存比率小于40,JVM便會(huì)擴(kuò)展堆空間 (2)-XX:MaxHeapFreeRatio 參數(shù)用于設(shè)置堆空間的最大空閑比率。默認(rèn)值是70, 當(dāng)堆空間的空閑內(nèi)存比率大于70,JVM便會(huì)壓縮堆空間。 (3)當(dāng)-Xmx和-Xmx相等時(shí),上面兩個(gè)參數(shù)無(wú)效 6、通過增大吞吐量提高系統(tǒng)性能,可以通過設(shè)置并行垃圾回收收集器。 (1)-XX:+UseParallelGC :年輕代使用并行垃圾回收收集器。這是一個(gè)關(guān)注吞吐量的收集器,可以盡可能的減少垃圾回收時(shí)間。 (2)-XX:+UseParallelOldGC :設(shè)置老年代使用并行垃圾回收收集器。 7、嘗試使用大的內(nèi)存分頁(yè):使用大的內(nèi)存分頁(yè)增加CPU的內(nèi)存尋址能力,從而系統(tǒng)的性能。-XX:+LargePageSizeInBytes 設(shè)置內(nèi)存頁(yè)的大小 8、使用非占用的垃圾收集器。-XX:+UseConcMarkSweepGC 老年代使用CMS收集器降低停頓。 9、-XXSurvivorRatio=3 ,表示年輕代中的分配比率:survivor:eden = 2:3 10、JVM性能調(diào)優(yōu)的工具: (1)jps(Java Process Status):輸出JVM中運(yùn)行的進(jìn)程狀態(tài)信息(現(xiàn)在一般使用jconsole) (2)jstack:查看java進(jìn)程內(nèi)線程的堆棧信息。 (3)jmap:用于生成堆轉(zhuǎn)存快照 (4)jhat:用于分析jmap生成的堆轉(zhuǎn)存快照(一般不推薦使用,而是使用Ecplise Memory Analyzer) (3)jstat是JVM統(tǒng)計(jì)監(jiān)測(cè)工具??梢杂脕?lái)顯示垃圾回收信息、類加載信息、新生代統(tǒng)計(jì)信息等。 (4)VisualVM:故障處理工具 類加載機(jī)制:一、 概念:類加載器把class文件中的二進(jìn)制數(shù)據(jù)讀入到內(nèi)存中,存放在方法區(qū),然后在堆區(qū)創(chuàng)建一個(gè)java.lang.Class對(duì)象,用來(lái)封裝類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)。類加載的步驟如下: 1、加載:查找并加載類的二進(jìn)制數(shù)據(jù)(把class文件里面的信息加載到內(nèi)存里面) 2、連接:把內(nèi)存中類的二進(jìn)制數(shù)據(jù)合并到虛擬機(jī)的運(yùn)行時(shí)環(huán)境中 (1)驗(yàn)證:確保被加載的類的正確性。包括: A、類文件的結(jié)構(gòu)檢查:檢查是否滿足Java類文件的固定格式 B、語(yǔ)義檢查:確保類本身符合Java的語(yǔ)法規(guī)范 C、字節(jié)碼驗(yàn)證:確保字節(jié)碼流可以被Java虛擬機(jī)安全的執(zhí)行。字節(jié)碼流是操作碼組成的序列。每一個(gè)操作碼后面都會(huì)跟著一個(gè)或者多個(gè)操作數(shù)。字節(jié)碼檢查這個(gè)步驟會(huì)檢查每一個(gè)操作碼是否合法。 D、二進(jìn)制兼容性驗(yàn)證:確保相互引用的類之間是協(xié)調(diào)一致的。
(2)準(zhǔn)備:為類的靜態(tài)變量分配內(nèi)存,并將其初始化為默認(rèn)值 (3)解析:把類中的符號(hào)引用轉(zhuǎn)化為直接引用(比如說方法的符號(hào)引用,是有方法名和相關(guān)描述符組成,在解析階段,JVM把符號(hào)引用替換成一個(gè)指針,這個(gè)指針就是直接引用,它指向該類的該方法在方法區(qū)中的內(nèi)存位置) 3、初始化:為類的靜態(tài)變量賦予正確的初始值。當(dāng)靜態(tài)變量的等號(hào)右邊的值是一個(gè)常量表達(dá)式時(shí),不會(huì)調(diào)用static代碼塊進(jìn)行初始化。只有等號(hào)右邊的值是一個(gè)運(yùn)行時(shí)運(yùn)算出來(lái)的值,才會(huì)調(diào)用static初始化。 二、雙親委派模型:
1、當(dāng)一個(gè)類加載器收到類加載請(qǐng)求的時(shí)候,它首先不會(huì)自己去加載這個(gè)類的信息,而是把該 請(qǐng)求轉(zhuǎn)發(fā)給父類加載器,依次向上。所以所有的類加載請(qǐng)求都會(huì)被傳遞到父類加載器中,只有當(dāng)父類加載器中無(wú)法加載到所需的類,子類加載器才會(huì)自己嘗試去加載該類。當(dāng)當(dāng)前類加載器和所有父類加載器都無(wú)法加載該類時(shí),拋出ClassNotFindException異常。 2、意義: 提高系統(tǒng)的安全性。用戶自定義的類加載器不可能加載應(yīng)該由父加載器加載的可靠類。(比如用戶定義了一個(gè)惡意代碼,自定義的類加載器首先讓系統(tǒng)加載器去加載,系統(tǒng)加載器檢查該代碼不符合規(guī)范,于是就不繼續(xù)加載了)
3、定義類加載器:如果某個(gè)類加載器能夠加載一個(gè)類,那么這個(gè)類加載器就叫做定義類加載器 4、初始類加載器:定義類加載器及其所有子加載器都稱作初始類加載器。 5、運(yùn)行時(shí)包: (1)由同一個(gè)類加載器加載并且擁有相同包名的類組成運(yùn)行時(shí)包 (2)只有屬于同一個(gè)運(yùn)行時(shí)包的類,才能訪問包可見(default)的類和類成員。作用是 限制用戶自定義的類冒充核心類庫(kù)的類去訪問核心類庫(kù)的包可見成員。 6、加載兩份相同的class對(duì)象的情況:A和B不屬于父子類加載器關(guān)系,并且各自都加載了同一個(gè)類。 三、特點(diǎn):
1、全盤負(fù)責(zé):當(dāng)一個(gè)類加載器加載一個(gè)類時(shí),該類所依賴的其他類也會(huì)被這個(gè)類加載器加載到內(nèi)存中。 2、緩存機(jī)制:所有的Class對(duì)象都會(huì)被緩存,當(dāng)程序需要使用某個(gè)Class時(shí),類加載器先從緩存中查找,找不到,才從class文件中讀取數(shù)據(jù),轉(zhuǎn)化成Class對(duì)象,存入緩存中。 四、 類加載器: 兩種類型的類加載器: 1、 JVM自帶的類加載器(3種): (1)根類加載器(Bootstrap): a、C++編寫的,程序員無(wú)法在程序中獲取該類 b、負(fù)責(zé)加載虛擬機(jī)的核心庫(kù),比如java.lang.Object c、沒有繼承ClassLoader類 (2)擴(kuò)展類加載器(Extension): a、Java編寫的,從指定目錄中加載類庫(kù) b、父加載器是根類加載器 c、是ClassLoader的子類 d、如果用戶把創(chuàng)建的jar文件放到指定目錄中,也會(huì)被擴(kuò)展加載器加載。 (3)系統(tǒng)加載器(System)或者應(yīng)用加載器(App): a、Java編寫的 b、父加載器是擴(kuò)展類加載器 c、從環(huán)境變量或者class.path中加載類 d、是用戶自定義類加載的默認(rèn)父加載器 e、是ClassLoader的子類 2、用戶自定義的類加載器: (1)Java.lang.ClassLoader類的子類 (2)用戶可以定制類的加載方式 (3)父類加載器是系統(tǒng)加載器 (4)編寫步驟: A、繼承ClassLoader B、重寫findClass方法。從特定位置加載class文件,得到字節(jié)數(shù)組,然后利用defineClass把字節(jié)數(shù)組轉(zhuǎn)化為Class對(duì)象 (5)為什么要自定義類加載器? A、可以從指定位置加載class文件,比如說從數(shù)據(jù)庫(kù)、云端加載class文件 B、加密:Java代碼可以被輕易的反編譯,因此,如果需要對(duì)代碼進(jìn)行加密,那么加密以后的代碼,就不能使用Java自帶的ClassLoader來(lái)加載這個(gè)類了,需要自定義ClassLoader,對(duì)這個(gè)類進(jìn)行解密,然后加載。 問題:Java程序?qū)︻惖膱?zhí)行有幾種方式: 1、 主動(dòng)使用(6種情況): JVM必須在每個(gè)類“首次 主動(dòng)使用”的時(shí)候,才會(huì)初始化這些類。 (1) 創(chuàng)建類的實(shí)例 (2) 讀寫某個(gè)類或者接口的靜態(tài)變量 (3) 調(diào)用類的靜態(tài)方法 (4) 同過反射的API(Class.forName())獲取類 (5) 初始化一個(gè)類的子類 (6) JVM啟動(dòng)的時(shí)候,被標(biāo)明啟動(dòng)類的類(包含Main方法的類) 只有當(dāng)程序使用的靜態(tài)變量或者靜態(tài)方法確實(shí)在該類中定義時(shí),該可以認(rèn)為是對(duì)該類或者接口的主動(dòng)使用。 2、 被動(dòng)使用:除了主動(dòng)使用的6種情況,其他情況都是被動(dòng)使用,都不會(huì)導(dǎo)致類的初始化。 3、 JVM規(guī)范允許類加載器在預(yù)料某個(gè)類將要被使用的時(shí)候,就預(yù)先加載它。如果該class文件缺失或者存在錯(cuò)誤,則在程序“首次 主動(dòng)使用”的時(shí)候,才報(bào)告這個(gè)錯(cuò)誤。(Linkage Error錯(cuò)誤)。如果這個(gè)類一直沒有被程序“主動(dòng)使用”,就不會(huì)報(bào)錯(cuò)。 類加載機(jī)制與接口: 1、 當(dāng)Java虛擬機(jī)初始化一個(gè)類時(shí),不會(huì)初始化該類實(shí)現(xiàn)的接口。 2、 在初始化一個(gè)接口時(shí),不會(huì)初始化這個(gè)接口父接口。 3、 只有當(dāng)程序首次使用該接口的靜態(tài)變量時(shí),才導(dǎo)致該接口的初始化。 ClassLoader: 1、 調(diào)用Classloader的loadClass方法去加載一個(gè)類,不是主動(dòng)使用,因此不會(huì)進(jìn)行類的初始化。 類的卸載: 1、 有JVM自帶的三種類加載器(根、擴(kuò)展、系統(tǒng))加載的類始終不會(huì)卸載。因?yàn)镴VM始終引用這些類加載器,這些類加載器使用引用他們所加載的類,因此這些Class類對(duì)象始終是可到達(dá)的。 2、 由用戶自定義類加載器加載的類,是可以被卸載的。 補(bǔ)充: (1)JDK :Java Development Kit,開發(fā)的時(shí)候用到的類包。 (2)JRE :Java Runtime Environment,Java運(yùn)行的基礎(chǔ),包含運(yùn)行時(shí)需要的所有類庫(kù)。 JVM虛擬機(jī)先將java文件編譯成class文件(字節(jié)碼文件),然后再將class文件轉(zhuǎn)換成所有操作系統(tǒng)都能運(yùn)行的機(jī)器指令。 ---------- END ----------
|