1 CPU和內(nèi)存的交互
了解jvm內(nèi)存模型前,了解下cpu和計(jì)算機(jī)內(nèi)存的交互情況?!疽?yàn)镴ava虛擬機(jī)內(nèi)存模型定義的訪問(wèn)操作與計(jì)算機(jī)十分相似】
有篇很棒的文章,從cpu講到內(nèi)存模型:什么是java內(nèi)存模型
在計(jì)算機(jī)中,cpu和內(nèi)存的交互最為頻繁,相比內(nèi)存,磁盤(pán)讀寫(xiě)太慢,內(nèi)存相當(dāng)于高速的緩沖區(qū)。
但是隨著cpu的發(fā)展,內(nèi)存的讀寫(xiě)速度也遠(yuǎn)遠(yuǎn)趕不上cpu。因此cpu廠商在每顆cpu上加上高速緩存,用于緩解這種情況。現(xiàn)在cpu和內(nèi)存的交互大致如下。
cpu上加入了高速緩存這樣做解決了處理器和內(nèi)存的矛盾(一快一慢),但是引來(lái)的新的問(wèn)題 - 緩存一致性
在多核cpu中,每個(gè)處理器都有各自的高速緩存(L1,L2,L3),而主內(nèi)存確只有一個(gè) 。
以我的pc為例,因?yàn)閏pu成本高,緩存區(qū)一般也很小。
CPU要讀取一個(gè)數(shù)據(jù)時(shí),首先從一級(jí)緩存中查找,如果沒(méi)有找到再?gòu)亩?jí)緩存中查找,如果還是沒(méi)有就從三級(jí)緩存或內(nèi)存中查找,每個(gè)cpu有且只有一套自己的緩存。
如何保證多個(gè)處理器運(yùn)算涉及到同一個(gè)內(nèi)存區(qū)域時(shí),多線程場(chǎng)景下會(huì)存在緩存一致性問(wèn)題,那么運(yùn)行時(shí)保證數(shù)據(jù)一致性?
為了解決這個(gè)問(wèn)題,各個(gè)處理器需遵循一些協(xié)議保證一致性。【如MSI,MESI啥啥的協(xié)議。。】
大概如下
在CPU層面,內(nèi)存屏障提供了個(gè)充分必要條件
1.1.1 內(nèi)存屏障(Memory Barrier)
CPU中,每個(gè)CPU又有多級(jí)緩存【上圖統(tǒng)一定義為高速緩存】,一般分為L(zhǎng)1,L2,L3,因?yàn)檫@些緩存的出現(xiàn),提高了數(shù)據(jù)訪問(wèn)性能,避免每次都向內(nèi)存索取,但是弊端也很明顯,不能實(shí)時(shí)的和內(nèi)存發(fā)生信息交換,分在不同CPU執(zhí)行的不同線程對(duì)同一個(gè)變量的緩存值不同。
硬件層的內(nèi)存屏障分為兩種:
Load Barrier
和Store Barrier
即讀屏障和寫(xiě)屏障?!緝?nèi)存屏障是硬件層的】
為什么需要內(nèi)存屏障
由于現(xiàn)代操作系統(tǒng)都是多處理器操作系統(tǒng),每個(gè)處理器都會(huì)有自己的緩存,可能存再不同處理器緩存不一致的問(wèn)題,而且由于操作系統(tǒng)可能存在重排序,導(dǎo)致讀取到錯(cuò)誤的數(shù)據(jù),因此,操作系統(tǒng)提供了一些內(nèi)存屏障以解決這種問(wèn)題.簡(jiǎn)單來(lái)說(shuō):1.在不同CPU執(zhí)行的不同線程對(duì)同一個(gè)變量的緩存值不同,為了解決這個(gè)問(wèn)題。2.用volatile可以解決上面的問(wèn)題,不同硬件對(duì)內(nèi)存屏障的實(shí)現(xiàn)方式不一樣。java屏蔽掉這些差異,通過(guò)jvm生成內(nèi)存屏障的指令。 對(duì)于讀屏障:在指令前插入讀屏障,可以讓高速緩存中的數(shù)據(jù)失效,強(qiáng)制從主內(nèi)存取。
內(nèi)存屏障的作用
cpu執(zhí)行指令可能是無(wú)序的,它有兩個(gè)比較重要的作用 1.阻止屏障兩側(cè)指令重排序 2.強(qiáng)制把寫(xiě)緩沖區(qū)/高速緩存中的臟數(shù)據(jù)等寫(xiě)回主內(nèi)存,讓緩存中相應(yīng)的數(shù)據(jù)失效。
volatile型變量
當(dāng)我們聲明某個(gè)變量為volatile修飾時(shí),這個(gè)變量就有了線程可見(jiàn)性,volatile通過(guò)在讀寫(xiě)操作前后添加內(nèi)存屏障。
用代碼可以這么理解
//相當(dāng)于讀寫(xiě)時(shí)加鎖,保證及時(shí)可見(jiàn)性,并發(fā)時(shí)不被隨意修改。public class SynchronizedInteger { private long value; public synchronized int get() { return value; } public synchronized void set(long value) { this.value = value; }}
volatile型變量擁有如下特性
可見(jiàn)性,對(duì)于一個(gè)該變量的讀,一定能看到讀之前最后的寫(xiě)入。 防止指令重排序,執(zhí)行代碼時(shí),為了提高執(zhí)行效率,會(huì)在不影響最后結(jié)果的前提下對(duì)指令進(jìn)行重新排序,使用volatile可以防止,比如單例模式雙重校驗(yàn)鎖的創(chuàng)建中有使用到,如(https://www.jianshu.com/p/b30a4d568be4)注意的是volatile不具有原子性,如volatile++這樣的復(fù)合操作,這里感謝大家的指正。
至于volatile底層是怎么實(shí)現(xiàn)保證不同線程可見(jiàn)性的,這里涉及到的就是硬件上的,被volatile修飾的變量在進(jìn)行寫(xiě)操作時(shí),會(huì)生成一個(gè)特殊的匯編指令,該指令會(huì)觸發(fā)mesi協(xié)議,會(huì)存在一個(gè)總線嗅探機(jī)制的東西,簡(jiǎn)單來(lái)說(shuō)就是這個(gè)cpu會(huì)不停檢測(cè)總線中該變量的變化,如果該變量一旦變化了,由于這個(gè)嗅探機(jī)制,其它c(diǎn)pu會(huì)立馬將該變量的cpu緩存數(shù)據(jù)清空掉,重新的去從主內(nèi)存拿到這個(gè)數(shù)據(jù)。簡(jiǎn)單畫(huà)了個(gè)圖。
2. Java內(nèi)存區(qū)域
前提:本文講的基本都是以Sun HotSpot虛擬機(jī)為基礎(chǔ)的,Oracle收購(gòu)了Sun后目前得到了兩個(gè)【Sun的HotSpot和JRockit(以后可能合并這兩個(gè)),還有一個(gè)是IBM的IBMJVM】
之所以扯了那么多計(jì)算機(jī)內(nèi)存模型,是因?yàn)閖ava內(nèi)存模型的設(shè)定符合了計(jì)算機(jī)的規(guī)范。
Java程序內(nèi)存的分配是在JVM虛擬機(jī)內(nèi)存分配機(jī)制下完成。
Java內(nèi)存模型(Java Memory Model ,JMM)就是一種符合內(nèi)存模型規(guī)范的,屏蔽了各種硬件和操作系統(tǒng)的訪問(wèn)差異的,保證了Java程序在各種平臺(tái)下對(duì)內(nèi)存的訪問(wèn)都能保證效果一致的機(jī)制及規(guī)范。
簡(jiǎn)要言之,jmm是jvm的一種規(guī)范,定義了jvm的內(nèi)存模型。它屏蔽了各種硬件和操作系統(tǒng)的訪問(wèn)差異,不像c那樣直接訪問(wèn)硬件內(nèi)存,相對(duì)安全很多,它的主要目的是解決由于多線程通過(guò)共享內(nèi)存進(jìn)行通信時(shí),存在的本地內(nèi)存數(shù)據(jù)不一致、編譯器會(huì)對(duì)代碼指令重排序、處理器會(huì)對(duì)代碼亂序執(zhí)行等帶來(lái)的問(wèn)題??梢员WC并發(fā)編程場(chǎng)景中的原子性、可見(jiàn)性和有序性。
從下面這張圖可以看出來(lái),Java數(shù)據(jù)區(qū)域分為五大數(shù)據(jù)區(qū)域。這些區(qū)域各有各的用途,創(chuàng)建及銷毀時(shí)間。
其中方法區(qū)和堆是所有線程共享的,棧,本地方法棧和程序虛擬機(jī)則為線程私有的。
根據(jù)java虛擬機(jī)規(guī)范,java虛擬機(jī)管理的內(nèi)存將分為下面五大區(qū)域。
2.1 五大內(nèi)存區(qū)域
2.1.1 程序計(jì)數(shù)器
程序計(jì)數(shù)器是一塊很小的內(nèi)存空間,它是線程私有的,可以認(rèn)作為當(dāng)前線程的行號(hào)指示器。
為什么需要程序計(jì)數(shù)器
我們知道對(duì)于一個(gè)處理器(如果是多核cpu那就是一核),在一個(gè)確定的時(shí)刻都只會(huì)執(zhí)行一條線程中的指令,一條線程中有多個(gè)指令,為了線程切換可以恢復(fù)到正確執(zhí)行位置,每個(gè)線程都需有獨(dú)立的一個(gè)程序計(jì)數(shù)器,不同線程之間的程序計(jì)數(shù)器互不影響,獨(dú)立存儲(chǔ)。
注意:如果線程執(zhí)行的是個(gè)java方法,那么計(jì)數(shù)器記錄虛擬機(jī)字節(jié)碼指令的地址。如果為native【底層方法】,那么計(jì)數(shù)器為空。這塊內(nèi)存區(qū)域是虛擬機(jī)規(guī)范中唯一沒(méi)有OutOfMemoryError的區(qū)域。
2.1.2 Java棧(虛擬機(jī)棧)
同計(jì)數(shù)器也為線程私有,生命周期與相同,就是我們平時(shí)說(shuō)的棧,棧描述的是Java方法執(zhí)行的內(nèi)存模型。
每個(gè)方法被執(zhí)行的時(shí)候都會(huì)創(chuàng)建一個(gè)棧幀用于存儲(chǔ)局部變量表,操作棧,動(dòng)態(tài)鏈接,方法出口等信息。每一個(gè)方法被調(diào)用的過(guò)程就對(duì)應(yīng)一個(gè)棧幀在虛擬機(jī)棧中從入棧到出棧的過(guò)程?!緱O冗M(jìn)后出,下圖棧1先進(jìn)最后出來(lái)】
對(duì)于棧幀的解釋參考 Java虛擬機(jī)運(yùn)行時(shí)棧幀結(jié)構(gòu)
棧幀: 是用來(lái)存儲(chǔ)數(shù)據(jù)和部分過(guò)程結(jié)果的數(shù)據(jù)結(jié)構(gòu)。 棧幀的位置: 內(nèi)存 -> 運(yùn)行時(shí)數(shù)據(jù)區(qū) -> 某個(gè)線程對(duì)應(yīng)的虛擬機(jī)棧 -> here[在這里]棧幀大小確定時(shí)間: 編譯期確定,不受運(yùn)行期數(shù)據(jù)影響。
通常有人將java內(nèi)存區(qū)分為棧和堆,實(shí)際上java內(nèi)存比這復(fù)雜,這么區(qū)分可能是因?yàn)槲覀冏铌P(guān)注,與對(duì)象內(nèi)存分配關(guān)系最密切的是這兩個(gè)。
平時(shí)說(shuō)的棧一般指局部變量表部分。
局部變量表:一片連續(xù)的內(nèi)存空間,用來(lái)存放方法參數(shù),以及方法內(nèi)定義的局部變量,存放著編譯期間已知的數(shù)據(jù)類型(八大基本類型和對(duì)象引用(reference類型),returnAddress類型。它的最小的局部變量表空間單位為Slot,虛擬機(jī)沒(méi)有指明Slot的大小,但在jvm中,long和double類型數(shù)據(jù)明確規(guī)定為64位,這兩個(gè)類型占2個(gè)Slot,其它基本類型固定占用1個(gè)Slot。
reference類型:與基本類型不同的是它不等同本身,即使是String,內(nèi)部也是char數(shù)組組成,它可能是指向一個(gè)對(duì)象起始位置指針,也可能指向一個(gè)代表對(duì)象的句柄或其他與該對(duì)象有關(guān)的位置。
returnAddress類型:指向一條字節(jié)碼指令的地址【深入理解Java虛擬機(jī)】怎么理解returnAddress
需要注意的是,局部變量表所需要的內(nèi)存空間在編譯期完成分配,當(dāng)進(jìn)入一個(gè)方法時(shí),這個(gè)方法在棧中需要分配多大的局部變量空間是完全確定的,在方法運(yùn)行期間不會(huì)改變局部變量表大小。
Java虛擬機(jī)??赡艹霈F(xiàn)兩種類型的異常:
線程請(qǐng)求的棧深度大于虛擬機(jī)允許的棧深度,將拋出StackOverflowError。
虛擬機(jī)??臻g可以動(dòng)態(tài)擴(kuò)展,當(dāng)動(dòng)態(tài)擴(kuò)展是無(wú)法申請(qǐng)到足夠的空間時(shí),拋出OutOfMemory異常。
2.1.3 本地方法棧
本地方法棧是與虛擬機(jī)棧發(fā)揮的作用十分相似,區(qū)別是虛擬機(jī)棧執(zhí)行的是Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧則為虛擬機(jī)使用到的native方法服務(wù),可能底層調(diào)用的c或者c++,我們打開(kāi)jdk安裝目錄可以看到也有很多用c編寫(xiě)的文件,可能就是native方法所調(diào)用的c代碼。
2.1.4 堆
對(duì)于大多數(shù)應(yīng)用來(lái)說(shuō),堆是java虛擬機(jī)管理內(nèi)存最大的一塊內(nèi)存區(qū)域,因?yàn)槎汛娣诺膶?duì)象是線程共享的,所以多線程的時(shí)候也需要同步機(jī)制。因此需要重點(diǎn)了解下。
java虛擬機(jī)規(guī)范對(duì)這塊的描述是:所有對(duì)象實(shí)例及數(shù)組都要在堆上分配內(nèi)存,但隨著JIT編譯器的發(fā)展和逃逸分析技術(shù)的成熟,這個(gè)說(shuō)法也不是那么絕對(duì),但是大多數(shù)情況都是這樣的。
即時(shí)編譯器:可以把把Java的字節(jié)碼,包括需要被解釋的指令的程序)轉(zhuǎn)換成可以直接發(fā)送給處理器的指令的程序)
逃逸分析:通過(guò)逃逸分析來(lái)決定某些實(shí)例或者變量是否要在堆中進(jìn)行分配,如果開(kāi)啟了逃逸分析,即可將這些變量直接在棧上進(jìn)行分配,而非堆上進(jìn)行分配。這些變量的指針可以被全局所引用,或者其其它線程所引用。
注意:它是所有線程共享的,它的目的是存放對(duì)象實(shí)例。同時(shí)它也是GC所管理的主要區(qū)域,因此常被稱為GC堆,又由于現(xiàn)在收集器常使用分代算法,Java堆中還可以細(xì)分為新生代和老年代,再細(xì)致點(diǎn)還有Eden(伊甸園)空間之類的不做深究。
根據(jù)虛擬機(jī)規(guī)范,Java堆可以存在物理上不連續(xù)的內(nèi)存空間,就像磁盤(pán)空間只要邏輯是連續(xù)的即可。它的內(nèi)存大小可以設(shè)為固定大小,也可以擴(kuò)展。
當(dāng)前主流的虛擬機(jī)如HotPot都能按擴(kuò)展實(shí)現(xiàn)(通過(guò)設(shè)置 -Xmx和-Xms),如果堆中沒(méi)有內(nèi)存內(nèi)存完成實(shí)例分配,而且堆無(wú)法擴(kuò)展將報(bào)OOM錯(cuò)誤(OutOfMemoryError)
2.1.5 方法區(qū)
方法區(qū)同堆一樣,是所有線程共享的內(nèi)存區(qū)域,為了區(qū)分堆,又被稱為非堆。
用于存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量,如static修飾的變量加載類的時(shí)候就被加載到方法區(qū)中。
運(yùn)行時(shí)常量池
是方法區(qū)的一部分,class文件除了有類的字段、接口、方法等描述信息之外,還有常量池用于存放編譯期間生成的各種字面量和符號(hào)引用。
在老版jdk,方法區(qū)也被稱為永久代【因?yàn)闆](méi)有強(qiáng)制要求方法區(qū)必須實(shí)現(xiàn)垃圾回收,HotSpot虛擬機(jī)以永久代來(lái)實(shí)現(xiàn)方法區(qū),從而JVM的垃圾收集器可以像管理堆區(qū)一樣管理這部分區(qū)域,從而不需要專門為這部分設(shè)計(jì)垃圾回收機(jī)制。不過(guò)自從JDK7之后,Hotspot虛擬機(jī)便將運(yùn)行時(shí)常量池從永久代移除了。】
jdk1.7開(kāi)始逐步去永久代。從String.interns()方法可以看出來(lái) String.interns()native方法:作用是如果字符串常量池已經(jīng)包含一個(gè)等于這個(gè)String對(duì)象的字符串,則返回代表池中的這個(gè)字符串的String對(duì)象,在jdk1.6及以前常量池分配在永久代中??赏ㄟ^(guò) -XX:PermSize和-XX:MaxPermSize限制方法區(qū)大小。
public class StringIntern { //運(yùn)行如下代碼探究運(yùn)行時(shí)常量池的位置 public static void main(String[] args) throws Throwable { //用list保持著引用 防止full gc回收常量池 List<String> list = new ArrayList<String>(); int i = 0; while(true){ list.add(String.valueOf(i++).intern()); } }}//如果在jdk1.6環(huán)境下運(yùn)行 同時(shí)限制方法區(qū)大小 將報(bào)OOM后面跟著PermGen space說(shuō)明方法區(qū)OOM,即常量池在永久代//如果是jdk1.7或1.8環(huán)境下運(yùn)行 同時(shí)限制堆的大小 將報(bào)heap space 即常量池在堆中
idea設(shè)置相關(guān)內(nèi)存大小設(shè)置
這邊不用全局的方式,設(shè)置main方法的vm參數(shù)。
做相關(guān)設(shè)置,比如說(shuō)這邊設(shè)定堆大小。(-Xmx5m -Xms5m -XX:-UseGCOverheadLimit)
這邊如果不設(shè)置UseGCOverheadLimit將報(bào)java.lang.OutOfMemoryError: GC overhead limit exceeded, 這個(gè)錯(cuò)是因?yàn)镚C占用了多余98%(默認(rèn)值)的CPU時(shí)間卻只回收了少于2%(默認(rèn)值)的堆空間。目的是為了讓?xiě)?yīng)用終止,給開(kāi)發(fā)者機(jī)會(huì)去診斷問(wèn)題。一般是應(yīng)用程序在有限的內(nèi)存上創(chuàng)建了大量的臨時(shí)對(duì)象或者弱引用對(duì)象,從而導(dǎo)致該異常。雖然加大內(nèi)存可以暫時(shí)解決這個(gè)問(wèn)題,但是還是強(qiáng)烈建議去優(yōu)化代碼,后者更加有效,也可通過(guò)UseGCOverheadLimit避免[不推薦,這里是因?yàn)闇y(cè)試用,并不能解決根本問(wèn)題]
jdk8真正開(kāi)始廢棄永久代,而使用元空間(Metaspace)
java虛擬機(jī)對(duì)方法區(qū)比較寬松,除了跟堆一樣可以不存在連續(xù)的內(nèi)存空間,定義空間和可擴(kuò)展空間,還可以選擇不實(shí)現(xiàn)垃圾收集。
2.2 對(duì)象的內(nèi)存布局
在HotSpot虛擬機(jī)中。對(duì)象在內(nèi)存中存儲(chǔ)的布局分為
1.對(duì)象頭 2.實(shí)例數(shù)據(jù) 3.對(duì)齊填充
2.2.1 對(duì)象頭【markword】
在32位系統(tǒng)下,對(duì)象頭8字節(jié),64位則是16個(gè)字節(jié)【未開(kāi)啟壓縮指針,開(kāi)啟后12字節(jié)】。
markword很像網(wǎng)絡(luò)協(xié)議報(bào)文頭,劃分為多個(gè)區(qū)間,并且會(huì)根據(jù)對(duì)象的狀態(tài)復(fù)用自己的存儲(chǔ)空間。 為什么這么做:省空間,對(duì)象需要存儲(chǔ)的數(shù)據(jù)很多,32bit/64bit是不夠的,它被設(shè)計(jì)成非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間存儲(chǔ)更多的信息,
假設(shè)當(dāng)前為32bit,在對(duì)象未被鎖定情況下。25bit為存儲(chǔ)對(duì)象的哈希碼、4bit用于存儲(chǔ)分代年齡,2bit用于存儲(chǔ)鎖標(biāo)志位,1bit固定為0。
不同狀態(tài)下存放數(shù)據(jù)
這其中鎖標(biāo)識(shí)位需要特別關(guān)注下。鎖標(biāo)志位與是否為偏向鎖對(duì)應(yīng)到唯一的鎖狀態(tài)。
鎖的狀態(tài)分為四種無(wú)鎖狀態(tài)
、偏向鎖
、輕量級(jí)鎖
和重量級(jí)鎖
不同狀態(tài)時(shí)對(duì)象頭的區(qū)間含義,如圖所示。
HotSpot底層通過(guò)markOop實(shí)現(xiàn)Mark Word,具體實(shí)現(xiàn)位于markOop.hpp
文件。
markOop中提供了大量方法用于查看當(dāng)前對(duì)象頭的狀態(tài),以及更新對(duì)象頭的數(shù)據(jù),為synchronized鎖的實(shí)現(xiàn)提供了基礎(chǔ)。[比如說(shuō)我們知道synchronized鎖的是對(duì)象而不是代碼,而鎖的狀態(tài)保存在對(duì)象頭中,進(jìn)而實(shí)現(xiàn)鎖住對(duì)象]。
關(guān)于對(duì)象頭和鎖之間的轉(zhuǎn)換,網(wǎng)上大神總結(jié)
2.2.2 實(shí)例數(shù)據(jù)
存放對(duì)象程序中各種類型的字段類型,不管是從父類中繼承下來(lái)的還是在子類中定義的。 分配策略:相同寬度的字段總是放在一起,比如double和long
2.2.3 對(duì)齊填充
這部分沒(méi)有特殊的含義,僅僅起到占位符的作用滿足JVM要求。
由于HotSpot規(guī)定對(duì)象的大小必須是8的整數(shù)倍,對(duì)象頭剛好是整數(shù)倍,如果實(shí)例數(shù)據(jù)不是的話,就需要占位符對(duì)齊填充。
2.3 對(duì)象的訪問(wèn)定位
java程序需要通過(guò)引用(ref)數(shù)據(jù)來(lái)操作堆上面的對(duì)象,那么如何通過(guò)引用定位、訪問(wèn)到對(duì)象的具體位置。
對(duì)象的訪問(wèn)方式由虛擬機(jī)決定,java虛擬機(jī)提供兩種主流的方式 1.句柄訪問(wèn)對(duì)象 2.直接指針訪問(wèn)對(duì)象。(Sun HotSpot使用這種方式)
2.3.1 句柄訪問(wèn)
簡(jiǎn)單來(lái)說(shuō)就是java堆劃出一塊內(nèi)存作為句柄池,引用中存儲(chǔ)對(duì)象的句柄地址,句柄中包含對(duì)象實(shí)例數(shù)據(jù)、類型數(shù)據(jù)的地址信息。
優(yōu)點(diǎn):引用中存儲(chǔ)的是穩(wěn)定的句柄地址,在對(duì)象被移動(dòng)【垃圾收集時(shí)移動(dòng)對(duì)象是常態(tài)】只需改變句柄中實(shí)例數(shù)據(jù)的指針,不需要改動(dòng)引用【ref】本身。
2.3.2 直接指針
與句柄訪問(wèn)不同的是,ref中直接存儲(chǔ)的就是對(duì)象的實(shí)例數(shù)據(jù),但是類型數(shù)據(jù)跟句柄訪問(wèn)方式一樣。
優(yōu)點(diǎn):優(yōu)勢(shì)很明顯,就是速度快,相比于句柄訪問(wèn)少了一次指針定位的開(kāi)銷時(shí)間。【可能是出于Java中對(duì)象的訪問(wèn)時(shí)十分頻繁的,平時(shí)我們常用的JVM HotSpot采用此種方式】
3.內(nèi)存溢出
兩種內(nèi)存溢出異常[注意內(nèi)存溢出是error級(jí)別的] 1.StackOverFlowError:當(dāng)請(qǐng)求的棧深度大于虛擬機(jī)所允許的最大深度 2.OutOfMemoryError:虛擬機(jī)在擴(kuò)展棧時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)存空間[一般都能設(shè)置擴(kuò)大]
java -verbose:class -version 可以查看剛開(kāi)始加載的類,可以發(fā)現(xiàn)這兩個(gè)類并不是異常出現(xiàn)的時(shí)候才去加載,而是jvm啟動(dòng)的時(shí)候就已經(jīng)加載。這么做的原因是在vm啟動(dòng)過(guò)程中我們把類加載起來(lái),并創(chuàng)建幾個(gè)沒(méi)有堆棧的對(duì)象緩存起來(lái),只需要設(shè)置下不同的提示信息即可,當(dāng)需要拋出特定類型的OutOfMemoryError異常的時(shí)候,就直接拿出緩存里的這幾個(gè)對(duì)象就可以了。
比如說(shuō)OutOfMemoryError對(duì)象,jvm預(yù)留出4個(gè)對(duì)象【固定常量】,這就為什么最多出現(xiàn)4次有堆棧的OutOfMemoryError異常及大部分情況下都將看到?jīng)]有堆棧的OutOfMemoryError對(duì)象的原因。
兩個(gè)基本的例子
public class MemErrorTest { public static void main(String[] args) { try { List<Object> list = new ArrayList<Object>(); for(;;) { list.add(new Object()); //創(chuàng)建對(duì)象速度可能高于jvm回收速度 } } catch (OutOfMemoryError e) { e.printStackTrace(); } try { hi();//遞歸造成StackOverflowError 這邊因?yàn)槊窟\(yùn)行一個(gè)方法將創(chuàng)建一個(gè)棧幀,棧幀創(chuàng)建太多無(wú)法繼續(xù)申請(qǐng)到內(nèi)存擴(kuò)展 } catch (StackOverflowError e) { e.printStackTrace(); } } public static void hi() { hi(); }}
4.GC簡(jiǎn)介
GC(Garbage Collection):即垃圾回收器,誕生于1960年MIT的Lisp語(yǔ)言,主要是用來(lái)回收,釋放垃圾占用的空間。
java GC泛指java的垃圾回收機(jī)制,該機(jī)制是java與C/C++的主要區(qū)別之一,我們?cè)谌粘?xiě)java代碼的時(shí)候,一般都不需要編寫(xiě)內(nèi)存回收或者垃圾清理的代碼,也不需要像C/C++那樣做類似delete/free的操作。
4.1.為什么需要學(xué)習(xí)GC
對(duì)象的內(nèi)存分配在java虛擬機(jī)的自動(dòng)內(nèi)存分配機(jī)制下,一般不容易出現(xiàn)內(nèi)存泄漏問(wèn)題。但是寫(xiě)代碼難免會(huì)遇到一些特殊情況,比如OOM神馬的。。盡管虛擬機(jī)內(nèi)存的動(dòng)態(tài)分配與內(nèi)存回收技術(shù)很成熟,可萬(wàn)一出現(xiàn)了這樣那樣的內(nèi)存溢出問(wèn)題,那么將難以定位錯(cuò)誤的原因所在。
對(duì)于本人來(lái)說(shuō),由于水平有限,而且作為小開(kāi)發(fā),并沒(méi)必要深入到GC的底層實(shí)現(xiàn),但至少想要說(shuō)學(xué)會(huì)看懂gc及定位一些內(nèi)存泄漏問(wèn)題。
從三個(gè)角度切入來(lái)學(xué)習(xí)GC
1.哪些內(nèi)存要回收
2.什么時(shí)候回收
3.怎么回收
哪些內(nèi)存要回收
java內(nèi)存模型中分為五大區(qū)域已經(jīng)有所了解。我們知道
程序計(jì)數(shù)器
、虛擬機(jī)棧
、本地方法棧
,由線程而生,隨線程而滅,其中棧中的棧幀隨著方法的進(jìn)入順序的執(zhí)行的入棧和出棧的操作,一個(gè)棧幀需要分配多少內(nèi)存取決于具體的虛擬機(jī)實(shí)現(xiàn)并且在編譯期間即確定下來(lái)【忽略JIT編譯器做的優(yōu)化,基本當(dāng)成編譯期間可知】,當(dāng)方法或線程執(zhí)行完畢后,內(nèi)存就隨著回收,因此無(wú)需關(guān)心。而
Java堆
、方法區(qū)
則不一樣。方法區(qū)存放著類加載信息,但是一個(gè)接口中多個(gè)實(shí)現(xiàn)類需要的內(nèi)存可能不太一樣,一個(gè)方法中多個(gè)分支需要的內(nèi)存也可能不一樣【只有在運(yùn)行期間才可知道這個(gè)方法創(chuàng)建了哪些對(duì)象沒(méi)需要多少內(nèi)存】,這部分內(nèi)存的分配和回收都是動(dòng)態(tài)的,gc關(guān)注的也正是這部分的內(nèi)存。
Java堆是GC回收的“重點(diǎn)區(qū)域”。堆中基本存放著所有對(duì)象實(shí)例,gc進(jìn)行回收前,第一件事就是確認(rèn)哪些對(duì)象存活,哪些死去[即不可能再被引用]
4.2 堆的回收區(qū)域
為了高效的回收,jvm將堆分為三個(gè)區(qū)域 1.新生代(Young Generation)NewSize和MaxNewSize分別可以控制年輕代的初始大小和最大的大小 2.老年代(Old Generation) 3.永久代(Permanent Generation)【1.8以后采用元空間,就不在堆中了】
5 判斷對(duì)象是否存活算法
1.引用計(jì)數(shù)算法 早期判斷對(duì)象是否存活大多都是以這種算法,這種算法判斷很簡(jiǎn)單,簡(jiǎn)單來(lái)說(shuō)就是給對(duì)象添加一個(gè)引用計(jì)數(shù)器,每當(dāng)對(duì)象被引用一次就加1,引用失效時(shí)就減1。當(dāng)為0的時(shí)候就判斷對(duì)象不會(huì)再被引用。 優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單效率高,被廣泛使用與如python何游戲腳本語(yǔ)言上。 缺點(diǎn):難以解決循環(huán)引用的問(wèn)題,就是假如兩個(gè)對(duì)象互相引用已經(jīng)不會(huì)再被其它其它引用,導(dǎo)致一直不會(huì)為0就無(wú)法進(jìn)行回收。 2.可達(dá)性分析算法 目前主流的商用語(yǔ)言[如java、c#]采用的是可達(dá)性分析算法判斷對(duì)象是否存活。這個(gè)算法有效解決了循環(huán)利用的弊端。 它的基本思路是通過(guò)一個(gè)稱為“GC Roots”的對(duì)象為起始點(diǎn),搜索所經(jīng)過(guò)的路徑稱為引用鏈,當(dāng)一個(gè)對(duì)象到GC Roots沒(méi)有任何引用跟它連接則證明對(duì)象是不可用的。
可作為GC Roots的對(duì)象有四種
①虛擬機(jī)棧(棧楨中的本地變量表)中的引用的對(duì)象。 ②方法區(qū)中的類靜態(tài)屬性引用的對(duì)象,一般指被static修飾引用的對(duì)象,加載類的時(shí)候就加載到內(nèi)存中。 ③方法區(qū)中的常量引用的對(duì)象,④本地方法棧中JNI(native方法)引用的對(duì)象
即使可達(dá)性算法中不可達(dá)的對(duì)象,也不是一定要馬上被回收,還有可能被搶救一下。網(wǎng)上例子很多,基本上和深入理解JVM一書(shū)講的一樣對(duì)象的生存還是死亡
要真正宣告對(duì)象死亡需經(jīng)過(guò)兩個(gè)過(guò)程。 1.可達(dá)性分析后沒(méi)有發(fā)現(xiàn)引用鏈 2.查看對(duì)象是否有finalize方法,如果有重寫(xiě)且在方法內(nèi)完成自救[比如再建立引用],還是可以搶救一下,注意這邊一個(gè)類的finalize只執(zhí)行一次,這就會(huì)出現(xiàn)一樣的代碼第一次自救成功第二次失敗的情況。[如果類重寫(xiě)finalize且還沒(méi)調(diào)用過(guò),會(huì)將這個(gè)對(duì)象放到一個(gè)叫做F-Queue的序列里,這邊f(xié)inalize不承諾一定會(huì)執(zhí)行,這么做是因?yàn)槿绻锩嫠姥h(huán)的話可能會(huì)時(shí)F-Queue隊(duì)列處于等待,嚴(yán)重會(huì)導(dǎo)致內(nèi)存崩潰,這是我們不希望看到的。]
HotSpot虛擬機(jī)如何實(shí)現(xiàn)可達(dá)性算法
5 垃圾收集算法
jvm中,可達(dá)性分析算法幫我們解決了哪些對(duì)象可以回收的問(wèn)題,垃圾收集算法則關(guān)心怎么回收。
5.1 三大垃圾收集算法
1.標(biāo)記/清除算法【最基礎(chǔ)】2.復(fù)制算法3.標(biāo)記/整理算法 jvm采用`分代收集算法`對(duì)不同區(qū)域采用不同的回收算法。
新生代采用復(fù)制算法
新生代中因?yàn)閷?duì)象都是"朝生夕死的",【深入理解JVM虛擬機(jī)上說(shuō)98%的對(duì)象,不知道是不是這么多,總之就是存活率很低】,適用于復(fù)制算法【復(fù)制算法比較適合用于存活率低的內(nèi)存區(qū)域】。它優(yōu)化了標(biāo)記/清除算法的效率和內(nèi)存碎片問(wèn)題,且JVM不以5:5分配內(nèi)存【由于存活率低,不需要復(fù)制保留那么大的區(qū)域造成空間上的浪費(fèi),因此不需要按1:1【原有區(qū)域:保留空間】劃分內(nèi)存區(qū)域,而是將內(nèi)存分為一塊Eden空間和From Survivor、To Survivor【保留空間】,三者默認(rèn)比例為8:1:1,優(yōu)先使用Eden區(qū),若Eden區(qū)滿,則將對(duì)象復(fù)制到第二塊內(nèi)存區(qū)上。但是不能保證每次回收都只有不多于10%的對(duì)象存貨,所以Survivor區(qū)不夠的話,則會(huì)依賴?yán)夏甏甏孢M(jìn)行分配】。
GC開(kāi)始時(shí),對(duì)象只會(huì)存于Eden和From Survivor區(qū)域,To Survivor【保留空間】為空。
GC進(jìn)行時(shí),Eden區(qū)所有存活的對(duì)象都被復(fù)制到To Survivor區(qū),而From Survivor區(qū)中,仍存活的對(duì)象會(huì)根據(jù)它們的年齡值決定去向,年齡值達(dá)到年齡閾值(默認(rèn)15是因?yàn)閷?duì)象頭中年齡戰(zhàn)4bit,新生代每熬過(guò)一次垃圾回收,年齡+1),則移到老年代,沒(méi)有達(dá)到則復(fù)制到To Survivor。
老年代采用標(biāo)記/清除算法
或標(biāo)記/整理算法
由于老年代存活率高,沒(méi)有額外空間給他做擔(dān)保,必須使用這兩種算法。
5.2 枚舉根節(jié)點(diǎn)算法
GC Roots
被虛擬機(jī)用來(lái)判斷對(duì)象是否存活
可作為GC Roos的節(jié)點(diǎn)主要是在一些全局引用【如常量或靜態(tài)屬性】、執(zhí)行上下文【如棧幀中本地變量表】中。那么如何在這么多全局變量和本地變量表找到【枚舉】根節(jié)點(diǎn)將是個(gè)問(wèn)題。
可達(dá)性分析算法需考慮
1.如果方法區(qū)幾百兆,一個(gè)個(gè)檢查里面的引用,將耗費(fèi)大量資源。
2.在分析時(shí),需保證這個(gè)對(duì)象引用關(guān)系不再變化,否則結(jié)果將不準(zhǔn)確?!疽虼薌C進(jìn)行時(shí)需停掉其它所有java執(zhí)行線程(Sun把這種行為稱為‘Stop the World’),即使是號(hào)稱幾乎不會(huì)停頓的CMS收集器,枚舉根節(jié)點(diǎn)時(shí)也需停掉線程】
解決辦法:實(shí)際上當(dāng)系統(tǒng)停下來(lái)后JVM不需要一個(gè)個(gè)檢查引用,而是通過(guò)OopMap數(shù)據(jù)結(jié)構(gòu)【HotSpot的叫法】來(lái)標(biāo)記對(duì)象引用。
虛擬機(jī)先得知哪些地方存放對(duì)象的引用,在類加載完時(shí)。HotSpot把對(duì)象內(nèi)什么偏移量什么類型的數(shù)據(jù)算出來(lái),在jit編譯過(guò)程中,也會(huì)在特定位置記錄下棧和寄存器哪些位置是引用,這樣GC在掃描時(shí)就可以知道這些信息。【目前主流JVM使用準(zhǔn)確式GC】
OopMap可以幫助HotSpot快速且準(zhǔn)確完成GC Roots枚舉以及確定相關(guān)信息。但是也存在一個(gè)問(wèn)題,可能導(dǎo)致引用關(guān)系變化。
這個(gè)時(shí)候有個(gè)safepoint(安全點(diǎn))的概念。
HotSpot中GC不是在任意位置都可以進(jìn)入,而只能在safepoint處進(jìn)入。 GC時(shí)對(duì)一個(gè)Java線程來(lái)說(shuō),它要么處在safepoint,要么不在safepoint。
safepoint不能太少,否則GC等待的時(shí)間會(huì)很久
safepoint不能太多,否則將增加運(yùn)行GC的負(fù)擔(dān)
安全點(diǎn)主要存放的位置
1:循環(huán)的末尾 2:方法臨返回前/調(diào)用方法的call指令后 3:可能拋異常的位置
6.垃圾收集器
如果說(shuō)垃圾回收算法是內(nèi)存回收的方法論,那么垃圾收集器就是具體實(shí)現(xiàn)。jvm會(huì)結(jié)合針對(duì)不同的場(chǎng)景及用戶的配置使用不同的收集器。
年輕代收集器 Serial、ParNew、Parallel Scavenge 老年代收集器 Serial Old、Parallel Old、CMS收集器 特殊收集器 G1收集器[新型,不在年輕、老年代范疇內(nèi)]
新生代收集器
6.1 Serial
最基本、發(fā)展最久的收集器,在jdk3以前是gc收集器的唯一選擇,Serial是單線程收集器,Serial收集器只能使用一條線程進(jìn)行收集工作,在收集的時(shí)候必須得停掉其它線程,等待收集工作完成其它線程才可以繼續(xù)工作。
雖然Serial看起來(lái)很坑,需停掉別的線程以完成自己的gc工作,但是也不是完全沒(méi)用的,比如說(shuō)Serial在運(yùn)行在Client模式下優(yōu)于其它收集器[簡(jiǎn)單高效,不過(guò)一般都是用Server模式,64bit的jvm甚至沒(méi)Client模式]
優(yōu)點(diǎn):對(duì)于Client模式下的jvm來(lái)說(shuō)是個(gè)好的選擇。適用于單核CPU【現(xiàn)在基本都是多核了】
缺點(diǎn):收集時(shí)要暫停其它線程,有點(diǎn)浪費(fèi)資源,多核下顯得。
6.2 ParNew收集器
可以認(rèn)為是Serial的升級(jí)版,因?yàn)樗С侄嗑€程[GC線程],而且收集算法、Stop The World、回收策略和Serial一樣,就是可以有多個(gè)GC線程并發(fā)運(yùn)行,它是HotSpot第一個(gè)真正意義實(shí)現(xiàn)并發(fā)的收集器。默認(rèn)開(kāi)啟線程數(shù)和當(dāng)前cpu數(shù)量相同【幾核就是幾個(gè),超線程cpu的話就不清楚了 - -】,如果cpu核數(shù)很多不想用那么多,可以通過(guò)-XX:ParallelGCThreads來(lái)控制垃圾收集線程的數(shù)量。
優(yōu)點(diǎn):1.支持多線程,多核CPU下可以充分的利用CPU資源2.運(yùn)行在Server模式下新生代首選的收集器【重點(diǎn)是因?yàn)樾律倪@幾個(gè)收集器只有它和Serial可以配合CMS收集器一起使用】 缺點(diǎn): 在單核下表現(xiàn)不會(huì)比Serial好,由于在單核能利用多核的優(yōu)勢(shì),在線程收集過(guò)程中可能會(huì)出現(xiàn)頻繁上下文切換,導(dǎo)致額外的開(kāi)銷。
6.3 Parallel Scavenge
采用復(fù)制算法的收集器,和ParNew一樣支持多線程。
但是該收集器重點(diǎn)關(guān)心的是吞吐量【吞吐量 = 代碼運(yùn)行時(shí)間 / (代碼運(yùn)行時(shí)間 + 垃圾收集時(shí)間) 如果代碼運(yùn)行100min垃圾收集1min,則為99%】
對(duì)于用戶界面,適合使用GC停頓時(shí)間短,不然因?yàn)榭D導(dǎo)致交互界面卡頓將很影響用戶體驗(yàn)。
對(duì)于后臺(tái)
高吞吐量可以高效率的利用cpu盡快完成程序運(yùn)算任務(wù),適合后臺(tái)運(yùn)算
Parallel Scavenge注重吞吐量,所以也成為"吞吐量?jī)?yōu)先"收集器。
老年代收集器
6.4 Serial Old
和新生代的Serial一樣為單線程,Serial的老年代版本,不過(guò)它采用"標(biāo)記-整理算法",這個(gè)模式主要是給Client模式下的JVM使用。
如果是Server模式有兩大用途
1.jdk5前和Parallel Scavenge搭配使用,jdk5前也只有這個(gè)老年代收集器可以和它搭配。
2.作為CMS收集器的后備。
6.5 Parallel Old
支持多線程,Parallel Scavenge的老年版本,jdk6開(kāi)始出現(xiàn), 采用"標(biāo)記-整理算法"【老年代的收集器大都采用此算法】
在jdk6以前,新生代的Parallel Scavenge只能和Serial Old配合使用【根據(jù)圖,沒(méi)有這個(gè)的話只剩Serial Old,而Parallel Scavenge又不能和CMS配合使用】,而且Serial Old為單線程Server模式下會(huì)拖后腿【多核cpu下無(wú)法充分利用】,這種結(jié)合并不能讓?xiě)?yīng)用的吞吐量最大化。
Parallel Old的出現(xiàn)結(jié)合Parallel Scavenge,真正的形成“吞吐量?jī)?yōu)先”的收集器組合。
6.6 CMS
CMS收集器(Concurrent Mark Sweep)是以一種獲取最短回收停頓時(shí)間為目標(biāo)的收集器。【重視響應(yīng),可以帶來(lái)好的用戶體驗(yàn),被sun稱為并發(fā)低停頓收集器】
啟用CMS:-XX:+UseConcMarkSweepGC
正如其名,CMS采用的是"標(biāo)記-清除"(Mark Sweep)算法,而且是支持并發(fā)(Concurrent)的
它的運(yùn)作分為4個(gè)階段
1.初始標(biāo)記:標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對(duì)象,速度很快 2.并發(fā)標(biāo)記:GC Roots Tarcing過(guò)程,即可達(dá)性分析 3.重新標(biāo)記:為了修正因并發(fā)標(biāo)記期間用戶程序運(yùn)作而產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄,會(huì)有些許停頓,時(shí)間上一般 初始標(biāo)記 < 重新標(biāo)記 < 并發(fā)標(biāo)記 4.并發(fā)清除
以上初始標(biāo)記和重新標(biāo)記需要stw(停掉其它運(yùn)行java線程)
之所以說(shuō)CMS的用戶體驗(yàn)好,是因?yàn)镃MS收集器的內(nèi)存回收工作是可以和用戶線程一起并發(fā)執(zhí)行。
總體上CMS是款優(yōu)秀的收集器,但是它也有些缺點(diǎn)。
1.cms堆cpu特別敏感,cms運(yùn)行線程和應(yīng)用程序并發(fā)執(zhí)行需要多核cpu,如果cpu核數(shù)多的話可以發(fā)揮它并發(fā)執(zhí)行的優(yōu)勢(shì),但是cms默認(rèn)配置啟動(dòng)的時(shí)候垃圾線程數(shù)為 (cpu數(shù)量+3)/4,它的性能很容易受cpu核數(shù)影響,當(dāng)cpu的數(shù)目少的時(shí)候比如說(shuō)為為2核,如果這個(gè)時(shí)候cpu運(yùn)算壓力比較大,還要分一半給cms運(yùn)作,這可能會(huì)很大程度的影響到計(jì)算機(jī)性能。
2.cms無(wú)法處理浮動(dòng)垃圾,可能導(dǎo)致Concurrent Mode Failure(并發(fā)模式故障)而觸發(fā)full GC
3.由于cms是采用"標(biāo)記-清除“算法,因此就會(huì)存在垃圾碎片的問(wèn)題,為了解決這個(gè)問(wèn)題cms提供了 -XX:+UseCMSCompactAtFullCollection選項(xiàng),這個(gè)選項(xiàng)相當(dāng)于一個(gè)開(kāi)關(guān)【默認(rèn)開(kāi)啟】,用于CMS頂不住要進(jìn)行full GC時(shí)開(kāi)啟內(nèi)存碎片合并,內(nèi)存整理的過(guò)程是無(wú)法并發(fā)的,且開(kāi)啟這個(gè)選項(xiàng)會(huì)影響性能(比如停頓時(shí)間變長(zhǎng))
浮動(dòng)垃圾:由于cms支持運(yùn)行的時(shí)候用戶線程也在運(yùn)行,程序運(yùn)行的時(shí)候會(huì)產(chǎn)生新的垃圾,這里產(chǎn)生的垃圾就是浮動(dòng)垃圾,cms無(wú)法當(dāng)次處理,得等下次才可以。
6.7 G1收集器
G1(garbage first:盡可能多收垃圾,避免full gc)收集器是當(dāng)前最為前沿的收集器之一(1.7以后才開(kāi)始有),同cms一樣也是關(guān)注降低延遲,是用于替代cms功能更為強(qiáng)大的新型收集器,因?yàn)樗鉀Q了cms產(chǎn)生空間碎片等一系列缺陷。
摘自甲骨文:適用于 Java HotSpot VM 的低暫停、服務(wù)器風(fēng)格的分代式垃圾回收器。G1 GC 使用并發(fā)和并行階段實(shí)現(xiàn)其目標(biāo)暫停時(shí)間,并保持良好的吞吐量。當(dāng) G1 GC 確定有必要進(jìn)行垃圾回收時(shí),它會(huì)先收集存活數(shù)據(jù)最少的區(qū)域(垃圾優(yōu)先)
g1的特別之處在于它強(qiáng)化了分區(qū),弱化了分代的概念,是區(qū)域化、增量式的收集器,它不屬于新生代也不屬于老年代收集器。
用到的算法為標(biāo)記-清理、復(fù)制算法
jdk1.7,1.8的都是默認(rèn)關(guān)閉的,更高版本的還不知道 開(kāi)啟選項(xiàng) -XX:+UseG1GC 比如在tomcat的catania.sh啟動(dòng)參數(shù)加上
g1是區(qū)域化的,它將java堆內(nèi)存劃分為若干個(gè)大小相同的區(qū)域【region】,jvm可以設(shè)置每個(gè)region的大小(1-32m,大小得看堆內(nèi)存大小,必須是2的冪),它會(huì)根據(jù)當(dāng)前的堆內(nèi)存分配合理的region大小。
jdk7中計(jì)算region的源碼,這邊博主看了下也看不怎么懂,也翻了下openjdk8的看了下關(guān)于region的處理似乎不太一樣。。
g1通過(guò)并發(fā)(并行)標(biāo)記階段查找老年代存活對(duì)象,通過(guò)并行復(fù)制壓縮存活對(duì)象【這樣可以省出連續(xù)空間供大對(duì)象使用】。
g1將一組或多組區(qū)域中存活對(duì)象以增量并行的方式復(fù)制到不同區(qū)域進(jìn)行壓縮,從而減少堆碎片,目標(biāo)是盡可能多回收堆空間【垃圾優(yōu)先】,且盡可能不超出暫停目標(biāo)以達(dá)到低延遲的目的。
g1提供三種垃圾回收模式 young gc、mixed gc 和 full gc,不像其它的收集器,根據(jù)區(qū)域而不是分代,新生代老年代的對(duì)象它都能回收。
幾個(gè)重要的默認(rèn)值,更多的查看官方文檔oracle官方g1中文文檔
g1是自適應(yīng)的回收器,提供了若干個(gè)默認(rèn)值,無(wú)需修改就可高效運(yùn)作 -XX:G1HeapRegionSize=n 設(shè)置g1 region大小,不設(shè)置的話自己會(huì)根據(jù)堆大小算,目標(biāo)是根據(jù)最小堆內(nèi)存劃分2048個(gè)區(qū)域 -XX:MaxGCPauseMillis=200 最大停頓時(shí)間 默認(rèn)200毫秒
7 Minor GC、Major GC、FULL GC、mixed gc
7.1 Minor GC
在年輕代
Young space
(包括Eden區(qū)和Survivor區(qū))中的垃圾回收稱之為 Minor GC,Minor GC只會(huì)清理年輕代.
7.2 Major GC
Major GC清理老年代(old GC),但是通常也可以指和Full GC是等價(jià),因?yàn)槭占夏甏臅r(shí)候往往也會(huì)伴隨著升級(jí)年輕代,收集整個(gè)Java堆。所以有人問(wèn)的時(shí)候需問(wèn)清楚它指的是full GC還是old GC。
7.3 Full GC
full gc是對(duì)新生代、老年代、永久代【jdk1.8后沒(méi)有這個(gè)概念了】統(tǒng)一的回收。
【知乎R大的回答:收集整個(gè)堆,包括young gen、old gen、perm gen(如果存在的話)、元空間(1.8及以上)等所有部分的模式】
7.4 mixed GC【g1特有】
混合GC
收集整個(gè)young gen以及部分old gen的GC。只有G1有這個(gè)模式
8 查看GC日志
8.1 簡(jiǎn)單日志查看
要看得懂并理解GC,需要看懂GC日志。
這邊我在idea上試了個(gè)小例子,需要在idea配置參數(shù)(-XX:+PrintGCDetails)。
public class GCtest { public static void main(String[] args) { for(int i = 0; i < 10000; i++) { List<String> list = new ArrayList<>(); list.add("aaaaaaaaaaaaa"); } System.gc(); }}
[GC (System.gc()) [PSYoungGen: 3998K->688K(38400K)] 3998K->696K(125952K), 0.0016551 secs[本次回收時(shí)間]] [Times: user=0.01 sys=0.00, real=0.00 secs] [Full GC (System.gc()) [PSYoungGen: 688K->0K(38400K)] [ParOldGen: 8K->603K(87552K)] 696K->603K(125952K), [Metaspace: 3210K->3210K(1056768K)], 0.0121034 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] Heap PSYoungGen[年輕代] total 38400K, used 333K [0x0000000795580000, 0x0000000798000000, 0x00000007c0000000) eden space 33280K, 1% used [0x0000000795580000,0x00000007955d34a8,0x0000000797600000) from space 5120K, 0% used [0x0000000797600000,0x0000000797600000,0x0000000797b00000) to space 5120K, 0% used [0x0000000797b00000,0x0000000797b00000,0x0000000798000000) ParOldGen[老年代] total 87552K, used 603K [0x0000000740000000, 0x0000000745580000, 0x0000000795580000) object space 87552K, 0% used [0x0000000740000000,0x0000000740096fe8,0x0000000745580000) Metaspace[元空間] used 3217K, capacity 4496K, committed 4864K, reserved 1056768K class space used 352K, capacity 388K, committed 512K, reserved 1048576K
8.2 離線工具查看
比如sun的gchisto,gcviewer離線分析工具,做個(gè)筆記先了解下還沒(méi)用過(guò),可視化好像很好用的樣子。
8.3 自帶的jconsole工具、jstat命令
終端輸入jconsole就會(huì)出現(xiàn)jdk自帶的gui監(jiān)控工具
可以根據(jù)內(nèi)存使用情況間接了解內(nèi)存使用和gc情況
jstat命令
比如jstat -gcutil pid查看對(duì)應(yīng)java進(jìn)程gc情況
s0: 新生代survivor space0簡(jiǎn)稱 就是準(zhǔn)備復(fù)制的那塊 單位為%s1:指新生代s1已使用百分比,為0的話說(shuō)明沒(méi)有存活對(duì)象到這邊e:新生代eden(伊甸園)區(qū)域(%)o:老年代(%)ygc:新生代 次數(shù)ygct:minor gc耗時(shí)fgct:full gc耗時(shí)(秒)GCT: ygct+fgct 耗時(shí)
幾個(gè)疑問(wèn)
1.GC是怎么判斷對(duì)象是被標(biāo)記的
通過(guò)枚舉根節(jié)點(diǎn)的方式,通過(guò)jvm提供的一種oopMap的數(shù)據(jù)結(jié)構(gòu),簡(jiǎn)單來(lái)說(shuō)就是不要再通過(guò)去遍歷內(nèi)存里的東西,而是通過(guò)OOPMap的數(shù)據(jù)結(jié)構(gòu)去記錄該記錄的信息,比如說(shuō)它可以不用去遍歷整個(gè)棧,而是掃描棧上面引用的信息并記錄下來(lái)。
總結(jié):通過(guò)OOPMap把棧上代表引用的位置全部記錄下來(lái),避免全棧掃描,加快枚舉根節(jié)點(diǎn)的速度,除此之外還有一個(gè)極為重要的作用,可以幫HotSpot實(shí)現(xiàn)準(zhǔn)確式GC【這邊的準(zhǔn)確關(guān)鍵就是類型,可以根據(jù)給定位置的某塊數(shù)據(jù)知道它的準(zhǔn)確類型,HotSpot是通過(guò)oopMap外部記錄下這些信息,存成映射表一樣的東西】。
2.什么時(shí)候觸發(fā)GC
簡(jiǎn)單來(lái)說(shuō),觸發(fā)的條件就是GC算法區(qū)域滿了或?qū)M了。
minor GC(young GC):當(dāng)年輕代中eden區(qū)分配滿的時(shí)候觸發(fā)[值得一提的是因?yàn)閥oung GC后部分存活的對(duì)象會(huì)已到老年代(比如對(duì)象熬過(guò)15輪),所以過(guò)后old gen的占用量通常會(huì)變高] full GC:①手動(dòng)調(diào)用System.gc()方法 [增加了full GC頻率,不建議使用而是讓jvm自己管理內(nèi)存,可以設(shè)置-XX:+ DisableExplicitGC來(lái)禁止RMI調(diào)用System.gc] ②發(fā)現(xiàn)perm gen(如果存在永久代的話)需分配空間但已經(jīng)沒(méi)有足夠空間 ③老年代空間不足,比如說(shuō)新生代的大對(duì)象大數(shù)組晉升到老年代就可能導(dǎo)致老年代空間不足。 ④CMS GC時(shí)出現(xiàn)Promotion Faield[pf] ⑤統(tǒng)計(jì)得到的Minor GC晉升到舊生代的平均大小大于老年代的剩余空間。 這個(gè)比較難理解,這是HotSpot為了避免由于新生代晉升到老年代導(dǎo)致老年代空間不足而觸發(fā)的FUll GC。 比如程序第一次觸發(fā)Minor GC后,有5m的對(duì)象晉升到老年代,姑且現(xiàn)在平均算5m,那么下次Minor GC發(fā)生時(shí),先判斷現(xiàn)在老年代剩余空間大小是否超過(guò)5m,如果小于5m,則HotSpot則會(huì)觸發(fā)full GC(這點(diǎn)挺智能的)
Promotion Faield:minor GC時(shí) survivor space放不下[滿了或?qū)ο筇骫,對(duì)象只能放到老年代,而老年代也放不下會(huì)導(dǎo)致這個(gè)錯(cuò)誤。 Concurrent Model Failure:cms時(shí)特有的錯(cuò)誤,因?yàn)閏ms時(shí)垃圾清理和用戶線程可以是并發(fā)執(zhí)行的,如果在清理的過(guò)程中 可能原因: 1 cms觸發(fā)太晚,可以把XX:CMSInitiatingOccupancyFraction調(diào)小[比如-XX:CMSInitiatingOccupancyFraction=70 是指設(shè)定CMS在對(duì)內(nèi)存占用率達(dá)到70%的時(shí)候開(kāi)始GC(因?yàn)镃MS會(huì)有浮動(dòng)垃圾,所以一般都較早啟動(dòng)GC)] 2 垃圾產(chǎn)生速度大于清理速度,可能是晉升閾值設(shè)置過(guò)小,Survivor空間小導(dǎo)致跑到老年代,eden區(qū)太小,存在大對(duì)象、數(shù)組對(duì)象等情況 3.空間碎片過(guò)多,可以開(kāi)啟空間碎片整理并合理設(shè)置周期時(shí)間
full gc導(dǎo)致了concurrent mode failure,而不是因?yàn)閏oncurrent mode failure錯(cuò)誤導(dǎo)致觸發(fā)full gc,真正觸發(fā)full gc的原因可能是ygc時(shí)發(fā)生的promotion failure。
3.cms收集器是否會(huì)掃描年輕代
會(huì),在初始標(biāo)記的時(shí)候會(huì)掃描新生代。
雖然cms是老年代收集器,但是我們知道年輕代的對(duì)象是可以晉升為老年代的,為了空間分配擔(dān)保,還是有必要去掃描年輕代。
4.什么是空間分配擔(dān)保
在minor gc前,jvm會(huì)先檢查老年代最大可用空間是否大于新生代所有對(duì)象總空間,如果是的話,則minor gc可以確保是安全的,
如果擔(dān)保失敗,會(huì)檢查一個(gè)配置(HandlePromotionFailire),即是否允許擔(dān)保失敗。
如果允許:繼續(xù)檢查老年代最大可用可用的連續(xù)空間是否大于之前晉升的平均大小,比如說(shuō)剩10m,之前每次都有9m左右的新生代到老年代,那么將嘗試一次minor gc(大于的情況),這會(huì)比較冒險(xiǎn)。
如果不允許,而且還小于的情況,則會(huì)觸發(fā)full gc?!緸榱吮苊饨?jīng)常full GC 該參數(shù)建議打開(kāi)】
這邊為什么說(shuō)是冒險(xiǎn)是因?yàn)閙inor gc過(guò)后如果出現(xiàn)大對(duì)象,由于新生代采用復(fù)制算法,survivor無(wú)法容納將跑到老年代,所以才會(huì)去計(jì)算之前的平均值作為一種擔(dān)保的條件與老年代剩余空間比較,這就是分配擔(dān)保。
這種擔(dān)保是動(dòng)態(tài)概率的手段,但是也有可能出現(xiàn)之前平均都比較低,突然有一次minor gc對(duì)象變得很多遠(yuǎn)高于以往的平均值,這個(gè)時(shí)候就會(huì)導(dǎo)致?lián)J 綡andle Promotion Failure】,這就只好再失敗后再觸發(fā)一次FULL GC,
5.為什么復(fù)制算法要分兩個(gè)Survivor,而不直接移到老年代
這樣做的話效率可能會(huì)更高,但是old區(qū)一般都是熬過(guò)多次可達(dá)性分析算法過(guò)后的存活的對(duì)象,要求比較苛刻且空間有限,而不能直接移過(guò)去,這將導(dǎo)致一系列問(wèn)題(比如老年代容易被撐爆)
分兩個(gè)Survivor(from/to),自然是為了保證復(fù)制算法運(yùn)行以提高效率。
6.各個(gè)版本的JVM使用的垃圾收集器是怎么樣的
準(zhǔn)確來(lái)說(shuō),垃圾收集器的使用跟當(dāng)前jvm也有很大的關(guān)系,比如說(shuō)g1是jdk7以后的版本才開(kāi)始出現(xiàn)。
并不是所有的垃圾收集器都是默認(rèn)開(kāi)啟的,有些得通過(guò)設(shè)置相應(yīng)的開(kāi)關(guān)參數(shù)才會(huì)使用。比如說(shuō)cms,需設(shè)置(XX:+UseConcMarkSweepGC)
這邊有幾個(gè)實(shí)用的命令,比如說(shuō)server模式下
#UnlockExperimentalVMOptions UnlockDiagnosticVMOptions解鎖獲取jvm參數(shù),PrintFlagsFinal用于輸出xx相關(guān)參數(shù),以Benchmark類測(cè)試,這邊會(huì)有很多結(jié)果 大都看不懂- - 在這邊查(usexxxxxxgc會(huì)看到j(luò)vm不同收集器的開(kāi)關(guān)情況) java -server -XX:+UnlockExperimentalVMOptions -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal Benchmark #后面跟| grep ":"獲取已賦值的參數(shù)[加:代表被賦值過(guò)] java -server -XX:+UnlockExperimentalVMOptions -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal Benchmark| grep ":" #獲得用戶自定義的設(shè)置或者jvm設(shè)置的詳細(xì)的xx參數(shù)和值 java -server -XX:+PrintCommandLineFlags Benchmark
本人用的jdk8,這邊UseParallelGC為true,參考深入理解jvm那本書(shū)說(shuō)這個(gè)是Parallel Scavenge+Serial old搭配組合的開(kāi)關(guān),但是網(wǎng)上又說(shuō)8默認(rèn)是Parallel Scavenge+Parallel Old,我還是信書(shū)的吧 - -。
更多相關(guān)參數(shù)來(lái)源
據(jù)說(shuō)更高版本的jvm默認(rèn)使用g1
7 stop the world具體是什么,有沒(méi)有辦法避免
stop the world簡(jiǎn)單來(lái)說(shuō)就是gc的時(shí)候,停掉除gc外的java線程。
無(wú)論什么gc都難以避免停頓,即使是g1也會(huì)在初始標(biāo)記階段發(fā)生,stw并不可怕,可以盡可能的減少停頓時(shí)間。
8 新生代什么樣的情況會(huì)晉升為老年代
對(duì)象優(yōu)先分配在eden區(qū),eden區(qū)滿時(shí)會(huì)觸發(fā)一次minor GC
對(duì)象晉升規(guī)則
1 長(zhǎng)期存活的對(duì)象進(jìn)入老年代,對(duì)象每熬過(guò)一次GC年齡+1(默認(rèn)年齡閾值15,可配置)。
2 對(duì)象太大新生代無(wú)法容納則會(huì)分配到老年代
3 eden區(qū)滿了,進(jìn)行minor gc后,eden和一個(gè)survivor區(qū)仍然存活的對(duì)象無(wú)法放到(to survivor區(qū))則會(huì)通過(guò)分配擔(dān)保機(jī)制放到老年代,這種情況一般是minor gc后新生代存活的對(duì)象太多。
4 動(dòng)態(tài)年齡判定,為了使內(nèi)存分配更靈活,jvm不一定要求對(duì)象年齡達(dá)到MaxTenuringThreshold(15)才晉升為老年代,若survior區(qū)相同年齡對(duì)象總大小大于survior區(qū)空間的一半,則大于等于這個(gè)年齡的對(duì)象將會(huì)在minor gc時(shí)移到老年代
8.怎么理解g1,適用于什么場(chǎng)景
G1 GC 是區(qū)域化、并行-并發(fā)、增量式垃圾回收器,相比其他 HotSpot 垃圾回收器,可提供更多可預(yù)測(cè)的暫停。增量的特性使 G1 GC 適用于更大的堆,在最壞的情況下仍能提供不錯(cuò)的響應(yīng)。G1 GC 的自適應(yīng)特性使 JVM 命令行只需要軟實(shí)時(shí)暫停時(shí)間目標(biāo)的最大值以及 Java 堆大小的最大值和最小值,即可開(kāi)始工作。
g1不再區(qū)分老年代、年輕代這樣的內(nèi)存空間,這是較以往收集器很大的差異,所有的內(nèi)存空間就是一塊劃分為不同子區(qū)域,每個(gè)區(qū)域大小為1m-32m,最多支持的內(nèi)存為64g左右,且由于它為了的特性適用于大內(nèi)存機(jī)器。
適用場(chǎng)景:
1.像cms能與應(yīng)用程序并發(fā)執(zhí)行,GC停頓短【短而且可控】,用戶體驗(yàn)好的場(chǎng)景。
2.面向服務(wù)端,大內(nèi)存,高cpu的應(yīng)用機(jī)器?!揪W(wǎng)上說(shuō)差不多是6g或更大】
3.應(yīng)用在運(yùn)行過(guò)程中經(jīng)常會(huì)產(chǎn)生大量?jī)?nèi)存碎片,需要壓縮空間【比cms好的地方之一,g1具備壓縮功能】。
參考
深入理解Java虛擬機(jī)
JVM內(nèi)存模型、指令重排、內(nèi)存屏障概念解析
JVM 垃圾回收 Minor gc vs Major gc vs Full gc
總結(jié)
JMM 是一種規(guī)范,是解決由于多線程通過(guò)共享內(nèi)存進(jìn)行通信時(shí),存在的本地內(nèi)存數(shù)據(jù)不一致、編譯器會(huì)對(duì)代碼指令重排序、處理器會(huì)對(duì)代碼亂序執(zhí)行等帶來(lái)的問(wèn)題,而且寫(xiě)java代碼的時(shí)候難免會(huì)經(jīng)常和內(nèi)存打交道,遇到各種內(nèi)存溢出問(wèn)題,有時(shí)候又難以定位問(wèn)題,因此是一定要學(xué)習(xí)jmm以及GC的。
由于博主本人水平有限【目前還是小菜雞】,所以花了點(diǎn)時(shí)間,寫(xiě)下這篇博客當(dāng)做為筆記總結(jié)歸納,但是寫(xiě)博客這種事如果全都是照抄別人的成果就很沒(méi)意思了,吸收別人的成果的同時(shí),也希望自己有能力多寫(xiě)點(diǎn)自己獨(dú)特的理解和干貨后續(xù)繼續(xù)更新,所以如果有哪里寫(xiě)的不好或?qū)戝e(cuò)請(qǐng)指出,以便我繼續(xù)學(xué)習(xí)和改進(jìn)。