JVM和GC
JVM運行時內存區
一、線程私有數據區
1、程序計數器
在JVM中,多線程是通過線程輪流切換來獲得CPU執行時間的,因此,在任一具體時刻,一個CPU的內核只會執行一條線程中的指令,因此為了能夠使得每個線程都在線程切換后能夠恢復在切換之前的程序執行位置,每個線程都需要有自己獨立的程序計數器,并且不能相互干擾,否則就會影響到程序的正確執行次序。程序計數器中記錄的是正在執行的線程的虛擬機字節碼指令的地址,字節碼的解釋器工作的時候就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令。程序計數器是每個線程私有的。
2、虛擬機棧
虛擬機棧也就是我們常說的棧。虛擬機棧是Java方法執行的內存模型。Java棧中存放的是一個個棧幀。并且是線程私有的,生命周期與線程相同,描述的是Java方法執行的內存模型:每一個方法執行的同時都會創建一個棧幀(Stack Frame),用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法的執行就是對應棧幀在虛擬機棧中的入棧、出棧的過程。下圖表示了一個Java棧的模型:
3、本地方法棧
本地方法棧與虛擬機棧所發揮的作用很相似,他們的區別在于虛擬機棧為執行Java代碼方法服務,而本地方法棧為Native方法服務。
二、線程共享區域
1、Java堆
Java堆可以說是虛擬機中最大的一塊內存了。它是所有線程共享的內存區域,幾乎所有的實例對象都是在這塊區域中存放。堆可以處理物理上不連續的內存空間,只要邏輯上連續的就可以。當然,隨著JIT(just in time,及時編譯技術) 編譯器的發展,所有對象在"堆"上分配也變得不那么"絕對"了。同時Java堆也是垃圾收集器管理的主要區域。由于現在收集器基本上采用的都是分代收集算法,所有Java堆又可以細分為:"新生代"和"老年代"。再細致分就是把新生代分為:Eden空間、From Survivor空間、To Survivor空間。
2、方法區
方法區在JVM中也是一個非常重要的區域,在方法區中,存儲了每個類的信息(包括類的名稱、方法信息、字段信息)、靜態變量、常量以及編譯器編譯后的代碼等。它與堆一樣,是被線程共享的區域,很容易理解,我們在寫Java代碼時,每個線程都可以訪問同一個類的靜態變量。在Class文件中除了類的字段、方法、接口等描述信息外,還有一項信息是常量池,用來存儲編譯期間生成的字面量和符號引用。
垃圾回收
哪些對象需要回收
1、引用計數法:判斷對象的引用數量
引用計數法是通過判斷對象的引用數量來決定對象是否可以被回收
給對象中添加一個引用計數器,每當有一個地方引用他時,計數器值就+1,;當引用失效時,計數器值就-1;任何時刻計數器為0的對象就是不可能在被使用。
- 優點:
判定效率很高
- 確定:
不會完全準確,因為如果出現兩個對象相互引用的問題就不行了,如下圖所示:
如上圖對象A和對象B相互引用,導致他們的引用計數都不為0,那么垃圾收集器就永遠不會回收他們。
2、可達性分析算法:判斷對象的引用鏈是否可達
通過一系列的GC Roots的對象作為起始點,從這些根節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的。
上圖中,ObjD和ObjE都是不可用的,可以被GC回收掉。
在Java中,可作為 GC Root 的對象包括以下幾種:
- 虛擬機棧(棧幀中的局部變量表)中引用的對象
- 方法區中靜態屬性引用的對象
- 方法區中常量引用的對象
- 本地方法棧中Native引用的對象
垃圾收集算法
1、標記清除算法
標記清除即Mark-Sweep,是一種最簡單的收集算法。在經歷過對象判活以后,我們把需要回收的對象標記出來,然后在統一時刻回收所有被標記的對象。如圖所示:
黑色標記的可回收對象在回收后全部變成未使用空間,但是這樣回收后有木有發現空間碎片很多,碎片太多就會導致再分配稍微大點的空間時,找不到這樣的連續內存,從而導致GC會被頻繁調用,所以標記清除是一種基礎的垃圾收集算法,其它算法基本都是以它為基礎優化產生。
2、復制算法
復制算法的思想就是把內存分為兩塊,每次只在一邊分配內存,當一邊的內存用完了,就把所有還存活的對象復制到另一半去,這時候把原來使用過的這一邊的所有空間一次性清理掉,所以也就不存在內存碎片的問題了。缺點就是會浪費一半的內存空間。基本思路如圖:
其實分代GC算法在新生代區域就用了復制算法,并且也沒有分成1:1,而是8:1,也就是所謂的Eden區和survivor區,新生代中大多數對象都是“朝生夕死”的,所以在minorGC時,只把存活下來的對象全部復制到survivor區。
3、標記整理算法
上面提到的復制算法也有它的弱點,就是當對象存活率很高的時候,就會存在很多的復制操作,從而影響了效率。所以這種算法運用在老年代的話很明顯不合適,于是又有了標記整理算法,這種算法的主要思路就是把活躍對象標記出來,之后再向內存的一側移動,然后直接清理掉邊界以外的內存,具體思路如下:
4、分代收集算法
新生代中的對象每次回收都基本上只有10%左右的對象存活,所以需要復制的對象很少,效率還不錯。實踐中會將新生代分為一塊較大的Eden空間和兩塊較小的Surivor空間,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活著對象一次地復制到另外一塊Survivor空間上,最后清理掉Eden和剛才用過的Survivor空間。HotSpot虛擬機默認的Eden和Survivor的大小比例是8:1:1。也就是每次新生代中可用內存空間為整個新生代容量的90%(80%+10%),只有10%的內存會被"浪費"。
對于一個大型的系統,當創建的對象和方法變量比較多時,堆內存中的對象也會比較多,如果逐一分析對象是否該回收,那么勢必造成效率低下。分代收集算法是基于這樣一個事實:不同的對象的生命周期(存活情況)是不一樣的,故而不同生命周期的對象位于堆中不同的區域,因此對堆內存不同區域采用不同的策略進行回收可以提高JVM的執行效率。當代商用虛擬機使用的都是分代收集算法:新生代對象存活率低,就采用復制算法;老年代存活率高,就采用標記清除算法或者標記整理算法。Java堆內存一般可以分為新生代、老年代和永久帶三個模塊。如下所示:
- 1、新生代(Young Generation)
新生代的目標是盡可能快速收集掉那些生命周期短的對象,一般情況下,所有新生成的對象首先都是放在新生代的。新生代內存按照8:1:1的比例分成一個eden區和兩個Survivor(s0,s1)區,大部分對象在Eden區中生成。在進行垃圾回收時,先將eden區存活對象復制到s0區,然后清空eden區,當這個s0也滿了時,則將eden區和s0區存對象復制到s1區,然后清空eden和s0。此時s0區是空的,然后交換s0區和s1區的角色(即下次垃圾回收時會掃描Eden區和s1區),即保持s0區為空,如此往返。特別地,當s1區也不足以存放eden區和s0區的存活對象時,就將存活對象直接存放到老年代。如果老年代也滿了,就會觸發一次FullGC,也就是新生代、老年代都進行回收。注意,新生代發生的GC也叫MinorGC,MinorGC發生頻率比較高,不一定等到Eden區滿了才觸發。
- 2、老年代(Old Generation)
老年代存放的都是一些生命周期長的對象,就像上面的所敘述的那樣,在新生代中經歷了N次垃圾回收后仍然存活的對象就會被放到老年代中。此外,老年代的內存也比新生代大很多,大概比例是(1:2),當老年代滿時會觸發Major GC/Full GC,老年代對象存活時間比較長,因此Major GC/Full GC發生的頻率比較低。
- 3、永久代(Permanent Generation)
永久代主要用于存放靜態文件,如Java類、方法等。永久代對垃圾回收沒有顯著影響,但是有些應用可能動態生成或者調用一些class,例如使用反射、動態代理、GCLib等bytecode框架時,在這種時候需要設置一個比較大的永久代空間來存放這些運行過程中新增的類。
- 4、小結
由于對象進行了分代處理,因此垃圾回收區域、時間也不一樣。垃圾回收有兩種類型,Minor GC 和Major GC/Full GC。
Minor GC:對新生代進行回收,不會影響到老年代。因為新生代的Java對象大多死亡頻繁,所以 Minor GC 非常頻繁,一般在這里使用速度快、效率高的算法,使垃圾回收能盡快完成。
Major GC/Full GC:對整個堆進行回收,包括新生代和老年代。由于Full GC需要對整個堆進行回收,所以比Minor GC要慢,因此應該盡可能減少Full GC的次數,導致Full GC的原因包括:老年代要被寫滿、永久代被寫滿和System.gc()被顯式調用等。