1?jvm和jdk和jre分別指的是什么
jdk:java?development?kit?java開發(fā)包?包含jre和其他java工具包括編譯和調(diào)試工具(如lib中的tools的jar中的javac)(因?yàn)槲覀冞\(yùn)行javac.exe進(jìn)行java文件的時(shí)候需要運(yùn)行tool的jar包中的javac方法,由于該方法是class文件的,所以在運(yùn)行時(shí)候也需要一套jre,所以有的機(jī)器上有兩套jre,server一套client一套,一般server上一套就夠用了,server上的性能更好啟動(dòng)更慢。在執(zhí)行該方法時(shí)候我們調(diào)用java命令,java命令會(huì)尋找對(duì)應(yīng)的jre。先是查找本目錄,再是查找父目錄。)以及一些其他java的類庫(不重要)(dt.jar為swing的包)(供開發(fā)者使用)
jre:?java?runtime?environment(jre中的bin為jvm,lib為配合jvm運(yùn)行的類庫)java運(yùn)行環(huán)境?包含jvm和一些標(biāo)準(zhǔn)類庫和核心類庫(普通用戶使用)。(rt.jar在此處我們通常所說的jdk源碼就是javadoc中的class源碼包括map,list,lang等一系列的api的包都是這里)(java命令在此執(zhí)行)rt包含的包如圖:

jvm;java?virtual?machine?java虛擬機(jī)(最核心部分所有的java程序最后編譯的class文件全部都在這上面運(yùn)行)因?yàn)槲覀儗懙膉ava程序最后都是交付給jvm運(yùn)行的,與操作系統(tǒng)是沒有關(guān)系的。因?yàn)椴煌牟僮飨到y(tǒng)平臺(tái)會(huì)有不通的jvm版本,jvm都會(huì)把class文件的二進(jìn)制字節(jié)碼轉(zhuǎn)為機(jī)器能識(shí)別的二進(jìn)制指令。所以是跨平臺(tái)的。
小結(jié):jdk用來編譯,jre用來運(yùn)行,jvm為核心運(yùn)行平臺(tái)

另圖:

2?jvm結(jié)構(gòu):如圖:1.6的結(jié)構(gòu)

1?線程共享的就要做好線程不安全的準(zhǔn)備
2?線程私有的就是線程安全的
1,程序計(jì)數(shù)器(Program?Counter?Register):也叫pc寄存器
程序計(jì)數(shù)器是一個(gè)比較小的內(nèi)存區(qū)域,用于指示當(dāng)前線程所執(zhí)行的字節(jié)碼執(zhí)行到了第幾行,可以理解為是當(dāng)前線程的行號(hào)指示器。字節(jié)碼解釋器在工作時(shí),會(huì)通過改變這個(gè)計(jì)數(shù)器的值來取下一條語句指令。
所謂的行號(hào)指示器其實(shí)是jvm能識(shí)別的class字節(jié)碼中反匯編數(shù)據(jù)的指令偏移地址。
每個(gè)程序計(jì)數(shù)器只用來記錄一個(gè)線程的行號(hào),所以它是線程私有(一個(gè)線程就有一個(gè)程序計(jì)數(shù)器)的。
線程A切換到線程B再切換回來的時(shí)候,會(huì)根據(jù)A的程序計(jì)數(shù)器記錄A執(zhí)行到哪一行了然后接著往下執(zhí)行。如果程序只有一個(gè)單線程那么是不需要程序計(jì)數(shù)器的依次往下執(zhí)行就可以了。
如果程序執(zhí)行的是一個(gè)Java方法,則計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令地址;如果正在執(zhí)行的是一個(gè)本地(native,由C語言編寫完成)方法,則計(jì)數(shù)器的值為Undefined(因?yàn)閖vm中的程序計(jì)數(shù)器只能識(shí)別class字節(jié)碼中的指令偏移地址,c不會(huì)生成class字節(jié)碼文件所以為空,native方法的執(zhí)行和系統(tǒng)執(zhí)行c調(diào)用系統(tǒng)指令是一樣的),由于程序計(jì)數(shù)器只是記錄當(dāng)前指令地址,改變的只是數(shù)值。所以不存在內(nèi)存溢出的情況,因此,程序計(jì)數(shù)器也是所有JVM內(nèi)存區(qū)域中唯一一個(gè)沒有定義OutOfMemoryError的區(qū)域。
特點(diǎn):
1?如果線程正在執(zhí)行的是Java?方法,則這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令地址
2?如果正在執(zhí)行的是Native?方法,則這個(gè)技術(shù)器值為空(Undefined)
3?此內(nèi)存區(qū)域是唯一一個(gè)在Java虛擬機(jī)規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域
4?線程隔離
生命周期:線程啟動(dòng)的時(shí)候產(chǎn)生,線程結(jié)束消亡。
簡單的理解為,是程序計(jì)數(shù)器保證了程序的正常執(zhí)行?
2,虛擬機(jī)棧(JVM?Stack):

