JVM讀書筆記篇二:碼農(nóng)的福利

開篇閑話:
c哥(c語言)統(tǒng)治著編程界達20年余久,橫跨北美、歐洲、亞洲等大陸板塊,c哥名氣大是因為他能貼近硬件、高效運行。但是c哥也有兩個讓人難受的臭毛?。孩僦羔槪ㄖ苯硬僮鲀?nèi)存,高效)不提供越界檢查工具;②自己創(chuàng)建的內(nèi)存空間,自己釋放!
java哥穿著一件藍色T恤,犀利的眼神閃閃發(fā)光,嘴唇不算厚,但是聲音卻很有穿透力:“碼農(nóng)寶寶們,跟我混,對象銷毀的事就不用你們管了,你們只需要創(chuàng)建對象就行啦”。java哥說完,攝影鏡頭竟然也顫抖了一下。

預(yù)備知識:

1、JVM運行時內(nèi)存分配
JVM讀書筆記篇一:如何管理4個G的“封地”
2、client模式和server模式

當虛擬機運行在-client模式的時候,使用的是一個代號為C1的輕量級編譯器, 而-server模式啟動的虛擬機采用相對重量級,代號為C2的編譯器。 C2比C1編譯器編譯的相對徹底,服務(wù)起來之后,性能更高。

3、并行和并發(fā)

并行(Parallel):在同一時刻有多條垃圾收集線程并行工作,當然,此時的用戶線程是處于等待狀態(tài)的。
并發(fā)(Concurrent):在一個時間段內(nèi),垃圾回收線程與用戶線程同時執(zhí)行(可能是并行,可能是交替執(zhí)行),比如用戶程序繼續(xù)運行,而垃圾回收器運行在另一個CPU上。

4、吞吐量(Throughput)

吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾回收時間)

5、安全點(SafePoint)

GC時需要在某一快照狀態(tài)下(某一時刻的運行狀態(tài)),從而保持一致性(在分析期間對象引用關(guān)系保持不變)。此時會產(chǎn)生GC停頓(因為所有java線程都被停止了),如果數(shù)據(jù)量比較大的話,在進行GC Roots Tracing(對象引用鏈)查找對象引用關(guān)系時,停頓時間會很長,在HotSpot虛擬機中,使用OopMap這個數(shù)據(jù)結(jié)構(gòu)(在編譯字節(jié)碼指令時指明偏移量+基地址的內(nèi)存物理地址處有什么引用)來存儲對象的引用。
有了這個數(shù)據(jù)結(jié)構(gòu)仍然不能解決字節(jié)碼指令執(zhí)行時導(dǎo)致的引用關(guān)系變化,所以并不會為每條指令都生成OopMap,而是在“安全點”:也就是在特定的字節(jié)碼指令處生成。安全點一般設(shè)置在“方法調(diào)用”、“循環(huán)跳轉(zhuǎn)”、“異常跳轉(zhuǎn)”等指令序列復(fù)用的地方(指令序列復(fù)用的意思是說,這些指令包含的指令流都是可以被反復(fù)調(diào)用的)

哪些內(nèi)存需要回收?

篇一介紹了java內(nèi)存運行時區(qū)域的各個部分,其中程序計數(shù)器、虛擬機棧、本地方法棧3個區(qū)域與線程同生共死,棧中的棧幀空間大小是在類結(jié)構(gòu)確定下來就已知的(最大棧深度、最大局部變量表個數(shù))。所以這3個區(qū)域的內(nèi)存分配和回收都是確定的。
方法區(qū)和堆的內(nèi)存分配是不固定的,例如一個接口中的多個實現(xiàn)類、一個方法中的多個分支所需的內(nèi)存都可能不同, 堆中的對象是在運行時才創(chuàng)建的。因此方法區(qū)和堆才是我們關(guān)注的重點。

如何判定對象是否存活?

對象存在的意義是被使用,而被使用在JVM中也有明確的定義,即對象引用。引用存在于函數(shù)棧幀中的局部變量表、操作數(shù)棧(指向堆)和常量池引用(指向方法區(qū))。我們順著引用便可以找到對象是否存活的依據(jù)了。有兩種方式可以判定:

1、引用計數(shù)法:
方式:給每個對象添加一個引用計數(shù)器,每當有一個地方引用它時,計數(shù)器加1,失效時,計數(shù)器減1;為0時進行回收。很簡單吧,但是它沒法解決循環(huán)引用,請看:

public class Main {
    public static void main(String[] args) {
        GCObject obj1 = new GCObject();//obj1的計數(shù)器值為1
        GCObject obj2 = new GCObject();//obj2的計數(shù)器值為1
        obj1.object = obj2;//obj2的計數(shù)器值為2
        obj2.object = obj1;//obj2的計數(shù)器值為2
        obj1 = null;//obj1的計數(shù)器值為1
        obj2 = null;//obj2的計數(shù)器值為1
    }
   class GCObject{
      public Object object = null;
  }
}

