Java内存结构与垃圾回收机制算法分析
生活随笔
收集整理的這篇文章主要介紹了
Java内存结构与垃圾回收机制算法分析
小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.
什么是HotSpot VM
提起HotSpot VM,相信所有Java程序員都知道,它是Sun JDK和OpenJDK中所帶的虛擬機(jī),也是目前使用范圍最廣的Java虛擬機(jī)。
但不一定所有人都知道的是,這個(gè)目前看起來“血統(tǒng)純正”的虛擬機(jī)在最初并非由Sun公司開發(fā),而是由一家名為“Longview
Technologies”的小公司設(shè)計(jì)的;
甚至這個(gè)虛擬機(jī)最初并非是為Java語言而開發(fā)的,它來源于Strongtalk VM,
而這款虛擬機(jī)中相當(dāng)多的技術(shù)又是來源于一款支持Self語言實(shí)現(xiàn)“達(dá)到C語言50%以上的執(zhí)行效率”的目標(biāo)而設(shè)計(jì)的虛擬機(jī),
Sun公司注意到了這款虛擬機(jī)在JIT編譯上有許多優(yōu)秀的理念和實(shí)際效果,在1997年收購了Longview Technologies公司,
從而獲得了HotSpot VM。
HotSpot VM既繼承了Sun之前兩款商用虛擬機(jī)的優(yōu)點(diǎn)(如前面提到的準(zhǔn)確式內(nèi)存管理),也有許多自己新的技術(shù)優(yōu)勢(shì),
如它名稱中的HotSpot指的就是它的熱點(diǎn)代碼探測(cè)技術(shù)(其實(shí)兩個(gè)VM基本上是同時(shí)期的獨(dú)立產(chǎn)品,HotSpot還稍早一些,
HotSpot一開始就是準(zhǔn)確式GC,而Exact VM之中也有與HotSpot幾乎一樣的熱點(diǎn)探測(cè)。
為了Exact VM和HotSpot VM哪個(gè)成為Sun主要支持的VM產(chǎn)品,在Sun公司內(nèi)部還有過爭(zhēng)論,HotSpot打敗Exact并不能算
技術(shù)上的勝利),
HotSpot VM的熱點(diǎn)代碼探測(cè)能力可以通過執(zhí)行計(jì)數(shù)器找出最具有編譯價(jià)值的代碼,然后通知JIT編譯器以方法為單位
進(jìn)行編譯。
如果一個(gè)方法被頻繁調(diào)用,或方法中有效循環(huán)次數(shù)很多,將會(huì)分別觸發(fā)標(biāo)準(zhǔn)編譯和OSR(棧上替換)編譯動(dòng)作。
通過編譯器與解釋器恰當(dāng)?shù)貐f(xié)同工作,可以在最優(yōu)化的程序響應(yīng)時(shí)間與最佳執(zhí)行性能中取得平衡,而且無須等待本地
代碼輸出才能執(zhí)行程序,
即時(shí)編譯的時(shí)間壓力也相對(duì)減小,這樣有助于引入更多的代碼優(yōu)化技術(shù),輸出質(zhì)量更高的本地代碼。在2006年的JavaOne大會(huì)上,Sun公司宣布最終會(huì)把Java開源,并在隨后的一年,陸續(xù)將JDK的各個(gè)部分(其中當(dāng)然也
包括了HotSpot VM)在GPL協(xié)議下公開了源碼,
并在此基礎(chǔ)上建立了OpenJDK。這樣,HotSpot VM便成為了Sun JDK和OpenJDK兩個(gè)實(shí)現(xiàn)極度接近的JDK項(xiàng)目的共同虛擬機(jī)。在2008年和2009年,Oracle公司分別收購了BEA公司和Sun公司,這樣Oracle就同時(shí)擁有了兩款優(yōu)秀的Java虛擬機(jī):
JRockit VM和HotSpot VM。
Oracle公司宣布在不久的將來(大約應(yīng)在發(fā)布JDK 8的時(shí)候)會(huì)完成這兩款虛擬機(jī)的整合工作,使之優(yōu)勢(shì)互補(bǔ)。
整合的方式大致上是在HotSpot的基礎(chǔ)上,移植JRockit的優(yōu)秀特性,譬如使用JRockit的垃圾回收器與MissionControl服務(wù),
使用HotSpot的JIT編譯器與混合的運(yùn)行時(shí)系統(tǒng)。
Java內(nèi)存結(jié)構(gòu)Java堆(Java Heap)java堆是java虛擬機(jī)所管理的內(nèi)存中最大的一塊,是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。此內(nèi)存區(qū)域
的唯一目的就是存放對(duì)象實(shí)例,這一點(diǎn)在Java虛擬機(jī)規(guī)范中的描述是:所有的對(duì)象實(shí)例以及數(shù)組都要在堆上分配。
java堆是垃圾收集器管理的主要區(qū)域,因此也被成為“GC堆”(Garbage Collected Heap)。從內(nèi)存回收角度來看java堆可分為:
新生代和老生代(當(dāng)然還有更細(xì)致的劃分,在下一章會(huì)講到)。從內(nèi)存分配的角度看,線程共享的Java堆中可能劃分出多個(gè)
線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer,TLAB)。無論怎么劃分,都與存放內(nèi)容無關(guān),無論哪個(gè)區(qū)域,
存儲(chǔ)的都是對(duì)象實(shí)例,進(jìn)一步的劃分都是為了更好的回收內(nèi)存,或者更快的分配內(nèi)存。
根據(jù)Java虛擬機(jī)規(guī)范的規(guī)定,java堆可以處于物理上不連續(xù)的內(nèi)存空間中。當(dāng)前主流的虛擬機(jī)都是可擴(kuò)展的(
通過 -Xmx 和 -Xms 控制)。如果堆中沒有內(nèi)存完成實(shí)例分配,并且堆也無法再擴(kuò)展時(shí),將會(huì)拋出OutOfMemoryError異常。
Java虛擬機(jī)棧(Java Virtual Machine Stacks)
java虛擬機(jī)也是線程私有的,它的生命周期和線程相同。虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個(gè)方法在執(zhí)行的
同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。
咱們常說的堆內(nèi)存、棧內(nèi)存中,棧內(nèi)存指的就是虛擬機(jī)棧。局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型
(8個(gè)基本數(shù)據(jù)類型)、對(duì)象引用(地址指針)、returnAddress類型。
局部變量表所需的內(nèi)存空間在編譯期間完成分配。在運(yùn)行期間不會(huì)改變局部變量表的大小。
這個(gè)區(qū)域規(guī)定了兩種異常狀態(tài):如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,則拋出StackOverflowError異常;如果
虛擬機(jī)棧可以動(dòng)態(tài)擴(kuò)展,在擴(kuò)展是無法申請(qǐng)到足夠的內(nèi)存,就會(huì)拋出OutOfMemoryError異常。
本地方法棧(Native Method Stack)本地方法棧與虛擬機(jī)棧所發(fā)揮作用非常相似,它們之間的區(qū)別不過是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),
而本地方法棧則為虛擬機(jī)使用到的native方法服務(wù)。本地方法棧也是拋出兩個(gè)異常。
方法區(qū)(Method Area)
方法區(qū)與java堆一樣,是各個(gè)線程共享的內(nèi)存區(qū)域,它用于存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的
代碼等數(shù)據(jù)。它有個(gè)別命叫Non-Heap(非堆)。當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時(shí),拋出OutOfMemoryError異常。
直接內(nèi)存(Direct Memory)直接內(nèi)存不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域。但這部分區(qū)域也唄頻繁使用,而且也
可能導(dǎo)致OutOfMemoryError異常
在JDK1.4中新加入的NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的I/O方式,它可以使用
Native函數(shù)庫直接分配堆外內(nèi)存,然后通過一個(gè)存儲(chǔ)在java堆中的DirectByteBuffer對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。
運(yùn)行時(shí)常量池(Runtime Constant Pool)運(yùn)行時(shí)常量池是方法區(qū)的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池,
用于存放編譯期生成的各種字面量和符號(hào)引用,這部分內(nèi)容將在加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放。
程序計(jì)數(shù)器(Program Counter Register)
程序計(jì)數(shù)器是一塊較小的內(nèi)存空間,它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。
由于Java虛擬機(jī)的多線程是通過線程輪流切換并分配處理器執(zhí)行時(shí)間的方式來實(shí)現(xiàn)的,一個(gè)處理器都只會(huì)執(zhí)行一條線程中的指令。
因此,為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都有一個(gè)獨(dú)立的程序計(jì)數(shù)器,各個(gè)線程之間計(jì)數(shù)器互不影響,獨(dú)立存儲(chǔ)。
稱之為“線程私有”的內(nèi)存。程序計(jì)數(shù)器內(nèi)存區(qū)域是虛擬機(jī)中唯一沒有規(guī)定OutOfMemoryError情況的區(qū)域。
執(zhí)行引擎
虛擬機(jī)核心的組件就是執(zhí)行引擎,它負(fù)責(zé)執(zhí)行虛擬機(jī)的字節(jié)碼,一般戶先進(jìn)行編譯成機(jī)器碼后執(zhí)行。
垃圾收集系統(tǒng)
垃圾收集系統(tǒng)是Java的核心,也是不可少的,Java有一套自己進(jìn)行垃圾清理的機(jī)制,開發(fā)人員無需手工清理
垃圾回收機(jī)制算法分析
什么是垃圾回收機(jī)制
不定時(shí)去堆內(nèi)存中清理不可達(dá)對(duì)象。不可達(dá)的對(duì)象并不會(huì)馬上就會(huì)直接回收, 垃圾收集器在一個(gè)Java程序中的執(zhí)行是自動(dòng)的,
不能強(qiáng)制執(zhí)行,即使程序員能明確地判斷出有一塊內(nèi)存已經(jīng)無用了,是應(yīng)該回收的,程序員也不能強(qiáng)制垃圾收集器回收該內(nèi)存塊。
程序員唯一能做的就是通過調(diào)用System.gc 方法來"建議"執(zhí)行垃圾收集器,但其是否可以執(zhí)行,什么時(shí)候執(zhí)行卻都是不可知的。
這也是垃圾收集器的最主要的缺點(diǎn)。當(dāng)然相對(duì)于它給程序員帶來的巨大方便性而言,這個(gè)缺點(diǎn)是瑕不掩瑜的。
public class Test {public static void main(String[] args) {Test test = new Test();test = null;System.gc(); // 手動(dòng)回收垃圾}@Overrideprotected void finalize() throws Throwable {// gc回收垃圾之前調(diào)用System.out.println("垃圾回收機(jī)制...");}
}finalize方法作用
Java技術(shù)使用finalize()方法在垃圾收集器將對(duì)象從內(nèi)存中清除出去前,做必要的清理工作。這個(gè)方法是由垃圾收集器在確定這個(gè)對(duì)象
沒有被引用時(shí)對(duì)這個(gè)對(duì)象調(diào)用的。它是在Object類中定義的,因此所有的類都繼承了它。子類覆蓋finalize()方法以整理系統(tǒng)資源或者
執(zhí)行其他清理工作。finalize()方法是在垃圾收集器刪除對(duì)象之前對(duì)這個(gè)對(duì)象調(diào)用的。
新生代與老年代
Java 中的堆是 JVM 所管理的最大的一塊內(nèi)存空間,主要用于存放各種類的實(shí)例對(duì)象。
在 Java 中,堆被劃分成兩個(gè)不同的區(qū)域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被劃分為三個(gè)區(qū)域:Eden、
From Survivor、To Survivor。
這樣劃分的目的是為了使 JVM 能夠更好的管理堆內(nèi)存中的對(duì)象,包括內(nèi)存的分配以及回收。
堆的內(nèi)存模型大致為:(本人使用的是 JDK1.6,以下涉及的 JVM 默認(rèn)值均以該版本為準(zhǔn)。)
默認(rèn)的,新生代 ( Young ) 與老年代 ( Old ) 的比例的值為 1:2 ( 該值可以通過參數(shù) –XX:NewRatio 來指定 ),即:新生代
( Young ) = 1/3 的堆空間大小。老年代 ( Old ) = 2/3 的堆空間大小。其中,新生代 ( Young ) 被細(xì)分為 Eden 和 兩個(gè)Survivor 區(qū)域,這兩個(gè) Survivor 區(qū)域分別被命名為 from 和 to,以示區(qū)分。
默認(rèn)的,Edem : from : to = 8 : 1 : 1 ( 可以通過參數(shù) –XX:SurvivorRatio 來設(shè)定 ),即: Eden = 8/10
的新生代空間大小,from = to = 1/10 的新生代空間大小。根據(jù)垃圾回收機(jī)制的不同,Java堆有可能擁有不同的結(jié)構(gòu),最為常見的就是將整個(gè)Java堆分為
新生代和老年代。其中新生帶存放新生的對(duì)象或者年齡不大的對(duì)象,老年代則存放老年對(duì)象。
新生代分為den區(qū)、s0區(qū)、s1區(qū),s0和s1也被稱為from和to區(qū)域,他們是兩塊大小相等并且可以互相角色的空間。
絕大多數(shù)情況下,對(duì)象首先分配在eden區(qū),在新生代回收后,如果對(duì)象還存活,則進(jìn)入s0或s1區(qū),之后每經(jīng)過一次
新生代回收,如果對(duì)象存活則它的年齡就加1,對(duì)象達(dá)到一定的年齡后,則進(jìn)入老年代。如何判斷對(duì)象是否存活
引用計(jì)數(shù)法
概念
引用計(jì)數(shù)法就是如果一個(gè)對(duì)象沒有被任何引用指向,則可視之為垃圾。這種方法的缺點(diǎn)就是不能檢測(cè)到環(huán)的存在。
首先需要聲明,至少主流的Java虛擬機(jī)里面都沒有選用引用計(jì)數(shù)算法來管理內(nèi)存。
什么是引用計(jì)數(shù)算法:給對(duì)象中添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí),計(jì)數(shù)器值加1;當(dāng)引用失效時(shí),計(jì)數(shù)器值減1.
任何時(shí)刻計(jì)數(shù)器值為0的對(duì)象就是不可能再被使用的。那為什么主流的Java虛擬機(jī)里面都沒有選用這種算法呢?其中最主要的原因
是它很難解決對(duì)象之間相互循環(huán)引用的問題。根搜索算法
概念
根搜索算法的基本思路就是通過一系列名為”GC Roots”的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜索所走過的路徑稱為引用鏈
(Reference Chain),當(dāng)一個(gè)對(duì)象到GC Roots沒有任何引用鏈相連時(shí),則證明此對(duì)象是不可用的。
這個(gè)算法的基本思想是通過一系列稱為“GC Roots”的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)向下搜索,搜索所走過的路徑稱為引用鏈,
當(dāng)一個(gè)對(duì)象到GC Roots沒有任何引用鏈(即GC Roots到對(duì)象不可達(dá))時(shí),則證明此對(duì)象是不可用的。
那么問題又來了,如何選取GCRoots對(duì)象呢?在Java語言中,可以作為GCRoots的對(duì)象包括下面幾種:
(1). 虛擬機(jī)棧(棧幀中的局部變量區(qū),也叫做局部變量表)中引用的對(duì)象。
(2). 方法區(qū)中的類靜態(tài)屬性引用的對(duì)象。
(3). 方法區(qū)中常量引用的對(duì)象。
(4). 本地方法棧中JNI(Native方法)引用的對(duì)象。
下面給出一個(gè)GCRoots的例子,如下圖,為GCRoots的引用鏈。根搜索算法的基本思路就是通過一系列名為”GC Roots”的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜索所走過的路徑稱為
引用鏈(Reference Chain),當(dāng)一個(gè)對(duì)象到GC Roots沒有任何引用鏈相連時(shí),則證明此對(duì)象是不可用的。
從上圖,reference1、reference2、reference3都是GC Roots,可以看出:
reference1-> 對(duì)象實(shí)例1;
reference2-> 對(duì)象實(shí)例2;
reference3-> 對(duì)象實(shí)例4;
reference3-> 對(duì)象實(shí)例4 -> 對(duì)象實(shí)例6;
可以得出對(duì)象實(shí)例1、2、4、6都具有GC Roots可達(dá)性,也就是存活對(duì)象,不能被GC回收的對(duì)象。
而對(duì)于對(duì)象實(shí)例3、5直接雖然連通,但并沒有任何一個(gè)GC Roots與之相連,這便是GC Roots不可達(dá)的對(duì)象,這就是GC需要
回收的垃圾對(duì)象。垃圾回收機(jī)制策略標(biāo)記清除算法
概念
該算法有兩個(gè)階段。
1. 標(biāo)記階段:找到所有可訪問的對(duì)象,做個(gè)標(biāo)記
2. 清除階段:遍歷堆,把未被標(biāo)記的對(duì)象回收
應(yīng)用場(chǎng)景
該算法一般應(yīng)用于老年代,因?yàn)槔夏甏膶?duì)象生命周期比較長(zhǎng)。
優(yōu)缺點(diǎn)
標(biāo)記清除算法的優(yōu)點(diǎn)和缺點(diǎn)
1. 優(yōu)點(diǎn)
- 是可以解決循環(huán)引用的問題
- 必要時(shí)才回收(內(nèi)存不足時(shí))
2. 缺點(diǎn):
- 回收時(shí),應(yīng)用需要掛起,也就是stop the world。
- 標(biāo)記和清除的效率不高,尤其是要掃描的對(duì)象比較多的時(shí)候
- 會(huì)造成內(nèi)存碎片(會(huì)導(dǎo)致明明有內(nèi)存空間,但是由于不連續(xù),申請(qǐng)稍微大一些的對(duì)象無法做到),復(fù)制算法
概念
如果jvm使用了coping算法,一開始就會(huì)將可用內(nèi)存分為兩塊,from域和to域, 每次只是使用from域,to域則空閑著。當(dāng)from域
內(nèi)存不夠了,開始執(zhí)行GC操作,這個(gè)時(shí)候,會(huì)把from域存活的對(duì)象拷貝到to域,然后直接把from域進(jìn)行內(nèi)存清理。應(yīng)用場(chǎng)景
coping算法一般是使用在新生代中,因?yàn)樾律械膶?duì)象一般都是朝生夕死的,存活對(duì)象的數(shù)量并不多,這樣使用coping算法進(jìn)行
拷貝時(shí)效率比較高。jvm將Heap 內(nèi)存劃分為新生代與老年代,又將新生代劃分為Eden(伊甸園) 與2塊Survivor Space(幸存者區(qū)) ,
然后在Eden –>Survivor Space 以及From Survivor Space 與To Survivor Space 之間實(shí)行Copying 算法。
不過jvm在應(yīng)用coping算法時(shí),并不是把內(nèi)存按照1:1來劃分的,這樣太浪費(fèi)內(nèi)存空間了。一般的jvm都是8:1。
也即是說,Eden區(qū):From區(qū):To區(qū)域的比例是始終有90%的空間是可以用來創(chuàng)建對(duì)象的,而剩下的10%用來存放回收后存活的對(duì)象。1、當(dāng)Eden區(qū)滿的時(shí)候,會(huì)觸發(fā)第一次young gc,把還活著的對(duì)象拷貝到Survivor From區(qū);當(dāng)Eden區(qū)再次觸發(fā)young gc的時(shí)候,
會(huì)掃描Eden區(qū)和From區(qū)域,對(duì)兩個(gè)區(qū)域進(jìn)行垃圾回收,經(jīng)過這次回收后還存活的對(duì)象,則直接復(fù)制到To區(qū)域,并將Eden和From
區(qū)域清空。
2、當(dāng)后續(xù)Eden又發(fā)生young gc的時(shí)候,會(huì)對(duì)Eden和To區(qū)域進(jìn)行垃圾回收,存活的對(duì)象復(fù)制到From區(qū)域,并將Eden和To區(qū)域清空。
3、可見部分對(duì)象會(huì)在From和To區(qū)域中復(fù)制來復(fù)制去,如此交換15次(由JVM參數(shù)MaxTenuringThreshold決定,這個(gè)參數(shù)默認(rèn)是15),
最終如果還是存活,就存入到老年代
注意: 萬一存活對(duì)象數(shù)量比較多,那么To域的內(nèi)存可能不夠存放,這個(gè)時(shí)候會(huì)借助老年代的空間。
優(yōu)缺點(diǎn)
優(yōu)點(diǎn):在存活對(duì)象不多的情況下,性能高,能解決內(nèi)存碎片和java垃圾回收算法之-標(biāo)記清除 中導(dǎo)致的引用更新問題。
缺點(diǎn): 會(huì)造成一部分的內(nèi)存浪費(fèi)。不過可以根據(jù)實(shí)際情況,將內(nèi)存塊大小比例適當(dāng)調(diào)整;如果存活對(duì)象的數(shù)量比較大,
coping的性能會(huì)變得很差。
標(biāo)記壓縮算法
標(biāo)記清除算法和標(biāo)記壓縮算法非常相同,但是標(biāo)記壓縮算法在標(biāo)記清除算法之上解決內(nèi)存碎片化
概念壓縮算法簡(jiǎn)單介紹
任意順序 : 即不考慮原先對(duì)象的排列順序,也不考慮對(duì)象之間的引用關(guān)系,隨意移動(dòng)對(duì)象;
線性順序 : 考慮對(duì)象的引用關(guān)系,例如a對(duì)象引用了b對(duì)象,則盡可能將a和b移動(dòng)到一塊;
滑動(dòng)順序 : 按照對(duì)象原來在堆中的順序滑動(dòng)到堆的一端。
優(yōu)缺點(diǎn)
優(yōu)點(diǎn):解決內(nèi)存碎片問題,缺點(diǎn)壓縮階段,由于移動(dòng)了可用對(duì)象,需要去更新引用。
分代算法
概述
這種算法,根據(jù)對(duì)象的存活周期的不同將內(nèi)存劃分成幾塊,新生代和老年代,這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴ā?可以用抓重點(diǎn)的思路來理解這個(gè)算法。
新生代對(duì)象朝生夕死,對(duì)象數(shù)量多,只要重點(diǎn)掃描這個(gè)區(qū)域,那么就可以大大提高垃圾收集的效率。另外老年代對(duì)象存儲(chǔ)久,無需經(jīng)常
掃描老年代,避免掃描導(dǎo)致的開銷。
新生代
在新生代,每次垃圾收集器都發(fā)現(xiàn)有大批對(duì)象死去,只有少量存活,采用復(fù)制算法,只需要付出少量存活對(duì)象的復(fù)制成本就可以完成收集;
可以參看我之前寫的java垃圾回收算法之-coping復(fù)制
老年代
而老年代中因?yàn)閷?duì)象存活率高、沒有額外空間對(duì)它進(jìn)行分配擔(dān)保,就必須“標(biāo)記-清除-壓縮”算法進(jìn)行回收。參看java垃圾回收算法之
-標(biāo)記_清除壓縮
新創(chuàng)建的對(duì)象被分配在新生代,如果對(duì)象經(jīng)過幾次回收后仍然存活,那么就把這個(gè)對(duì)象劃分到老年代。
老年代區(qū)存放Young區(qū)Survivor滿后觸發(fā)minor GC后仍然存活的對(duì)象,當(dāng)Eden區(qū)滿后會(huì)將存活的對(duì)象放入Survivor區(qū)域,
如果Survivor區(qū)存不下這些對(duì)象,GC收集器就會(huì)將這些對(duì)象直接存放到Old區(qū)中,如果Survivor區(qū)中的對(duì)象足夠老,
也直接存放到Old區(qū)中。如果Old區(qū)滿了,
將會(huì)觸發(fā)Full GC回收整個(gè)堆內(nèi)存。
JVM參數(shù)配置
JVM提供了諸多的參數(shù)進(jìn)行JVM各個(gè)方面內(nèi)存大小的設(shè)置,為Java應(yīng)用進(jìn)行優(yōu)化提供了諸多的工具,
本文將會(huì)詳細(xì)分析各個(gè)參數(shù)的功能與使用。
常見參數(shù)配置
-XX:+PrintGC 每次觸發(fā)GC的時(shí)候打印相關(guān)日志
-XX:+UseSerialGC 串行回收
-XX:+PrintGCDetails 更詳細(xì)的GC日志
-Xms 堆初始值
-Xmx 堆最大可用值
-Xmn 新生代堆最大可用值
-XX:SurvivorRatio 用來設(shè)置新生代中eden空間和from/to空間的比例.
-XX:NewRatio 配置新生代與老年代占比 1:2
含以-XX:SurvivorRatio=eden/from=den/to
總結(jié):在實(shí)際工作中,我們可以直接將初始的堆大小與最大堆大小相等,
這樣的好處是可以減少程序運(yùn)行時(shí)垃圾回收次數(shù),從而提高效率。
-XX:SurvivorRatio 用來設(shè)置新生代中eden空間和from/to空間的比例.
堆內(nèi)存大小配置
使用示例: -Xmx20m -Xms5m
說明: 當(dāng)下Java應(yīng)用最大可用內(nèi)存為20M, 初始內(nèi)存為5M
// byte[] b = new byte[4 * 1024 * 1024];
// System.out.println("分配了4M空間給數(shù)組");
System.out.print("最大內(nèi)存");
System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M");
System.out.print("可用內(nèi)存");
System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M");
System.out.print("已經(jīng)使用內(nèi)存");
System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");設(shè)置新生代比例參數(shù)
使用示例:-Xms20m -Xmx20m -Xmn1m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC
說明:堆內(nèi)存初始化值20m,堆內(nèi)存最大值20m,新生代最大值可用1m,eden空間和from/to空間的比例為2/1
byte[] b = null;
for (int i = 0; i < 10; i++) {
b = new byte[1 * 1024 * 1024];
}
設(shè)置新生代與老年代比例參數(shù)
使用示例: -Xms20m -Xmx20m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC
-XX:NewRatio=2
說明:堆內(nèi)存初始化值20m,堆內(nèi)存最大值20m,新生代最大值可用1m,eden空間和from/to空間的比例為2/1
新生代和老年代的占比為1/2實(shí)戰(zhàn)OutOfMemoryError異常Java堆溢出
錯(cuò)誤原因: java.lang.OutOfMemoryError: Java heap space 堆內(nèi)存溢出
解決辦法:設(shè)置堆內(nèi)存大小 // -Xms1m -Xmx10m -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
// -Xms1m -Xmx10m -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
List<Object> listObject = new ArrayList<>();
for (int i = 0; i < 10; i++) {
System.out.println("i:" + i);
Byte[] bytes = new Byte[1 * 1024 * 1024];
listObject.add(bytes);
}
System.out.println("添加成功...");虛擬機(jī)棧溢出
錯(cuò)誤原因: java.lang.StackOverflowError 棧內(nèi)存溢出
棧溢出 產(chǎn)生于遞歸調(diào)用,循環(huán)遍歷是不會(huì)的,但是循環(huán)方法里面產(chǎn)生遞歸調(diào)用, 也會(huì)發(fā)生棧溢出。
解決辦法:設(shè)置線程最大調(diào)用深度
-Xss5m 設(shè)置最大調(diào)用深度
public class JvmDemo04 {private static int count;public static void count(){try {count++;count(); } catch (Throwable e) {System.out.println("最大深度:"+count);e.printStackTrace();}}public static void main(String[] args) {count();}
}內(nèi)存溢出與內(nèi)存泄漏區(qū)別
Java內(nèi)存泄漏就是沒有及時(shí)清理內(nèi)存垃圾,導(dǎo)致系統(tǒng)無法再給你提供內(nèi)存資源(內(nèi)存資源耗盡);
而Java內(nèi)存溢出就是你要求分配的內(nèi)存超出了系統(tǒng)能給你的,系統(tǒng)不能滿足需求,于是產(chǎn)生溢出。
內(nèi)存溢出,這個(gè)好理解,說明存儲(chǔ)空間不夠大。就像倒水倒多了,從杯子上面溢出了來了一樣。
內(nèi)存泄漏,原理是,使用過的內(nèi)存空間沒有被及時(shí)釋放,長(zhǎng)時(shí)間占用內(nèi)存,最終導(dǎo)致內(nèi)存空間不足,而出現(xiàn)內(nèi)存溢出。垃圾收集器
串行與并行收集器
串行回收: JDK1.5前的默認(rèn)算法 缺點(diǎn)是只有一個(gè)線程,執(zhí)行垃圾回收時(shí)程序停止的時(shí)間比較長(zhǎng)
并行回收: 多個(gè)線程執(zhí)行垃圾回收適合于吞吐量的系統(tǒng),回收時(shí)系統(tǒng)會(huì)停止運(yùn)行serial收集器
串行收集器是最古老,最穩(wěn)定以及效率高的收集器,可能會(huì)產(chǎn)生較長(zhǎng)的停頓,只使用一個(gè)線程去回收。新生代、老年代使用串行回收;
新生代復(fù)制算法、老年代標(biāo)記-壓縮;垃圾收集的過程中會(huì)Stop The World(服務(wù)暫停)一個(gè)單線程的收集器,在進(jìn)行垃圾收集時(shí)候,必須暫停其他所有的工作線程直到它收集結(jié)束。
特點(diǎn):CPU利用率最高,停頓時(shí)間即用戶等待時(shí)間比較長(zhǎng)。
適用場(chǎng)景:小型應(yīng)用
通過JVM參數(shù)-XX:+UseSerialGC可以使用串行垃圾回收器。ParNew收集器ParNew收集器其實(shí)就是Serial收集器的多線程版本。新生代并行,老年代串行;新生代復(fù)制算法、老年代標(biāo)記-壓縮
參數(shù)控制:-XX:+UseParNewGC ParNew收集器
-XX:ParallelGCThreads 限制線程數(shù)量
parallel 收集器
Parallel Scavenge收集器類似ParNew收集器,Parallel收集器更關(guān)注系統(tǒng)的吞吐量。可以通過參數(shù)來打開自適應(yīng)調(diào)節(jié)策略,
虛擬機(jī)會(huì)根據(jù)當(dāng)前系統(tǒng)的運(yùn)行情況收集性能監(jiān)控信息,動(dòng)態(tài)調(diào)整這些參數(shù)以提供最合適的停頓時(shí)間或最大的吞吐量;
也可以通過參數(shù)控制GC的時(shí)間不大于多少毫秒或者比例;新生代復(fù)制算法、老年代標(biāo)記-壓縮采用多線程來通過掃描并壓縮堆
特點(diǎn):停頓時(shí)間短,回收效率高,對(duì)吞吐量要求高。
適用場(chǎng)景:大型應(yīng)用,科學(xué)計(jì)算,大規(guī)模數(shù)據(jù)采集等。
通過JVM參數(shù) XX:+USeParNewGC 打開并發(fā)標(biāo)記掃描垃圾回收器。cms收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器。
目前很大一部分的Java應(yīng)用都集中在互聯(lián)網(wǎng)站或B/S系統(tǒng)的服務(wù)端上,這類應(yīng)用尤其重視服務(wù)的響應(yīng)速度,
希望系統(tǒng)停頓時(shí)間最短,以給用戶帶來較好的體驗(yàn)。從名字(包含“Mark Sweep”)上就可以看出
CMS收集器是基于“標(biāo)記-清除”算法實(shí)現(xiàn)的,它的運(yùn)作過程相對(duì)于前面幾種收集器來說要更復(fù)雜一些,
整個(gè)過程分為4個(gè)步驟,包括:
初始標(biāo)記(CMS initial mark)
并發(fā)標(biāo)記(CMS concurrent mark)
重新標(biāo)記(CMS remark)
并發(fā)清除(CMS concurrent sweep)
其中初始標(biāo)記、重新標(biāo)記這兩個(gè)步驟仍然需要“Stop The World”。初始標(biāo)記僅僅只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對(duì)象,
速度很快,并發(fā)標(biāo)記階段就是進(jìn)行GC Roots Tracing的過程,而重新標(biāo)記階段則是為了修正并發(fā)標(biāo)記期間,因用戶程序繼續(xù)
運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄,這個(gè)階段的停頓時(shí)間一般會(huì)比初始標(biāo)記階段稍長(zhǎng)一些,但遠(yuǎn)比并發(fā)標(biāo)記的時(shí)間短。 由于整個(gè)過程中耗時(shí)最長(zhǎng)的并發(fā)標(biāo)記和并發(fā)清除過程中,收集器線程都可以與用戶線程一起工作,所以總體上來說,CMS收集器的內(nèi)存回收過程是與用戶線程一起并發(fā)地執(zhí)行。老年代收集器(新生代使用ParNew)優(yōu)點(diǎn):并發(fā)收集、低停頓
缺點(diǎn):產(chǎn)生大量空間碎片、并發(fā)階段會(huì)降低吞吐量采用“標(biāo)記-清除”算法實(shí)現(xiàn),使用多線程的算法去掃描堆,對(duì)發(fā)現(xiàn)未使用的對(duì)象進(jìn)行回收。
(1)初始標(biāo)記
(2)并發(fā)標(biāo)記
(3)并發(fā)預(yù)處理
(4)重新標(biāo)記
(5)并發(fā)清除
(6)并發(fā)重置
特點(diǎn):響應(yīng)時(shí)間優(yōu)先,減少垃圾收集停頓時(shí)間
適應(yīng)場(chǎng)景:大型服務(wù)器等。
通過JVM參數(shù) -XX:+UseConcMarkSweepGC設(shè)置
g1收集器
在G1中,堆被劃分成 許多個(gè)連續(xù)的區(qū)域(region)。采用G1算法進(jìn)行回收,吸收了CMS收集器特點(diǎn)。
特點(diǎn):支持很大的堆,高吞吐量--支持多CPU和垃圾回收線程--在主線程暫停的情況下,使用并行收集--在主線程運(yùn)行的情況下,使用并發(fā)收集
實(shí)時(shí)目標(biāo):可配置在N毫秒內(nèi)最多只占用M毫秒的時(shí)間進(jìn)行垃圾回收
通過JVM參數(shù) -XX:+UseG1GC 使用G1垃圾回收器
注意: 并發(fā)是指一個(gè)處理器同時(shí)處理多個(gè)任務(wù)。
并行是指多個(gè)處理器或者是多核的處理器同時(shí)處理多個(gè)不同的任務(wù)。
并發(fā)是邏輯上的同時(shí)發(fā)生(simultaneous),而并行是物理上的同時(shí)發(fā)生。
來個(gè)比喻:并發(fā)是一個(gè)人同時(shí)吃三個(gè)饅頭,而并行是三個(gè)人同時(shí)吃三個(gè)饅頭。
Tomcat配置調(diào)優(yōu)測(cè)試
Jmeter壓力測(cè)試工具
JMeter是一款在國(guó)外非常流行和受歡迎的開源性能測(cè)試工具,像LoadRunner 一樣,它也提供了一個(gè)利用本地Proxy Server
(代理服務(wù)器)來錄制生成測(cè)試腳本的功能,但是這個(gè)功能并不好用。所以在本文中介紹一個(gè)更為常用的方法——使用
Badboy錄制生成 JMeter 腳本。簡(jiǎn)單的介紹一下Badboy。Badboy是一款不錯(cuò)的Web自動(dòng)化測(cè)試工具,如果你將它用于非商業(yè)用途,
或者用于商業(yè)用途但是安裝Badboy 的機(jī)器數(shù)量不超過5臺(tái),你是不需要為它支付任何費(fèi)用的。也許是一種推廣策略,
Badboy提供了將Web測(cè)試腳本直接導(dǎo)出生成JMeter 腳本的功能,
并且這個(gè)功能非常好用,也非常簡(jiǎn)單。你可以跟著下面的試驗(yàn)步驟來邁出你在開源世界的第一步。
1. 通過Badboy的官方網(wǎng)站下載Badboy的最新版本;
2. 安裝Badboy。安裝過程同一般的Windows 應(yīng)用程序沒有什么區(qū)別,安裝完成后你可以在桌面和Windows開始菜單中
看到相應(yīng)的快捷方式——如果找不到,可以找一下Badboy安裝目錄下的Badboy.exe 文件,直接雙擊啟動(dòng)Badboy;
3. 啟動(dòng)Badboy,你可以看到下面的界面。在地址欄(圖中紅色方框標(biāo)注的部分)中輸入你需要錄制的Web應(yīng)用的URL——這里我們以http://www.yahoo.com 為例,
并點(diǎn)擊GO 按鈕開始錄制。如果你用過LoadRunner之類的商業(yè)工具,對(duì)于這個(gè)操作一定不會(huì)陌生吧 ^_^
4. 開始錄制后,你可以直接在Badboy內(nèi)嵌的瀏覽器(主界面的右側(cè))中對(duì)被測(cè)應(yīng)用進(jìn)行操作,所有的操作都會(huì)被記錄在
主界面左側(cè)的編輯窗口中——在這個(gè)試驗(yàn)中,我們?cè)赮ahoo的搜索引擎中輸入 JMeter 進(jìn)行搜索。不過你將看到,錄制下來的腳本
并不是一行行的代碼,
而是一個(gè)個(gè)Web對(duì)象——這就有點(diǎn)像LoadRunner的VuGen中的Tree View視圖;
5. 錄制完成后,點(diǎn)擊工具欄中的“停止錄制”按鈕,完成腳本的錄制;
6. 選擇“File -> Export to JMeter”菜單,填寫文件名“l(fā)ogin_mantis.jmx”,將錄制好腳本導(dǎo)出為JMeter腳本格式。
也可以選擇“File -> Save”菜單保存為Badboy腳本;
7. 啟動(dòng)JMeter并打開剛剛生成的測(cè)試腳本。
也許你已經(jīng)急不可待的準(zhǔn)備開始嘗試著用JMeter處理你手頭的工作了^_^ 在下面的幾節(jié),我將繼續(xù)為大家介紹如何在 JMeter
中完成一個(gè)測(cè)試場(chǎng)景的設(shè)置和JMeter測(cè)試結(jié)果分析入門,以及如何參數(shù)化JMeter腳本。
當(dāng)然,如果你的動(dòng)手能力很強(qiáng),幾分鐘你就可以熟悉這些內(nèi)容。不過還是請(qǐng)?jiān)试S我一點(diǎn)點(diǎn)由淺入深的來幫大家完成“JMeter
從入門到精通”的過程。
我相信在這個(gè)過程中你將會(huì)了解到更多有關(guān)性能測(cè)試的知識(shí)和經(jīng)驗(yàn),甚至包括一些LoadRunner等商業(yè)測(cè)試工具所無法提供給
你的經(jīng)驗(yàn)。
什么是吞吐量
QPS:Queries Per Second意思是“每秒查詢率”,是一臺(tái)服務(wù)器每秒能夠相應(yīng)的查詢次數(shù),是對(duì)一個(gè)特定的查詢服務(wù)器在
規(guī)定時(shí)間內(nèi)所處理
流量多少的衡量標(biāo)準(zhǔn)。
測(cè)試串行吞吐量
-XX:+PrintGCDetails -Xmx32M -Xms1M
-XX:+HeapDumpOnOutOfMemoryError
-XX:+UseSerialGC
-XX:PermSize=32M
GC 回收次數(shù)25次 吞吐量4662
堆的初始值和堆的最大一致
加大初始堆內(nèi)存大小-Xms1M 修改為32mGC 回收次數(shù)7次 吞吐量5144擴(kuò)大堆的內(nèi)存
-XX:+PrintGCDetails -Xmx512M -Xms32M
-XX:+HeapDumpOnOutOfMemoryError
-XX:+UseSerialGC
-XX:PermSize=32MGC 回收次數(shù)6次 吞吐量5141
結(jié)論:垃圾回收次數(shù)和設(shè)置最大堆內(nèi)存大小無關(guān),只和初始內(nèi)存有關(guān)系。
初始內(nèi)存會(huì)影響吞吐量。調(diào)整初始堆
-XX:+PrintGCDetails -Xmx512M –Xms512M
-XX:+HeapDumpOnOutOfMemoryError
-XX:+UseSerialGC
-XX:PermSize=32M
GC回收次數(shù)0次 吞吐量6561次
結(jié)論:堆的初始值和最大堆內(nèi)存一致,并且初始堆越大就會(huì)高。并行回收(UseParNewGC)
-XX:+PrintGCDetails -Xmx512M -Xms512M
-XX:+HeapDumpOnOutOfMemoryError
-XX:+UseParNewGC
-XX:PermSize=32M
GC回收0次 吞吐量6800CMS收集器
-XX:+PrintGCDetails -Xmx512M -Xms512M
-XX:+HeapDumpOnOutOfMemoryError
-XX:+UseConcMarkSweepGC
-XX:PermSize=32MG1回收方式
-XX:+PrintGCDetails -Xmx512M -Xms512M
-XX:+HeapDumpOnOutOfMemoryError
-XX:+UseG1GC
-XX:PermSize=32M調(diào)優(yōu)總結(jié)
初始堆值和最大堆內(nèi)存內(nèi)存越大,吞吐量就越高。
最好使用并行收集器,因?yàn)椴⑿惺占魉俣缺却型掏铝扛?#xff0c;速度快。
設(shè)置堆內(nèi)存新生代的比例和老年代的比例最好為1:2或者1:3。
減少GC對(duì)老年代的回收。垃圾回收策略
理解gc日志
Minor GC和Full GC區(qū)別
概念:新生代 GC(Minor GC):指發(fā)生在新生代的垃圾收集動(dòng)作,因?yàn)?Java 對(duì)象大多都具
備朝生夕滅的特性,所以 Minor GC 非常頻繁,一般回收速度也比較快。老年代 GC(Major GC / Full GC):指發(fā)生在老年代的 GC,出現(xiàn)了 Major GC,經(jīng)常
會(huì)伴隨至少一次的 Minor GC(但非絕對(duì)的,在 ParallelScavenge 收集器的收集策略里
就有直接進(jìn)行 Major GC 的策略選擇過程) 。MajorGC 的速度一般會(huì)比 Minor GC 慢 10
倍以上。
Minor GC觸發(fā)機(jī)制:
當(dāng)年輕代滿時(shí)就會(huì)觸發(fā)Minor GC,這里的年輕代滿指的是Eden代滿,Survivor滿不會(huì)引發(fā)GCFull GC觸發(fā)機(jī)制:
當(dāng)年老代滿時(shí)會(huì)引發(fā)Full GC,Full GC將會(huì)同時(shí)回收年輕代、年老代,當(dāng)永久代滿時(shí)也會(huì)引發(fā)Full GC,會(huì)導(dǎo)致Class、Method
元信息的卸載其Minor 如下圖所示
虛擬機(jī)給每個(gè)對(duì)象定義了一個(gè)對(duì)象年齡(Age)計(jì)數(shù)器。如果對(duì)象在 Eden 出生并經(jīng)過第一次 Minor GC 后仍然存活,
并且能被 Survivor 容納的話,將被移動(dòng)到 Survivor 空間中,并將對(duì)象年齡設(shè)為 1。對(duì)象在 Survivor 區(qū)中每熬過一次
Minor GC,年齡就增加 1 歲,當(dāng)它的年齡增加到一定程度(默認(rèn)為 15 歲)時(shí),就會(huì)被晉升到老年代中。
對(duì)象晉升老年代的年齡閾值,可以通過參數(shù) -XX:MaxTenuringThreshold (閾值)來設(shè)置。 JVM的永久代中會(huì)發(fā)生垃圾回收么?
垃圾回收不會(huì)發(fā)生在永久代,如果永久代滿了或者是超過了臨界值,會(huì)觸發(fā)完全垃圾回收(Full GC)。如果你仔細(xì)查看
垃圾收集器的輸出信息,就會(huì)發(fā)現(xiàn)永久代也是被回收的。這就是為什么正確的永久代大小對(duì)避免Full GC是非常重要的原因。
請(qǐng)參考下Java8:從永久代到元數(shù)據(jù)區(qū)
(注:Java8中已經(jīng)移除了永久代,新加了一個(gè)叫做元數(shù)據(jù)區(qū)的native內(nèi)存區(qū))
對(duì)象優(yōu)先在eden分配
大多數(shù)情況下,對(duì)象在新生代Eden區(qū)中分配。當(dāng)Eden區(qū)沒有足夠的空間進(jìn)行分配時(shí),虛擬機(jī)將發(fā)起一次Minor GC。
虛擬機(jī)提供了-XX:+PrintGCDetails這個(gè)收集器日志參數(shù),告訴虛擬機(jī)在發(fā)生垃圾收集行為時(shí)打印內(nèi)存回收日志,
并且在進(jìn)程退出的時(shí)候輸出當(dāng)前內(nèi)存各區(qū)域的分配情況。在實(shí)際應(yīng)用中,內(nèi)存回收日志一般是打印到文件后通過日志工具進(jìn)行分析,
不過本實(shí)驗(yàn)的日志并不多,
直接閱讀就能看得很清楚。public class Test00010 {private static final int _1MB = 1024 * 1024;// -Xms20m -Xmx20m -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails// -XX:+UseSerialGCpublic static void main(String[] args) {testAllocation();}public static void testAllocation() {byte[] allocation1, allocation2, allocation3, allocation4;allocation1 = new byte[2 * _1MB];allocation2 = new byte[2 * _1MB];allocation3 = new byte[2 * _1MB];allocation4 = new byte[4 * _1MB]; // 出現(xiàn)一次Minor GC}
}大對(duì)象直接進(jìn)入老年代
長(zhǎng)期存活的對(duì)象將進(jìn)入老年代
動(dòng)態(tài)對(duì)象年齡判斷
空間分配擔(dān)保理解GC日志JDK命令行工具JDK可視化工具
垃圾收集器
?
總結(jié)
以上是生活随笔為你收集整理的Java内存结构与垃圾回收机制算法分析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 垃圾回收机制之标记压缩算法与分代算法
- 下一篇: java美元兑换,(Java实现) 美元