一個(gè)線程的每個(gè)方法在執(zhí)行的同時(shí),都會(huì)創(chuàng)建一個(gè)棧幀(Statck?Frame)(虛擬機(jī)中的棧元素,虛擬機(jī)中的方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu)),棧幀中存儲(chǔ)的有局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等,當(dāng)方法被調(diào)用時(shí),棧幀在JVM棧中入棧,當(dāng)方法執(zhí)行完成時(shí),棧幀出棧。一個(gè)方法從執(zhí)行到結(jié)束對(duì)應(yīng)的是棧楨從入棧到出棧的過程。
局部變量表中存儲(chǔ)著方法的相關(guān)局部變量,包括各種基本數(shù)據(jù)類型,對(duì)象的引用,返回地址等。在局部變量表中,只有l(wèi)ong和double類型會(huì)占用2個(gè)局部變量空間(Slot,對(duì)于32位機(jī)器,一個(gè)Slot就是32個(gè)bit),其它都是1個(gè)Slot。需要注意的是,局部變量表是在編譯時(shí)就已經(jīng)確定好的,方法運(yùn)行所需要分配的空間在棧幀中是完全確定的,在方法的生命周期內(nèi)都不會(huì)改變。
虛擬機(jī)棧中定義了兩種異常,如果線程調(diào)用的棧深度大于虛擬機(jī)允許的最大深度,則拋出StatckOverFlowError(棧溢出);不過多數(shù)Java虛擬機(jī)都允許動(dòng)態(tài)擴(kuò)展虛擬機(jī)棧的大小(有少部分是固定長度的),所以線程可以一直申請棧,直到內(nèi)存不足,此時(shí),會(huì)拋出OutOfMemoryError(內(nèi)存溢出)。
每個(gè)線程對(duì)應(yīng)著一個(gè)虛擬機(jī)棧,因此虛擬機(jī)棧也是線程私有的。
-Xss?每個(gè)線程的棧容量大小
棧楨中局部變量應(yīng)用的對(duì)象就是gc?roots之一。gc?roots?是一組必須活躍的引用,不是對(duì)象。
特點(diǎn):
1?線程隔離
2?線程的啟動(dòng)產(chǎn)生虛擬機(jī)棧,線程的結(jié)束消亡
3?方法的執(zhí)行到結(jié)束是棧楨從入棧到出棧的過程
4?棧幀主要包括:局部變量表?操作數(shù)棧?動(dòng)態(tài)連接?方法返回地址?附加信息
5?局部變量表:?只存局部變量信息,局部變量的對(duì)象引用,不存引用的對(duì)象因?yàn)榇藢?duì)象是在堆中。局部變量表中的slot只要不被復(fù)用即:局部變量沒有被回收,那么它引用的對(duì)象就不能被gc。因?yàn)間croots可達(dá)。slot的復(fù)用和作用域有關(guān)系如果出了作用域則被復(fù)用但是不被回收。不出作用域不會(huì)被回收和復(fù)用。局部變量表的大小在編譯階段就已經(jīng)確定了。(靜態(tài)變量直接放值,變量如果是對(duì)象的話放的是對(duì)象的引用即對(duì)象的地址,局部變量一但脫離作用域,內(nèi)存立即釋放)
6?操作數(shù)棧:?jvm的工作區(qū),大多數(shù)的指令執(zhí)行所需要的數(shù)據(jù)都是從這里彈出數(shù)據(jù)然后執(zhí)行完操作結(jié)果再壓回操作數(shù)棧中。加減的邏輯操作主要是在操作數(shù)棧中進(jìn)行的。(系統(tǒng)中的運(yùn)算是在寄存器中運(yùn)行的,但是jvm指令不能訪問程序計(jì)數(shù)器,程序計(jì)數(shù)器只是用來記錄程序行號(hào)的):入棧之后的數(shù)據(jù)必須為基本數(shù)據(jù)類型可以進(jìn)行運(yùn)算。