我們發(fā)現(xiàn)obj1和obj2永遠都不會被回收。

2、可達性分析:
從GC Roots對象開始往下搜索,當一個對象到GC Roots沒有任何引用鏈相連,也就是從GC Roots到這個對象不可達時,則證明此對象是可回收的,如下圖的object5、object6、object7三個:

可達性分析模型示意圖

哪些對象可以做為GC Roots:

①虛擬機棧中引用的對象(局部變量表)
②方法區(qū)中靜態(tài)屬性引用的對象
③方法區(qū)中常量引用的對象
④本地方法棧中JNI引用的對象

怎么回收?

1、標記-清除 算法 (Mark-Sweep)
分為標記與清除兩個階段:標記出所有需要回收的對象,然后回收所有標記的對象。不足的地方有二:
①標記和清除兩個過程效率都比較低,
②產(chǎn)生大量的空間碎片

Mark-Sweep

2、復(fù)制算法
將內(nèi)存容量均分兩份A、B,每次只使用其中一塊,當塊A內(nèi)存用完后,將存活的對象復(fù)制到塊B,清理掉塊A即可。缺點是內(nèi)存浪費了50%。

Copying

3、復(fù)制算法的改進
將內(nèi)存的比例大致劃分為8:1:1的Eden和Survival1、Survival2。當回收時,將Eden和Survival中存活的對象復(fù)制到另外一塊Survival中,這樣內(nèi)存的浪費比例只占10%。
適用范圍:垃圾回收頻次較高的新生代

內(nèi)存分配比例8:1:1

備注:如果新生代的內(nèi)存空間不足,則由老年代分配擔(dān)保(即將對象分配在老年代中)。

4、標記-整理算法
標記后,讓所有存活的對象往一端移動,然后清理掉邊界以外的內(nèi)存。


Mark-Compact

適用范圍:垃圾回收頻次較低的老年代

5、分代算法

新生代:對象創(chuàng)建后,大部分即會死去,采用復(fù)制算法;
老年代:對象存活率高,且沒有額外的擔(dān)??臻g,采用標記清理或標記整理算法

根據(jù)對象存活周期分配在不同的內(nèi)存區(qū)域

內(nèi)存分配與回收策略

1、對象優(yōu)先在Eden分配:

當Eden區(qū)沒有足夠的空間時,虛擬機發(fā)起一次Minor GC。下面展示一個案例:通過-Xms20M -Xmx20M -Xmn10M設(shè)置堆初始大小為20M,新生代10M,老年代10M;當分配第四個對象時,由于空間不足會發(fā)起一次MinorGC,通過-XX:+PrintGCDetails打印GC日志:

/**
*vm參數(shù) -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 GCTest
*/
public class GCTest {
    private static final int _1MB=1024*1024;
    public static void main(String[] args) {
        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];
    }
}
運行結(jié)果:
[GC [PSYoungGen: 7307K->480K(9216K)] 7307K->6624K(19456K), 0.0072860 secs] [Times: user=0.01 sys=0.01, real=0.00 secs]
Heap
 PSYoungGen      total 9216K, used 7143K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 87% used [0x00000000ff600000,0x00000000ffcf9fc8,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
  to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 ParOldGen       total 10240K, used 8192K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 80% used [0x00000000fec00000,0x00000000ff400010,0x00000000ff600000)
 PSPermGen       total 21504K, used 2621K [0x00000000f9a00000, 0x00000000faf00000, 0x00000000fec00000)
  object space 21504K, 12% used [0x00000000f9a00000,0x00000000f9c8f610,0x00000000faf00000)

2、大對象直接進入老年代

大對象:需要大量連續(xù)內(nèi)存空間的對象,最典型的大對象就是那種很長的字符串及數(shù)組(如上面的例子中byte數(shù)組對象)。
虛擬機提供-XX:PretenureSizeThreshold參數(shù)設(shè)置在老年代進行分配

3、長期存活的對象進入老年代

每個對象都有一個年齡(age), 在Mark Word中
如果age > MaxTenuringThreshold(默認為15), 晉升老年代

4、動態(tài)對象年齡判斷:

如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半, 年齡大于或等于該年齡的對象可以直接進入老年代。

新生代——>老年代

5、方法區(qū)的回收(HotSpot虛擬機中將方法區(qū)放在永久代中,1.7已將字符串常量池從永久代中移除)

①廢棄無用的常量
例如:常量池中的字符串 “abc” 不再被任何字符串引用, 可以清除掉
②無用的類回收條件:
1、該類的所有實例都被回收;
2、加載該類的Classloader 已經(jīng)被回收;
3、該類對應(yīng)的java.lang.Class對象沒有任何地方被引用,無法在任何地方通過反射訪問該類的方法

垃圾收集器

垃圾收集器是垃圾回收算法的具體實現(xiàn),用戶體驗與運行效率之間的博弈在不同場景下都有需求,所以這些高度封裝的收集器本質(zhì)上并沒有好壞,在不同場景下,他們都能獨當一面。
以下是JDK1.7 update 14之后Hotspot虛擬機的七種收集器,分別作用于不同分代:

Hotspot7種垃圾收集器

1、Serial收集器

算法:新生代采用復(fù)制算法,暫停所有用戶線程,老年代采用標記-整理算法,暫停所有用戶線程;
特點:①單線程運行(第一層意義:它是運行在一個CPU核或一個收集線程,第二層意義:Stop the World,回收時,所有用戶線程必須停掉);②不用關(guān)心線程之間的交互,運行效率高,是client模式下默認的新生代收集器。

2、ParNew收集器
是Serial的多線程版本,可通過-XX:ParallelGCThreads參數(shù)來限定線程數(shù)量

3、Parallel Scavenge收集器
算法:復(fù)制算法;
特點:是一個新生代收集器,關(guān)注點在達到一個可控的吞吐量。有兩個參數(shù)來控制:①-XX:MaxGCPauseMills最大垃圾收集停頓時間,②-XX:GCTimeRatio直接設(shè)置吞吐量大小

4、Serial Old收集器
是Serial收集器的老年代版本,應(yīng)用于Client模式
算法:標記-整理

Serial收集器與Serial Old收集器配合
ParNew收集器與Serial Old收集器配合

5、Parallel Old收集器
Parallel Scavenge收集器的老年代版本,之前Parallel Scavenge只能與Serial Old,無法與CMS配合工作,導(dǎo)致在server模式下,由于Serial Old性能的“拖累”,導(dǎo)致整體應(yīng)用上吞吐量獲得最大化的效果。在JDK 1.6中開始提供Parallel Old配合來解決這個問題
算法:多線程+標記-整理

Parallel Scavenge與Parallel Old收集器配合

6、CMS收集器(Concurrent Low Pause Collector)
CMS收集器基于標記-清除算法,應(yīng)用于老年代。收集過程分為初始標記、并發(fā)標記、重新標記、并發(fā)清除四個階段:

①初始標記、重新標記這兩個步驟仍然需要Stop the World;
②初始標記階段僅僅是對GC Roots能直接關(guān)聯(lián)到的對象進行標記;
③并發(fā)標記進行GC Roots Tracing的過程,即按照引用鏈進行標記;
④重新標記是為了在并發(fā)標記期間因用戶線程繼續(xù)運行而導(dǎo)致標記變動的標記修正;

特點:

以獲取最短回收停頓時間為目標
互聯(lián)網(wǎng)網(wǎng)站, B/S服務(wù)器端
容易產(chǎn)生碎片

CMS收集器運行示意圖

7、G1收集器(Garbage-First)

面向服務(wù)端應(yīng)用的垃圾收集器,成熟版基于JDK1.7;
可針對年輕代和老年代;
采用標記-整理算法

特點:

充分利用多CPU、多核環(huán)境下的硬件優(yōu)勢,使用多個CPU來縮短Stop-The-World停頓時間;
G1收集器可收集新生代與老年代兩種,不需要其他收集器配合就可以獨立管理整個GC堆;
建立可預(yù)測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內(nèi),消耗在垃圾收集器上的時間不得超過N毫秒

G1收集器運行示意圖
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • JVM架構(gòu) 當一個程序啟動之前,它的class會被類裝載器裝入方法區(qū)(Permanent區(qū)),執(zhí)行引擎讀取方法區(qū)的...
    cocohaifang閱讀 1,852評論 0 7
  • 1.什么是垃圾回收? 垃圾回收(Garbage Collection)是Java虛擬機(JVM)垃圾回收器提供...
    簡欲明心閱讀 90,388評論 17 311
  • 原文閱讀 前言 這段時間懈怠了,罪過! 最近看到有同事也開始用上了微信公眾號寫博客了,挺好的~給他們點贊,這博客我...
    碼農(nóng)戲碼閱讀 6,163評論 2 31
  • http://www.cnblogs.com/angeldevil/p/3801189.html值得一看 Clas...
    snail_knight閱讀 1,621評論 1 0
  • 1.一些概念 1.1.數(shù)據(jù)類型 Java虛擬機中,數(shù)據(jù)類型可以分為兩類:基本類型和引用類型?;绢愋偷淖兞勘4嬖?..
    落落落落大大方方閱讀 4,830評論 4 86

友情鏈接更多精彩內(nèi)容