7?動(dòng)態(tài)鏈接:?棧楨里面變量的動(dòng)態(tài)引用,運(yùn)行時(shí)才能確定運(yùn)行的字節(jié)碼。
8?方法返回地址:?1?正常返回:返回的就是調(diào)用者的計(jì)數(shù)器。
2?異常返回?:?恢復(fù)上層局部變量和操作數(shù)棧,把返回值(如果有的話)壓入上層的操作數(shù)棧中。計(jì)數(shù)器指向后邊一條指令。
總是要返回調(diào)用者的計(jì)數(shù)器即調(diào)用者的位置
小結(jié):stackoverflow出現(xiàn)在遞歸調(diào)用的時(shí)候會(huì)遞歸出來無數(shù)的棧楨的調(diào)用導(dǎo)致虛擬機(jī)棧深度不夠。也可能是因?yàn)閟lot復(fù)用不夠,導(dǎo)致局部變量的內(nèi)存溢出。
3,本地方法棧(Native?Method?Statck):
本地方法棧在作用,運(yùn)行機(jī)制,異常類型等方面都與虛擬機(jī)棧相同,唯一的區(qū)別是:虛擬機(jī)棧是執(zhí)行Java方法的,而本地方法棧是用來執(zhí)行native方法的,在很多虛擬機(jī)中(如Sun的JDK默認(rèn)的HotSpot虛擬機(jī)),會(huì)將本地方法棧與虛擬機(jī)棧放在一起使用。
本地方法棧也是線程私有的。
4,堆區(qū)(Heap):
堆區(qū)是理解Java?GC機(jī)制最重要的區(qū)域,沒有之一。在JVM所管理的內(nèi)存中,堆區(qū)是最大的一塊,堆區(qū)也是Java?GC機(jī)制所管理的主要內(nèi)存區(qū)域,堆區(qū)由所有線程共享,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。堆區(qū)的存在是為了存儲(chǔ)對(duì)象實(shí)例,原則上講,所有的對(duì)象都在堆區(qū)上分配內(nèi)存(不過現(xiàn)代技術(shù)里,也不是這么絕對(duì)的,也有棧上直接分配的)。
一般的,根據(jù)Java虛擬機(jī)規(guī)范規(guī)定,堆內(nèi)存需要在邏輯上是連續(xù)的(在物理上不需要),在實(shí)現(xiàn)時(shí),可以是固定大小的,也可以是可擴(kuò)展的,目前主流的虛擬機(jī)都是可擴(kuò)展的。如果在執(zhí)行垃圾回收之后,仍沒有足夠的內(nèi)存分配,也不能再擴(kuò)展,將會(huì)拋出OutOfMemoryError:Java?heap?space異常。
1?堆中放的全是對(duì)象實(shí)例本身,不放引用和基本數(shù)據(jù)類型。
2?存儲(chǔ)的對(duì)象和與之對(duì)應(yīng)的class信息
5,方法區(qū)(Method?Area):在Java虛擬機(jī)規(guī)范中,將方法區(qū)作為堆的一個(gè)邏輯部分來對(duì)待,但事實(shí)上,方法區(qū)并不是堆(Non-Heap);另外,不少人的博客中,將Java?GC的分代收集機(jī)制分為3個(gè)代:青年代,老年代,永久代,這些作者將方法區(qū)定義為“永久代”,這是因?yàn)?,?duì)于之前的HotSpot?Java虛擬機(jī)的實(shí)現(xiàn)方式中,將分代收集的思想擴(kuò)展到了方法區(qū),并將方法區(qū)設(shè)計(jì)成了永久代。不過,除HotSpot之外的多數(shù)虛擬機(jī),并不將方法區(qū)當(dāng)做永久代,HotSpot本身,也計(jì)劃取消永久代。jdk8里面叫做元空間。
方法區(qū)是各個(gè)線程共享的區(qū)域,用于存儲(chǔ)已經(jīng)被虛擬機(jī)加載的類信息(即加載類時(shí)需要加載的信息,包括版本、field、方法、接口等信息)、final常量、靜態(tài)變量、編譯器即時(shí)編譯的代碼等。
方法區(qū)在物理上也不需要是連續(xù)的,可以選擇固定大小或可擴(kuò)展大小,并且方法區(qū)比堆還多了一個(gè)限制:可以選擇是否執(zhí)行垃圾收集。一般的,方法區(qū)上執(zhí)行的垃圾收集是很少的,這也是方法區(qū)被稱為永久代的原因之一(HotSpot),但這也不代表著在方法區(qū)上完全沒有垃圾收集,其上的垃圾收集主要是針對(duì)常量池的內(nèi)存回收和對(duì)已加載類的卸載。
在方法區(qū)上進(jìn)行垃圾收集,條件苛刻而且相當(dāng)困難,效果也不令人滿意,所以一般不做太多考慮,可以留作以后進(jìn)一步深入研究時(shí)使用。
在方法區(qū)上定義了OutOfMemoryError:PermGen?space異常,在內(nèi)存不足時(shí)拋出。
運(yùn)行時(shí)常量池(Runtime?Constant?Pool)是方法區(qū)的一部分,用于存儲(chǔ)編譯期就生成的字面常量、符號(hào)引用、翻譯出來的直接引用(符號(hào)引用就是編碼是用字符串表示某個(gè)變量、接口的位置,直接引用就是根據(jù)符號(hào)引用翻譯出來的地址,將在類鏈接階段完成翻譯);運(yùn)行時(shí)常量池除了存儲(chǔ)編譯期常量外,也可以存儲(chǔ)在運(yùn)行時(shí)間產(chǎn)生的常量(比如String類的intern()方法,作用是String維護(hù)了一個(gè)常量池,如果調(diào)用的字符“abc”已經(jīng)在常量池中,則返回池中的字符串地址,否則,新建一個(gè)常量加入池中,并返回地址)。
1?類型信息是由類加載器在類加載時(shí)從類文件中提取出來的。類(靜態(tài))變量也存儲(chǔ)在方法區(qū)中。
2?包含類的常量池,域信息(public,private,static,final,volatile等),方法信息,所有靜態(tài)變量和方法,類加載器。注:1.7中的常量池已經(jīng)移到堆里
3?常量(被聲明為final的類變量)的處理方法則不同,每個(gè)常量都會(huì)在常量池中有一個(gè)拷貝。non-final類變量被存儲(chǔ)在聲明它的類信息內(nèi),而final類被存儲(chǔ)在所有使用它的類信息內(nèi)。
6,直接內(nèi)存(Direct?Memory):直接內(nèi)存并不是JVM管理的內(nèi)存,可以這樣理解,直接內(nèi)存,就是JVM以外的機(jī)器內(nèi)存,比如,你有4G的內(nèi)存,JVM占用了1G,則其余的3G就是直接內(nèi)存,JDK中有一種基于通道(Channel)和緩沖區(qū)(Buffer)的內(nèi)存分配方式,將由C語言實(shí)現(xiàn)的native函數(shù)庫分配在直接內(nèi)存中,用存儲(chǔ)在JVM堆中的DirectByteBuffer來引用。由于直接內(nèi)存收到本機(jī)器內(nèi)存的限制,所以也可能出現(xiàn)OutOfMemoryError的異常。
Java對(duì)象的訪問方式
一般來說,一個(gè)Java的引用訪問涉及到3個(gè)內(nèi)存區(qū)域:JVM棧,堆,方法區(qū)。
以最簡單的本地變量引用:Object?obj?=?new?Object()為例:
Object?obj表示一個(gè)本地引用,存儲(chǔ)在JVM棧的本地變量表中,表示一個(gè)reference類型數(shù)據(jù);
new?Object()作為實(shí)例對(duì)象數(shù)據(jù)存儲(chǔ)在堆中;
堆中還記錄了Object類的類型信息(接口、方法、field、對(duì)象類型等)的地址,這些地址所執(zhí)行的數(shù)據(jù)存儲(chǔ)在方法區(qū)中;
在Java虛擬機(jī)規(guī)范中,對(duì)于通過reference類型引用訪問具體對(duì)象的方式并未做規(guī)定,目前主流的實(shí)現(xiàn)方式主要有兩種:
1,通過句柄訪問(圖來自于《深入理解Java虛擬機(jī):JVM高級(jí)特效與最佳實(shí)現(xiàn)》):

通過句柄訪問的實(shí)現(xiàn)方式中,JVM堆中會(huì)專門有一塊區(qū)域用來作為句柄池,存儲(chǔ)相關(guān)句柄所執(zhí)行的實(shí)例數(shù)據(jù)地址(包括在堆中地址和在方法區(qū)中的地址)。這種實(shí)現(xiàn)方法由于用句柄表示地址,因此十分穩(wěn)定。
2,通過直接指針引用訪問:(圖來自于《深入理解Java虛擬機(jī):JVM高級(jí)特效與最佳實(shí)現(xiàn)》)

通過直接指針訪問的方式中,reference中存儲(chǔ)的就是對(duì)象在堆中的實(shí)際地址,在堆中存儲(chǔ)的對(duì)象信息中包含了在方法區(qū)中的相應(yīng)類型數(shù)據(jù)。這種方法最大的優(yōu)勢是速度快,在HotSpot虛擬機(jī)中用的就是這種方式。
總結(jié)及轉(zhuǎn)向gc:
內(nèi)存溢出:申請內(nèi)存沒有足夠的內(nèi)存能夠獲得就會(huì)出現(xiàn)oom。正常情況下通過加內(nèi)存或者合理分配內(nèi)存就能得到解決。內(nèi)存泄漏最終也會(huì)導(dǎo)致內(nèi)存溢出,這種不是通過分配內(nèi)存或者加內(nèi)存能夠解決的。
內(nèi)存泄漏:在本該被垃圾回收的對(duì)象一直占有內(nèi)存不能釋放久而久之就會(huì)導(dǎo)致內(nèi)存溢出就是內(nèi)存泄漏。
Java內(nèi)存分配機(jī)制
這里所說的內(nèi)存分配,主要指的是在堆上的分配,一般的,對(duì)象的內(nèi)存分配都是在堆上進(jìn)行,但現(xiàn)代技術(shù)也支持將對(duì)象拆成標(biāo)量類型(標(biāo)量類型即原子類型,表示單個(gè)值,可以是基本類型或String等),然后在棧上分配,在棧上分配的很少見,我們這里不考慮。
Java內(nèi)存分配和回收的機(jī)制概括的說,就是:分代分配,分代回收。對(duì)象將根據(jù)存活的時(shí)間被分為:年輕代(Young?Generation)、年老代(Old?Generation)、永久代(Permanent?Generation,也就是方法區(qū))。如下圖(來源于《成為JavaGC專家part?I》,http://www.importnew.com/1993.html):

年輕代(Young?Generation):對(duì)象被創(chuàng)建時(shí),內(nèi)存的分配首先發(fā)生在年輕代(大對(duì)象可以直接被創(chuàng)建在年老代),大部分的對(duì)象在創(chuàng)建后很快就不再使用,因此很快變得不可達(dá),于是被年輕代的GC機(jī)制清理掉(IBM的研究表明,98%的對(duì)象都是很快消亡的),這個(gè)GC機(jī)制被稱為Minor?GC或叫Young?GC。注意,Minor?GC并不代表年輕代內(nèi)存不足,它事實(shí)上只表示在Eden區(qū)上的GC。
對(duì)象存活判定:?1?引用計(jì)數(shù)器算法?:計(jì)數(shù)器位置問題,相互引用問題(舍棄)
2?對(duì)象是否可達(dá)算法:即被gcroots所引用的對(duì)象都是可達(dá)對(duì)象,反之不可達(dá)。
gc?roots:?1棧中存活的方法2系統(tǒng)類加載器加載的對(duì)象3方法區(qū)中靜態(tài)屬性引用的對(duì)象4方法區(qū)?中常量引用的對(duì)象5本地方法棧中的jni引用的對(duì)象。

年輕代上的內(nèi)存分配是這樣的,年輕代可以分為3個(gè)區(qū)域:Eden區(qū)(伊甸園,亞當(dāng)和夏娃偷吃禁果生娃娃的地方,用來表示內(nèi)存首次分配的區(qū)域,再貼切不過)和兩個(gè)存活區(qū)(Survivor?0?、Survivor?1)。內(nèi)存分配過程為(來源于《成為JavaGC專家part?I》,http://www.importnew.com/1993.html):
絕大多數(shù)剛創(chuàng)建的對(duì)象會(huì)被分配在Eden區(qū),其中的大多數(shù)對(duì)象很快就會(huì)消亡。Eden區(qū)是連續(xù)的內(nèi)存空間,因此在其上分配內(nèi)存極快;
最初一次,當(dāng)Eden區(qū)滿的時(shí)候,執(zhí)行Minor?GC,將消亡的對(duì)象清理掉,并將剩余的對(duì)象復(fù)制到一個(gè)存活區(qū)Survivor0(此時(shí),Survivor1是空白的,兩個(gè)Survivor總有一個(gè)是空白的);
?下次Eden區(qū)滿了,再執(zhí)行一次Minor?GC,將消亡的對(duì)象清理掉,將存活的對(duì)象復(fù)制到Survivor1中,然后清空Eden區(qū);
?將Survivor0中消亡的對(duì)象清理掉,將其中可以晉級(jí)的對(duì)象晉級(jí)到Old區(qū),將存活的對(duì)象也復(fù)制到Survivor1區(qū),然后清空Survivor0區(qū);
當(dāng)兩個(gè)存活區(qū)切換了幾次(HotSpot虛擬機(jī)默認(rèn)15次,用-XX:MaxTenuringThreshold控制,大于該值進(jìn)入老年代,但這只是個(gè)最大值,并不代表一定是這個(gè)值)之后,仍然存活的對(duì)象(其實(shí)只有一小部分,比如,我們自己定義的對(duì)象),將被復(fù)制到老年代。
從上面的過程可以看出,Eden區(qū)是連續(xù)的空間,且Survivor總有一個(gè)為空。經(jīng)過一次GC和復(fù)制,一個(gè)Survivor中保存著當(dāng)前還活著的對(duì)象,而Eden區(qū)和另一個(gè)Survivor區(qū)的內(nèi)容都不再需要了,可以直接清空,到下一次GC時(shí),兩個(gè)Survivor的角色再互換。因此,這種方式分配內(nèi)存和清理內(nèi)存的效率都極高,這種垃圾回收的方式就是著名的“停止-復(fù)制(Stop-and-copy)”清理法(將Eden區(qū)和一個(gè)Survivor中仍然存活的對(duì)象拷貝到另一個(gè)Survivor中),這不代表著停止復(fù)制清理法很高效,其實(shí),它也只在這種情況下高效,如果在老年代采用停止復(fù)制,則挺悲劇的。
在Eden區(qū),HotSpot虛擬機(jī)使用了兩種技術(shù)來加快內(nèi)存分配。分別是bump-the-pointer和TLAB(Thread-Local?Allocation?Buffers),這兩種技術(shù)的做法分別是:由于Eden區(qū)是連續(xù)的,因此bump-the-pointer技術(shù)的核心就是跟蹤最后創(chuàng)建的一個(gè)對(duì)象,在對(duì)象創(chuàng)建時(shí),只需要檢查最后一個(gè)對(duì)象后面是否有足夠的內(nèi)存即可,從而大大加快內(nèi)存分配速度;而對(duì)于TLAB技術(shù)是對(duì)于多線程而言的,將Eden區(qū)分為若干段,每個(gè)線程使用獨(dú)立的一段,避免相互影響。TLAB結(jié)合bump-the-pointer技術(shù),將保證每個(gè)線程都使用Eden區(qū)的一段,并快速的分配內(nèi)存。
年老代(Old?Generation):對(duì)象如果在年輕代存活了足夠長的時(shí)間而沒有被清理掉(即在幾次Young?GC后存活了下來),則會(huì)被復(fù)制到年老代,年老代的空間一般比年輕代大,能存放更多的對(duì)象,在年老代上發(fā)生的GC次數(shù)也比年輕代少。當(dāng)年老代內(nèi)存不足時(shí),將執(zhí)行Major?GC,也叫?Full?GC。
? 可以使用-XX:+UseAdaptiveSizePolicy開關(guān)來控制是否采用動(dòng)態(tài)控制策略,如果動(dòng)態(tài)控制,則動(dòng)態(tài)調(diào)整Java堆中各個(gè)區(qū)域的大小以及進(jìn)入老年代的年齡。
如果對(duì)象比較大(比如長字符串或大數(shù)組),Young空間不足,則大對(duì)象會(huì)直接分配到老年代上(大對(duì)象可能觸發(fā)提前GC,應(yīng)少用,更應(yīng)避免使用短命的大對(duì)象)。用-XX:PretenureSizeThreshold來控制直接升入老年代的對(duì)象大小,大于這個(gè)值的對(duì)象會(huì)直接分配在老年代上。
可能存在年老代對(duì)象引用新生代對(duì)象的情況,如果需要執(zhí)行Young?GC,則可能需要查詢整個(gè)老年代以確定是否可以清理回收,這顯然是低效的。解決的方法是,年老代中維護(hù)一個(gè)512?byte的塊——”card?table“,所有老年代對(duì)象引用新生代對(duì)象的記錄都記錄在這里。Young?GC時(shí),只要查這里即可,不用再去查全部老年代,因此性能大大提高。
Java?GC機(jī)制
GC機(jī)制的基本算法是:分代收集,這個(gè)不用贅述。下面闡述每個(gè)分代的收集方法。
年輕代:
事實(shí)上,在上一節(jié),已經(jīng)介紹了新生代的主要垃圾回收方法,在新生代中,使用“停止-復(fù)制”算法進(jìn)行清理,將新生代內(nèi)存分為2部分,1部分?Eden區(qū)較大,1部分Survivor比較小,并被劃分為兩個(gè)等量的部分。每次進(jìn)行清理時(shí),將Eden區(qū)和一個(gè)Survivor中仍然存活的對(duì)象拷貝到?另一個(gè)Survivor中,然后清理掉Eden和剛才的Survivor。
這里也可以發(fā)現(xiàn),停止復(fù)制算法中,用來復(fù)制的兩部分并不總是相等的(傳統(tǒng)的停止復(fù)制算法兩部分內(nèi)存相等,但新生代中使用1個(gè)大的Eden區(qū)和2個(gè)小的Survivor區(qū)來避免這個(gè)問題)
由于絕大部分的對(duì)象都是短命的,甚至存活不到Survivor中,所以,Eden區(qū)與Survivor的比例較大,HotSpot默認(rèn)是?8:1,即分別占新生代的80%,10%,10%。如果一次回收中,Survivor+Eden中存活下來的內(nèi)存超過了10%,則需要將一部分對(duì)象分配到?老年代。用-XX:SurvivorRatio參數(shù)來配置Eden區(qū)域Survivor區(qū)的容量比值,默認(rèn)是8,代表Eden:Survivor1:Survivor2=8:1:1.
老年代:
老年代存儲(chǔ)的對(duì)象比年輕代多得多,而且不乏大對(duì)象,對(duì)老年代進(jìn)行內(nèi)存清理時(shí),如果使用停止-復(fù)制算法,則相當(dāng)?shù)托АR话?,老年代用的算法是?biāo)記-整理算法,即:標(biāo)記出仍然存活的對(duì)象(存在引用的),將所有存活的對(duì)象向一端移動(dòng),以保證內(nèi)存的連續(xù)。
?????在發(fā)生Minor?GC時(shí),虛擬機(jī)會(huì)檢查每次晉升進(jìn)入老年代的大小是否大于老年代的剩余空間大小,如果大于,則直接觸發(fā)一次Full?GC,否則,就查看是否設(shè)置了-XX:+HandlePromotionFailure(允許擔(dān)保失?。?,如果允許,則只會(huì)進(jìn)行MinorGC,此時(shí)可以容忍內(nèi)存分配失??;如果不允許,則仍然進(jìn)行Full?GC(這代表著如果設(shè)置-XX:+Handle?PromotionFailure,則觸發(fā)MinorGC就會(huì)同時(shí)觸發(fā)Full?GC,哪怕老年代還有很多內(nèi)存,所以,最好不要這樣做)。
方法區(qū)(永久代):
永久代的回收有兩種:常量池中的常量,無用的類信息,常量的回收很簡單,沒有引用了就可以被回收。對(duì)于無用的類進(jìn)行回收,必須保證3點(diǎn):
類的所有實(shí)例都已經(jīng)被回收
加載類的ClassLoader已經(jīng)被回收
類對(duì)象的Class對(duì)象沒有被引用(即沒有通過反射引用該類的地方)
?????永久代的回收并不是必須的,可以通過參數(shù)來設(shè)置是否對(duì)類進(jìn)行回收。HotSpot提供-Xnoclassgc進(jìn)行控制
?????使用-verbose,-XX:+TraceClassLoading、-XX:+TraceClassUnLoading可以查看類加載和卸載信息
?????-verbose、-XX:+TraceClassLoading可以在Product版HotSpot中使用;
?????-XX:+TraceClassUnLoading需要fastdebug版HotSpot支持
垃圾收集器
在GC機(jī)制中,起重要作用的是垃圾收集器,垃圾收集器是GC的具體實(shí)現(xiàn),Java虛擬機(jī)規(guī)范中對(duì)于垃圾收集器沒有任何規(guī)定,所以不同廠商實(shí)現(xiàn)的垃圾?收集器各不相同,HotSpot?1.6版使用的垃圾收集器如下圖(圖來源于《深入理解Java虛擬機(jī):JVM高級(jí)特效與最佳實(shí)現(xiàn)》,圖中兩個(gè)收集器之間有連線,說明它們可以配合使用):
在介紹垃圾收集器之前,需要明確一點(diǎn),就是在新生代采用的停止復(fù)制算法中,“停?止(Stop-the-world)”的意義是在回收內(nèi)存時(shí),需要暫停其他所?有線程的執(zhí)行。這個(gè)是很低效的,現(xiàn)在的各種新生代收集器越來越優(yōu)化這一點(diǎn),但仍然只是將停止的時(shí)間變短,并未徹底取消停止。
Serial收集器:新生代收集器,使用停止復(fù)制算法,使用一個(gè)線程進(jìn)行GC,串行,其它工作線程暫停。使用-XX:+UseSerialGC可以使用Serial+Serial?Old模式運(yùn)行進(jìn)行內(nèi)存回收(這也是虛擬機(jī)在Client模式下運(yùn)行的默認(rèn)值)
ParNew收集器:新生代收集器,使用停止復(fù)制算法,Serial收集器的多線程版,用多個(gè)線程進(jìn)行GC,并行,其它工作線程暫停,關(guān)注縮短垃圾收集時(shí)間。使用-XX:+UseParNewGC開關(guān)來控制使用ParNew+Serial?Old收集器組合收集內(nèi)存;使用-XX:ParallelGCThreads來設(shè)置執(zhí)行內(nèi)存回收的線程數(shù)。
Parallel?Scavenge?收集器:新生代收集器,使用停止復(fù)制算法,關(guān)注CPU吞吐量,即運(yùn)行用戶代碼的時(shí)間/總時(shí)間,比如:JVM運(yùn)行100分鐘,其中運(yùn)行用戶代碼99分鐘,垃?圾收集1分鐘,則吞吐量是99%,這種收集器能最高效率的利用CPU,適合運(yùn)行后臺(tái)運(yùn)算(關(guān)注縮短垃圾收集時(shí)間的收集器,如CMS,等待時(shí)間很少,所以適?合用戶交互,提高用戶體驗(yàn))。使用-XX:+UseParallelGC開關(guān)控制使用Parallel?Scavenge+Serial?Old收集器組合回收垃圾(這也是在Server模式下的默認(rèn)值);使用-XX:GCTimeRatio來設(shè)置用戶執(zhí)行時(shí)間占總時(shí)間的比例,默認(rèn)99,即1%的時(shí)間用來進(jìn)行垃圾回收。使用-XX:MaxGCPauseMillis設(shè)置GC的最大停頓時(shí)間(這個(gè)參數(shù)只對(duì)Parallel?Scavenge有效),用開關(guān)參數(shù)-XX:+UseAdaptiveSizePolicy可以進(jìn)行動(dòng)態(tài)控制,如自動(dòng)調(diào)整Eden/Survivor比例,老年代對(duì)象年齡,新生代大小等,這個(gè)參數(shù)在ParNew下沒有。
Serial?Old收集器:老年代收集器,單線程收集器,串行,使用標(biāo)記整理(整理的方法是Sweep(清理)和Compact(壓縮),清理是將廢棄的對(duì)象干掉,只留幸存的對(duì)象,壓縮是將移動(dòng)對(duì)象,將空間填滿保證內(nèi)存分為2塊,一塊全是對(duì)象,一塊空閑)算法,使用單線程進(jìn)行GC,其它工作線程暫停(注意,在老年代中進(jìn)行標(biāo)記整理算法清理,也需要暫停其它線程),在JDK1.5之前,Serial?Old收集器與ParallelScavenge搭配使用。
Parallel?Old收集器:老年代收集器,多線程,并行,多線程機(jī)制與Parallel?Scavenge差不錯(cuò),使用標(biāo)記整理(與Serial?Old不同,這里的整理是Summary(匯總)和Compact(壓縮),匯總的意思就是將幸存的對(duì)象復(fù)制到預(yù)先準(zhǔn)備好的區(qū)域,而不是像Sweep(清理)那樣清理廢棄的對(duì)象)算法,在Parallel?Old執(zhí)行時(shí),仍然需要暫停其它線程。Parallel?Old在多核計(jì)算中很有用。Parallel?Old出現(xiàn)后(JDK?1.6),與Parallel?Scavenge配合有很好的效果,充分體現(xiàn)Parallel?Scavenge收集器吞吐量優(yōu)先的效果。使用-XX:+UseParallelOldGC開關(guān)控制使用Parallel?Scavenge?+Parallel?Old組合收集器進(jìn)行收集。
CMS(Concurrent?Mark?Sweep)收集器:老年代收集器,致力于獲取最短回收停頓時(shí)間(即縮短垃圾回收的時(shí)間),使用標(biāo)記清除算法,多線程,優(yōu)點(diǎn)是并發(fā)收集(用戶線程可以和GC線程同時(shí)工作),停頓小。使用-XX:+UseConcMarkSweepGC進(jìn)行ParNew+CMS+Serial?Old進(jìn)行內(nèi)存回收,優(yōu)先使用ParNew+CMS(原因見后面),當(dāng)用戶線程內(nèi)存不足時(shí),采用備用方案Serial?Old收集。
CMS收集的執(zhí)行過程是:初始標(biāo)記(CMS-initial-mark)?->?并發(fā)標(biāo)記(CMS-concurrent-mark)?-->預(yù)清理(CMS-concurrent-preclean)-->可控預(yù)清理(CMS-concurrent-abortable-preclean)->?重新標(biāo)記(CMS-remark)?->?并發(fā)清除(CMS-concurrent-sweep)?->并發(fā)重設(shè)狀態(tài)等待下次CMS的觸發(fā)(CMS-concurrent-reset)
具體的說,先2次標(biāo)記,1次預(yù)清理,1次重新標(biāo)記,再1次清除。?
1,首先jvm根據(jù)-XX:CMSInitiatingOccupancyFraction,-XX:+UseCMSInitiatingOccupancyOnly來決定什么時(shí)間開始垃圾收集;
2,如果設(shè)置了-XX:+UseCMSInitiatingOccupancyOnly,那么只有當(dāng)old代占用確實(shí)達(dá)到了-XX:CMSInitiatingOccupancyFraction參數(shù)所設(shè)定的比例時(shí)才會(huì)觸發(fā)cms?gc;
3,如果沒有設(shè)置-XX:+UseCMSInitiatingOccupancyOnly,那么系統(tǒng)會(huì)根據(jù)統(tǒng)計(jì)數(shù)據(jù)自行決定什么時(shí)候觸發(fā)cms?gc;因此有時(shí)會(huì)遇到設(shè)置了80%比例才cms?gc,但是50%時(shí)就已經(jīng)觸發(fā)了,就是因?yàn)檫@個(gè)參數(shù)沒有設(shè)置的原因;
4,當(dāng)cms?gc開始時(shí),首先的階段是初始標(biāo)記(CMS-initial-mark),是stop?the?world階段,因此此階段標(biāo)記的對(duì)象只是從root集最直接可達(dá)的對(duì)象;
?????CMS-initial-mark:961330K(1572864K),指標(biāo)記時(shí),old代的已用空間和總空間
5,下一個(gè)階段是并發(fā)標(biāo)記(CMS-concurrent-mark),此階段是和應(yīng)用線程并發(fā)執(zhí)行的,所謂并發(fā)收集器指的就是這個(gè),主要作用是標(biāo)記可達(dá)的對(duì)象,此階段不需要用戶停頓。
???????此階段會(huì)打印2條日志:CMS-concurrent-mark-start,CMS-concurrent-mark
6,下一個(gè)階段是CMS-concurrent-preclean,此階段主要是進(jìn)行一些預(yù)清理,因?yàn)闃?biāo)記和應(yīng)用線程是并發(fā)執(zhí)行的,因此會(huì)有些對(duì)象的狀態(tài)在標(biāo)記后會(huì)改變,此階段正是解決這個(gè)問題因?yàn)橹蟮腞escan階段也會(huì)stop?the?world,為了使暫停的時(shí)間盡可能的小,也需要preclean階段先做一部分工作以節(jié)省時(shí)間
?????此階段會(huì)打印2條日志:CMS-concurrent-preclean-start,CMS-concurrent-preclean
7,下一階段是CMS-concurrent-abortable-preclean階段,加入此階段的目的是使cms?gc更加可控一些,作用也是執(zhí)行一些預(yù)清理,以減少Rescan階段造成應(yīng)用暫停的時(shí)間
?????此階段涉及幾個(gè)參數(shù):
?????-XX:CMSMaxAbortablePrecleanTime:當(dāng)abortable-preclean階段執(zhí)行達(dá)到這個(gè)時(shí)間時(shí)才會(huì)結(jié)束
?????-XX:CMSScheduleRemarkEdenSizeThreshold(默認(rèn)2m):控制abortable-preclean階段什么時(shí)候開始執(zhí)行,
??????即當(dāng)eden使用達(dá)到此值時(shí),才會(huì)開始abortable-preclean階段
?????-XX:CMSScheduleRemarkEdenPenetratio(默認(rèn)50%):控制abortable-preclean階段什么時(shí)候結(jié)束執(zhí)行
??????此階段會(huì)打印一些日志如下:
?????CMS-concurrent-abortable-preclean-start,CMS-concurrent-abortable-preclean,
??????CMS:abort?preclean?due?to?time?XXX
8,再下一個(gè)階段是第二個(gè)stop?the?world階段了,即Rescan階段,此階段暫停應(yīng)用線程,停頓時(shí)間比并發(fā)標(biāo)記小得多,但比初始標(biāo)記稍長。對(duì)對(duì)象進(jìn)行重新掃描并標(biāo)記;
???????YG?occupancy:964861K(2403008K),指執(zhí)行時(shí)young代的情況
???????CMS?remark:961330K(1572864K),指執(zhí)行時(shí)old代的情況
??????此外,還打印出了弱引用處理、類卸載等過程的耗時(shí)
9,再下一個(gè)階段是CMS-concurrent-sweep,進(jìn)行并發(fā)的垃圾清理
10,最后是CMS-concurrent-reset,為下一次cms?gc重置相關(guān)數(shù)據(jù)結(jié)構(gòu)
有2種情況會(huì)觸發(fā)CMS?的悲觀full?gc,在悲觀full?gc時(shí),整個(gè)應(yīng)用會(huì)暫停
???????A,concurrent-mode-failure:預(yù)清理階段可能出現(xiàn),當(dāng)cms?gc正進(jìn)行時(shí),此時(shí)有新的對(duì)象要進(jìn)行old代,但是old代空間不足造成的。其可能性有:1,O區(qū)空間不足以讓新生代晉級(jí),2,O區(qū)空間用完之前,無法完成對(duì)無引用的對(duì)象的清理。這表明,當(dāng)前有大量數(shù)據(jù)進(jìn)入內(nèi)存且無法釋放。
???????B,promotion-failed:新生代young?gc可能出現(xiàn),當(dāng)進(jìn)行young?gc時(shí),有部分young代對(duì)象仍然可用,但是S1或S2放不下,因此需要放到old代,但此時(shí)old代空間無法容納此。
影響cms?gc時(shí)長及觸發(fā)的參數(shù)是以下2個(gè):
????????-XX:CMSMaxAbortablePrecleanTime=5000
????????-XX:CMSInitiatingOccupancyFraction=80
解決也是針對(duì)這兩個(gè)參數(shù)來的,根本的原因是每次請求消耗的內(nèi)存量過大
解決方式:
??????A,針對(duì)cms?gc的觸發(fā)階段,調(diào)整-XX:CMSInitiatingOccupancyFraction=50,提早觸發(fā)cms?gc,就可以緩解當(dāng)old代達(dá)到80%,cms?gc處理不完,從而造成concurrent?mode?failure引發(fā)full?gc
?????B,修改-XX:CMSMaxAbortablePrecleanTime=500,縮小CMS-concurrent-abortable-preclean階段的時(shí)間
?????C,考慮到cms?gc時(shí)不會(huì)進(jìn)行compact,因此加入-XX:+UseCMSCompactAtFullCollection
???????(cms?gc后會(huì)進(jìn)行內(nèi)存的compact)和-XX:CMSFullGCsBeforeCompaction=4(在full?gc4次后會(huì)進(jìn)行compact)參數(shù)
在CMS清理過程中,只有初始標(biāo)記和重新標(biāo)記需要短暫停頓,并發(fā)標(biāo)記和并發(fā)清除都不需要暫停用戶線程,因此效率很高,很適合高交互的場合。
CMS也有缺點(diǎn),它需要消耗額外的CPU和內(nèi)存資源,在CPU和內(nèi)存資源緊張,CPU較少時(shí),會(huì)加重系統(tǒng)負(fù)擔(dān)(CMS默認(rèn)啟動(dòng)線程數(shù)為(CPU數(shù)量+3)/4)。
另外,在并發(fā)收集過程中,用戶線程仍然在運(yùn)行,仍然產(chǎn)生內(nèi)存垃圾,所以可能產(chǎn)生“浮動(dòng)垃圾”,本次無法清理,只能下一次Full?GC才清理,因此在GC期間,需要預(yù)留足夠的內(nèi)存給用戶線程使用。所以使用CMS的收集器并不是老年代滿了才觸發(fā)Full?GC,而是在使用了一大半(默認(rèn)68%,即2/3,使用-XX:CMSInitiatingOccupancyFraction來設(shè)置)的時(shí)候就要進(jìn)行Full?GC,如果用戶線程消耗內(nèi)存不是特別大,可以適當(dāng)調(diào)高-XX:CMSInitiatingOccupancyFraction以降低GC次數(shù),提高性能,如果預(yù)留的用戶線程內(nèi)存不夠,則會(huì)觸發(fā)Concurrent?Mode?Failure,此時(shí),將觸發(fā)備用方案:使用Serial?Old?收集器進(jìn)行收集,但這樣停頓時(shí)間就長了,因此-XX:CMSInitiatingOccupancyFraction不宜設(shè)的過大。
還有,CMS采用的是標(biāo)記清除算法,會(huì)導(dǎo)致內(nèi)存碎片的產(chǎn)生,可以使用-XX:+UseCMSCompactAtFullCollection來設(shè)置是否在Full?GC之后進(jìn)行碎片整理,用-XX:CMSFullGCsBeforeCompaction來設(shè)置在執(zhí)行多少次不壓縮的Full?GC之后,來一次帶壓縮的Full?GC。
G1收集器:在JDK1.7中正式發(fā)布,與現(xiàn)狀的新生代、老年代概念有很大不同,目前使用較少,不做介紹。
?????注意并發(fā)(Concurrent)和并行(Parallel)的區(qū)別:
?????并發(fā)是指用戶線程與GC線程同時(shí)執(zhí)行(不一定是并行,可能交替,但總體上是在同時(shí)執(zhí)行的),不需要停頓用戶線程(其實(shí)在CMS中用戶線程還是需要停頓的,只是非常短,GC線程在另一個(gè)CPU上執(zhí)行);
?????并行收集是指多個(gè)GC線程并行工作,但此時(shí)用戶線程是暫停的;
所以,Serial是串行的,Parallel收集器是并行的,而CMS收集器是并發(fā)的.