JVM面試題及答案


1、詳解JVM內存模型 

程序計數器:這里記錄了線程執行的字節碼的行號,在分支、循環、跳轉、異常、線程恢復等都依賴這個計數器。如果線程正在執行的是一個java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果正在執行的是Native方法,這個計數器值為空(Undefined)。此內存區域是唯一一個在java虛擬機規范中沒有規定任何OutOfMemoryError情況的區域。

java虛擬機棧:每個方法執行的時候都會創建一個棧幀(stack frame)用於存放 局部變量表、操作棧、動態鏈接、方法出口。每一個方法從調用直至執行完成的過程,就對應一個棧幀在虛擬機中入棧到出棧的過程。

其中虛擬機棧中的局部變量表部分是人們比較關心的部分。局部變量表存放了編譯期可知的各種基本數據類型和returnAddress類型,需要注意的是其中64位長度的long和double類型的數據會占用2個局部變量空間(Slot),其余的數據類型只占用1個。局部變量表所需的內存空間在編譯期間完成分配,在方法運行期間不會改變局部變量表的大小。

Java虛擬機棧有兩種異常狀況:StackOverflowError(超過棧深度)和OutOfMemoryError(動態擴展內存不足)異常。

本地方法棧:與虛擬機棧很類似,區別是一個shi是執行Java方法,一個是執行本地方法。有的虛擬機會把這2個棧合二為一。本地方法棧和虛擬機棧一樣會出現StackOverflowError和OutOfMemoryError異常。

Java堆:Java堆是Java虛擬機所管理的內存最大的一塊,被所有線程共享的一塊內存區域,在虛擬機啟動的時候就創建了。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內存(“幾乎”是因為隨着JIT編譯器的發展和逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化,所有的對象都分配在堆上也漸漸變得不那么絕對)。

Java堆是垃圾收集器管理的主要區域,有時候也被稱為“GC堆”。因為現在收集器基本都采用分代收集算法,所有Java堆還可以細分為:新生代和老年代,再細致一點有Eden空間、From Survivor空間、To Survivor空間等。堆是可以固定大小也是可以擴展的,如果在堆中沒有內存完成實例分配,並且堆也無法再擴展時,會拋出OutOfMemoryError異常。

方法區:用於存儲已被Java虛擬機加載的類信息、常量、靜態變量、及時編譯器編譯后的代碼等數據。雖然Java虛擬機規范把方法區描述為堆的一個邏輯部分,但是它有一個別名叫Non-Heap(非堆),目的應該是與Java堆區分開來。

需要注意的是很多在HotSpot上開發的人員把方法區稱為“永久代”,但是兩者並不等價(HotSpot設計團隊選擇把GC分代手機擴展至方法區,或者說使用永久代來實現方法區而已)。在JDK1.7的HotSpot中,已經把原本放在永久代的  字符串changliangc常量池移出。方法區無法滿足內存分配需求時會拋出OutOfMemoryError異常。

運行時常量池:屬於方法區的一部分,Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用於存放編譯器生成的各種字面量和符號引用,這部分內容將在類加載后進入方法區的運行時常量池中存放。

相對於Class文件常量池,運行時常量池具備動態性,運行期間也可以將新的常量放入池中,平時利用較多的是String類的intern()方法。

直接內存:Deirect Memory並不是虛擬機運行時數據區的一部分,也不是Java虛擬機規范中定義的內存區域,但是這部分內存也被頻繁的使用,而已也可能導致OutOfMemoryError異常,所以需要注意。服務器管理員在配置虛擬機參數時,會根據實際內存設置-Xmx等參數信息,但經常忽略直接內存,使得各個內存區域總和大於物理內存限制(包括物理的和操作系統級的限制),從而導致動態擴展時出現OutOfMemoryError異常。

2、講講什么情況下會出現內存溢出,內存泄漏

1、內存泄漏memory leak :是指程序在申請內存后,無法釋放已申請的內存空間,一次內存泄漏似乎不會有大的影響,但內存泄漏堆積后的后果就是內存溢出。 
2、內存溢出 out of memory :指程序申請內存時,沒有足夠的內存供申請者使用,或者說,給了你一塊存儲int類型數據的存儲空間,但是你卻存儲long類型的數據,那么結果就是內存不夠用,此時就會報錯OOM,即所謂的內存溢出。

Java內存泄漏的根本原因是什么呢?長生命周期的對象持有短生命周期對象的引用就很可能發生內存泄漏,盡管短生命周期對象已經不再需要,但是因為長生命周期持有它的引用而導致不能被回收,這就是Java中內存泄漏的發生場景。具體主要有如下幾大類:

1、靜態集合類引起內存泄漏:

像HashMap、Vector等的使用最容易出現內存泄露,這些靜態變量的生命周期和應用程序一致,他們所引用的所有的對象Object也不能被釋放,因為他們也將一直被Vector等引用着。

例如

1

2

3

4

5

6

7

Static Vector v = new Vector(10);

for (int i = 1; i<100; i++)

{

Object o = new Object();

v.add(o);

o = null;

}

在這個例子中,循環申請Object 對象,並將所申請的對象放入一個Vector 中,如果僅僅釋放引用本身(o=null),那么Vector 仍然引用該對象,所以這個對象對GC 來說是不可回收的。因此,如果對象加入到Vector 后,還必須從Vector 中刪除,最簡單的方法就是將Vector對象設置為null。

2、當集合里面的對象屬性被修改后,再調用remove()方法時不起作用。

例如:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

public static void main(String[] args)

{

Set<Person> set = new HashSet<Person>();

Person p1 = new Person("唐僧","pwd1",25);

Person p2 = new Person("孫悟空","pwd2",26);

Person p3 = new Person("豬八戒","pwd3",27);

set.add(p1);

set.add(p2);

set.add(p3);

System.out.println("總共有:"+set.size()+" 個元素!"); //結果:總共有:3 個元素!

p3.setAge(2); //修改p3的年齡,此時p3元素對應的hashcode值發生改變

set.remove(p3); //此時remove不掉,造成內存泄漏

set.add(p3); //重新添加,居然添加成功

System.out.println("總共有:"+set.size()+" 個元素!"); //結果:總共有:4 個元素!

for (Person person : set)

{

System.out.println(person);

}

}

3、監聽器

在java 編程中,我們都需要和監聽器打交道,通常一個應用當中會用到很多監聽器,我們會調用一個控件的諸如addXXXListener()等方法來增加監聽器,但往往在釋放對象的時候卻沒有記住去刪除這些監聽器,從而增加了內存泄漏的機會。

4、各種連接

比如數據庫連接(dataSourse.getConnection()),網絡連接(socket)和io連接,除非其顯式的調用了其close()方法將其連接關閉,否則是不會自動被GC 回收的。對於Resultset 和Statement 對象可以不進行顯式回收,但Connection 一定要顯式回收,因為Connection 在任何時候都無法自動回收,而Connection一旦回收,Resultset 和Statement 對象就會立即為NULL。但是如果使用連接池,情況就不一樣了,除了要顯式地關閉連接,還必須顯式地關閉Resultset Statement 對象(關閉其中一個,另外一個也會關閉),否則就會造成大量的Statement 對象無法釋放,從而引起內存泄漏。這種情況下一般都會在try里面去的連接,在finally里面釋放連接。

5、內部類和外部模塊的引用

內部類的引用是比較容易遺忘的一種,而且一旦沒釋放可能導致一系列的后繼類對象沒有釋放。此外程序員還要小心外部模塊不經意的引用,例如程序員A 負責A 模塊,調用了B 模塊的一個方法如:

public void registerMsg(Object b);

這種調用就要非常小心了,傳入了一個對象,很可能模塊B就保持了對該對象的引用,這時候就需要注意模塊B 是否提供相應的操作去除引用。

6、單例模式

不正確使用單例模式是引起內存泄漏的一個常見問題,單例對象在初始化后將在JVM的整個生命周期中存在(以靜態變量的方式),如果單例對象持有外部的引用,那么這個對象將不能被JVM正常回收,導致內存泄漏,考慮下面的例子:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

class A{

public A(){

B.getInstance().setA(this);

}

....

}

//B類采用單例模式

class B{

private A a;

private static B instance=new B();

public B(){}

public static B getInstance(){

return instance;

}

public void setA(A a){

this.a=a;

}

//getter...

}

顯然B采用singleton模式,它持有一個A對象的引用,而這個A類的對象將不能被回收。想象下如果A是個比較復雜的對象或者集合類型會發生什么情況。

 

java內存溢出常見的有:

第一種OutOfMemoryError: PermGen space

發生這種問題的原意是程序中使用了大量的jar或class,使java虛擬機裝載類的空間不夠,與Permanent Generation space有關。解決這類問題有以下兩種辦法:

  1. 增加java虛擬機中的XX:PermSize和XX:MaxPermSize參數的大小,其中XX:PermSize是初始永久保存區域大 小,XX:MaxPermSize是最大永久保存區域大小。如針對tomcat6.0,在catalina.sh 或catalina.bat文件中一系列環境變量名說明結束處(大約在70行左右) 增加一行: JAVA_OPTS=" -XX:PermSize=64M -XX:MaxPermSize=128m" 如果是windows服務器還可以在系統環境變量中設置。感覺用tomcat發布sprint+struts+hibernate架構的程序時很容易發生這種內存溢出錯誤。使用上述方法,我成功解決了部署ssh項目的tomcat服務器經常宕機的問題。
  2. 清理應用程序中web-inf/lib下的jar,如果tomcat部署了多個應用,很多應用都使用了相同的jar,可以將共同的jar移到 tomcat共同的lib下,減少類的重復加載。這種方法是網上部分人推薦的,我沒試過,但感覺減少不了太大的空間,最靠譜的還是第一種方法。

第二種OutOfMemoryError:  Java heap space

發生這種問題的原因是java虛擬機創建的對象太多,在進行垃圾回收之間,虛擬機分配的到堆內存空間已經用滿了,與Heap space有關。解決這類問題有兩種思路:

  1. 檢查程序,看是否有死循環或不必要地重復創建大量對象。找到原因后,修改程序和算法。 我以前寫一個使用K-Means文本聚類算法對幾萬條文本記錄(每條記錄的特征向量大約10來個)進行文本聚類時,由於程序細節上有問題,就導致了 Java heap space的內存溢出問題,后來通過修改程序得到了解決。
  2. 增加Java虛擬機中Xms(初始堆大小)和Xmx(最大堆大小)參數的大小。如:set JAVA_OPTS= -Xms256m -Xmx1024m

第三種OutOfMemoryError:unable to create new native thread

在java應用中,有時候會出現這樣的錯誤:OutOfMemoryError: unable to create new native thread.這種怪事是因為JVM已經被系統分配了大量的內存(比如1.5G),並且它至少要占用可用內存的一半。有人發現,在線程個數很多的情況下, 你分配給JVM的內存越多,那么,上述錯誤發生的可能性就越大。

那么是什么原因造成這種問題呢?

每一個32位的進程最多可以使用2G的可用內存,因為另外2G被操作系統保留。這里假設使用1.5G給JVM,那么還余下500M可用內存。這 500M內存中的一部分必須用於系統dll的加載,那么真正剩下的也許只有400M,現在關鍵的地方出現了:當你使用Java創建一個線程,在JVM的內 存里也會創建一個Thread對象,但是同時也會在操作系統里創建一個真正的物理線程(參考JVM規范),操作系統會在余下的400兆內存里創建這個物理 線程,而不是在JVM的1500M的內存堆里創建。在jdk1.4里頭,默認的棧大小是256KB,但是在jdk1.5里頭,默認的棧大小為1M每線程, 因此,在余下400M的可用內存里邊我們最多也只能創建400個可用線程。

這樣結論就出來了,要想創建更多的線程,你必須減少分配給JVM的最大內存。還有一種做法是讓JVM宿主在你的JNI代碼里邊。

3、說說Java線程棧 

Java線程棧從線程創建時存在,並且是私有的。線程棧用戶存儲棧幀,棧幀用於存儲局部變量、中間運算結果。所以局部是不存在並發的問題,因為每個棧是私有的。虛擬機只會對Java棧進行二種操作:以棧幀為單位的壓棧和出棧。對於執行引擎來說,在活動線程中,只有位於棧頂的棧幀才是有效的,稱為當前棧幀(Current Stack Frame),與這個棧幀相關聯的方法稱為當前方法(Current Method)。執行引擎運行的所有字節碼指令都只針對當前棧幀進行操作。(棧幀中各個部分的作用和數據結構詳見《深入理解虛擬機》第8章)。

4、JVM 年輕代到年老代的晉升過程的判斷條件是什么呢

虛擬機給每個對象定義一個對象年齡計數器。如果對象在Eden出生並經過第一次Minor GC后仍然存活,並且能被Survivor容納的話,將被移動到Survivor空間中,並且對象年齡設為1。對象在Survivor區中每“熬過”一次Minor GC,年齡就增加1歲,當它的年齡增加到一定程度(默認為15歲),就將會被晉升到老年代中。對象晉升老年代的年齡閾值,可以通過參數-XX:MaxTenuringThreshold設置。

還有一種方式是動態對象年齡判定。為了適應不同程序的內存狀況,虛擬機並不是永遠地要求對象的年齡必須達到看MaxTenuringThreshold才能晉升老年代,如果Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。

5、JVM 出現 fullGC 很頻繁,怎么去線上排查問題

https://blog.csdn.net/wilsonpeng3/article/details/70064336  這篇文章說的很好

6、類加載為什么要使用雙親委派模式,有沒有什么場景是打破了這個模式

使用雙親委派模型來組織類加載器之間的關系,有一個顯而易見的好處就是Java類隨着它的加載器一起具備了一種帶有優先級的層次關系。例如類java.lang.Object,它存放在rt.jar之中,無論哪個類加載器要加載這個類,最終都是委派給處於模型最頂端的啟動類加載器進行加載,因此Object類在程序的各種類加載器環境中都是同一個類。相反,如果沒有使用雙親委派模型,有各個類加載器自行去加載的話,如果用戶自己編寫了一個稱為java.lang.Object的類,並放在程序的ClassPath中,那系統中將會出現多個不同的Object類,Java類型體系中最基礎的行為也就無法保證,應用程序也將變得一片混亂。

雙親委派模型的實現很簡單,實現代碼都集中在java.lang.ClassLoader的loadClass()方法之中,邏輯清晰易懂:先檢查是否已經被加載過,若沒有加載則調用父加載器的loadClass()方法,若父加載器為空則默認使用啟動類加載器作為父加載器。如果父類加載失敗,拋出ClassNotFoundException異常后,再調用自己的findClass()方法進行加載。

破壞雙親委派模式的場景:

1、JDK1.2之前還沒有引入雙親委派模式,為了向前兼容,JDK1.2之后的java.lang.ClassLoader添加了一個新的protected方法findClass(),在此之前,用戶去繼承java.lang.ClassLoader的唯一目的就是重寫loadClass()方法,因為虛擬機在進行類加載的時候會調用加載器的私有方法loadClassInternal(),而這個方法的唯一邏輯就是去調用自己的loadClass()。JDK1.2之后已不提倡用戶再去覆蓋loadClass()方法,而應當把自己的類加載邏輯寫到findClass()方法來完成加載,這樣就可以保證新寫出來的類加載器是符合雙親委派規則的。

2、JNDI服務的代碼有啟動類加載器去加載,但JNDI的目的就是對資源進行集中管理和查找,它需要調用有獨立廠商實現並部署在應用程序的ClassPath下的JNDI接口提供者的代碼,單啟動類加載器不可能“認識”這些代碼。為了解決這個問題,Java設計團隊只好引入了一個不太優雅的設計:線程上下文類加載器(Thread Context ClassLoader)。這個類加載器可以通過java.lang.Thread類的setContextClassLoader()方法進行設置,如果創建線程時還未設置,它將會從父線程中繼承一個,如果在應用程序的全局范圍內都沒有設置過的話,那這個類加載器默認就是應用程序類加載器。JNDI服務使用這個線程上下文類加載器去加載所需要的SPI代碼,也就是父類加載器請求子類加載器去完成類加載動作,這個行為實際上就是打通了雙親委派模型的層次結構來逆向使用類加載器,實際上已經違背了雙親委派模型的一般性原則,但這也是無可奈何的事情。Java中所有涉及SPI的加載動作基本上都是采用這種方式,例如:JNDI、JDBC、JCE、JAXB、和JBI等。

3、業界“事實上”Java模塊化標准的OSGi,它實現模塊化熱部署的關鍵就是它自定義的類加載器機制的實現。在OSGi環境下,類加載器不再是雙親委派模型中的樹狀結構,而是進一步發展為更加復雜的網狀結構。具體詳見《深入理解虛擬機》第7章。

 7、類的實例化順序

1.首先是父類的靜態變量和靜態代碼塊(看兩者的書寫順序);

2.第二執行子類的靜態變量和靜態代碼塊(看兩者的書寫順序);

3.第三執行父類的成員變量賦值

4.第四執行父類類的構造代碼塊

5.第五執行父類的構造方法()

6.執行子類的構造代碼塊

7.第七執行子類的構造方法();

總結,也就是說雖然客戶端代碼是new 的構造方法,但是構造方法確實是在整個實例創建中的最后一個調用。切記切記!!!

**先是父類,再是子類; 
先是類靜態變量和靜態代碼塊,再是對象的成員變量和構造代碼塊–》構造方法。**

記住,構造方法最后調用!!!!成員變量優先構造代碼塊優先構造方法!!

8、JVM垃圾回收機制,何時觸發MinorGC等操作

當JVM創建對象遇到內存不足的時候,JVM會自動觸發垃圾回收garbage collecting(簡稱GC)操作,將不再使用但仍存在JVM內存中的對象當做垃圾一樣直接清理掉,釋放被占用的內存空間,供新創建的對象使用。

針對HotSpot VM的的GC其實准確分類只有兩大種:

1)Partial GC:部分回收模式

  • Young GC:只收集young gen的GC。和Minor GC一樣。
  • Old GC:只收集old gen的GC。只有CMS的concurrent - collection是這個模式
  • Mixed GC:收集整個young gen以及部分old gen的GC。只有G1有這個模式

2)Full GC:收集整個堆,包括young gen、old gen,還有永久代perm gen(如果存在的話)等所有部分的模式。同Major GC。

3)觸發時機
HotSpot VM的串行GC的觸發條件是:
young GC:當young gen中的eden區分配滿的時候觸發。

full GC:當准備要觸發一次young GC時,如果發現統計數據說之前young GC的平均晉升大小比目前old gen剩余的空間大,則不會觸發young GC而是轉為觸發full GC;或者,如果有perm gen的話,要在perm gen分配空間但已經沒有足夠空間時,也要觸發一次full GC;或者System.gc()、heap dump帶GC,默認也是觸發full GC。

並發GC的觸發條件就不太一樣。以CMS GC為例,它主要是定時去檢查old gen的使用量,當使用量超過了觸發比例就會啟動一次CMS GC,對old gen做並發收集。

年輕代GC過程

當需要在堆中創建一個新的對象,而年輕代內存不足時觸發一次GC,在年輕代觸發的GC稱為普通GC,Minor GC。注意到年輕代中的對象都是存活時間較短的對象,所以適合使用復制算法。這里肯定不會使用兩倍的內存來實現復制算法了,牛人們是這樣解決的,把年輕代內存組成是80%的Eden、10%的From Space和10%的To Space,然后在這些內存區域直接進行復制。

剛開始創建的對象是在Eden中,此時Eden中有對象,而兩個survivor區沒有對象,都是空閑區間。第一次Minor GC后,存活的對象被放到其中一個survivor,Eden中的內存空間直接被回收。在下一次GC到來時,Eden和一個survivor中又創建滿了對象,這個時候GC清除的就是Eden和這個放滿對象的survivor組成的大區域(占90%),Minor GC使用復制算法把活的對象復制到另一個空閑的survivor區間,然后直接回收之前90%的內存。周而復始。始終會有一個10%空閑的survivor區間,作為下一次Minor GC存放對象的准備空間。

要完成上面的算法,每次Minor GC過程都要滿足:
存活的對象大小都不能超過survivor那10%的內存空間,不然就沒有空間復制剩下的對象了。但是,萬一超過了呢?前面我們提到過年老代,對,就是把這些大對象放到年老代。

年老代GC

什么樣的對象可以進入年老代呢?如下:

  • 在年輕代中,如果一個對象的年齡(GC一次后還存活的對象年歲加1)達到一個閾值(可以配置),就會被移動到年老代。
  • Survivor中相同年齡的對象大小總和超過survivor空間的一半,則不小於這個年齡的對象都會直接進入年老代。
  • 創建的對象的大小超過設定閾值,這個對象會被直接存進年老代。
  • 年輕代中大於survivor空間的對象,Minor GC時會被移進年老代。

年老代中的對象特點就是存活時間較長,而且沒有備用的空閑空間,所以顯然不適合使用復制算法了,這個時候使用標記-清除算法或者標記-整理算法來實現GC。負責年老代中GC操作的是全局GC,Major GC,Full GC。

什么時候觸發Major GC呢?
在Minor GC時,先檢測JVM的統計數據,查看歷史上進入老年代的對象平均大小是否大於目前年老代中的剩余空間,如果大於則觸發Full GC。

9、JVM 中一次完整的 GC 流程(從 ygc 到 fgc)是怎樣的

10、各種回收器,各自優缺點,重點CMS、G1

上面有7中收集器,分為兩塊,上面為新生代收集器,下面是老年代收集器。如果兩個收集器之間存在連線,就說明它們可以搭配使用。

下面2個名詞都是並發編程中的概念,在談論垃圾收集器的上下文語境中,它們可以解釋如下:

並行:指多條垃圾收集線程並行工作,但此時用戶線程仍然處於等待狀態。

並發:指用戶線程與垃圾收集線程同時執行(但不一定是並行的,可能會交替執行),用戶程序在繼續運行,而垃圾收集程序運行於另一個CPU上。

Serial(串行GC)收集器

Serial收集器是一個新生代收集器,單線程執行,使用復制算法。它在進行垃圾收集時,必須暫停其他所有的工作線程(用戶線程)。是Jvm client模式下默認的新生代收集器。對於限定單個CPU的環境來說,Serial收集器由於沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。在用戶的桌面應用場景中,即Client模式下的虛擬機來說是一個很好的選擇。

ParNew(並行GC)收集器

ParNew收集器其實就是serial收集器的多線程版本,除了使用多條線程進行垃圾收集之外,其余行為與Serial收集器一樣。它是許多運行在Server模式下的虛擬機中首選的新生代收集器,其中有一個與性能無關但很重要的原因是,除了Serial收集器外,目前只有它能與CMS收集器配合工作。ParNew在單CPU環境下絕對不會有比Serial收集器更好的效果,甚至由於存在線程交互的開銷,該收集器在通過超線程技術實現的兩個CPU的環境中都不能百分百保證可以超越Serial收集器。當然,隨着可以使用的CPU的數量的增加,它對GC時系統資源的有效利用還是很有好處的。

Parallel Scavenge(並行回收GC)收集器

Parallel Scavenge收集器也是一個新生代收集器,它也是使用復制算法的收集器,又是並行多線程收集器。parallel Scavenge收集器的特點是它的關注點與其他收集器不同,CMS等收集器的關注點是盡可能地縮短垃圾收集時用戶線程的停頓時間,而parallel Scavenge收集器的目標則是達到一個可控制的吞吐量。吞吐量= 程序運行時間/(程序運行時間 + 垃圾收集時間),虛擬機總共運行了100分鍾。其中垃圾收集花掉1分鍾,那吞吐量就是99%。由於於吞吐量關系密切,Parallel Scavenge收集器也經常被稱為“吞吐量優先”收集器。Parallel Scavenge收集器有一個參數-XX:UseAdaptiveSizePolicy,當這個參數打開,虛擬機會根據當前系統的運行狀況收集性能監控信息,動態調整一些如新生代大小、Eden與Survivor區的比例等等細節參數。這種自適應調節策略也是Parallel Scavenge收集器與ParNew收集器的一個重要區別。

Serial Old(串行GC)收集器

Serial Old是Serial收集器的老年代版本,它同樣使用一個單線程執行收集,使用“標記-整理”算法。主要使用在Client模式下的虛擬機。如果在Server模式下,那么它還有兩大用途:一種用途是在JDK1.5以及之前的版本中與Parallel Scavenge收集器搭配使用,另一種用途是作為CMS收集器的后備預案,在並並發手機發生Concurrent Mode Failure時使用。

Parallel Old(並行GC)收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法。在注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge收集器加Parallel Old收集器。

CMS(並發GC)收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器,適用於集中在互聯網站或者B/S系統的服務端的Java應用。CMS收集器是基於“標記-清除”算法實現的,整個收集過程大致分為4個步驟:
①.初始標記(CMS initial mark)
②.並發標記(CMS concurrenr mark)
③.重新標記(CMS remark)
④.並發清除(CMS concurrent sweep)

     其中初始標記、重新標記這兩個步驟任然需要停頓其他用戶線程。初始標記僅僅只是標記出GC ROOTS能直接關聯到的對象,速度很快,並發標記階段是進行GC ROOTS 根搜索算法階段,會判定對象是否存活。而重新標記階段則是為了修正並發標記期間,因用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間會被初始標記階段稍長,但比並發標記階段要短。

     由於整個過程中耗時最長的並發標記和並發清除過程中,收集器線程都可以與用戶線程一起工作,所以整體來說,CMS收集器的內存回收過程是與用戶線程一起並發執行的。

CMS收集器的優點:並發收集、低停頓,但是CMS還遠遠達不到完美,主要有三個顯著缺點:

  CMS收集器對CPU資源非常敏感。在並發階段,雖然不會導致用戶線程停頓,但是會占用CPU資源而導致引用程序變慢,總吞吐量下降。CMS默認啟動的回收線程數是:(CPU數量+3) / 4。

  CMS收集器無法處理浮動垃圾,可能出現“Concurrent Mode Failure“,失敗后而導致另一次Full  GC的產生。由於CMS並發清理階段用戶線程還在運行,伴隨程序的運行自熱會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之后,CMS無法在本次收集中處理它們,只好留待下一次GC時將其清理掉。這一部分垃圾稱為“浮動垃圾”。也是由於在垃圾收集階段用戶線程還需要運行,
即需要預留足夠的內存空間給用戶線程使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分內存空間提供並發收集時的程序運作使用。在默認設置下,CMS收集器在老年代使用了68%的空間時就會被激活,也可以通過參數-XX:CMSInitiatingOccupancyFraction的值來提供觸發百分比,以降低內存回收次數提高性能。要是CMS運行期間預留的內存無法滿足程序其他線程需要,就會出現“Concurrent Mode Failure”失敗,這時候虛擬機將啟動后備預案:臨時啟用Serial Old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。所以說參數-XX:CMSInitiatingOccupancyFraction設置的過高將會很容易導致“Concurrent Mode Failure”失敗,性能反而降低。

  最后一個缺點,CMS是基於“標記-清除”算法實現的收集器,使用“標記-清除”算法收集后,會產生大量碎片。空間碎片太多時,將會給對象分配帶來很多麻煩,比如說大對象,內存空間找不到連續的空間來分配不得不提前觸發一次Full  GC。為了解決這個問題,CMS收集器提供了一個-XX:UseCMSCompactAtFullCollection開關參數,用於在Full  GC之后增加一個碎片整理過程,還可通過-XX:CMSFullGCBeforeCompaction參數設置執行多少次不壓縮的Full  GC之后,跟着來一次碎片整理過程。

G1收集器

G1(Garbage First)收集器是JDK1.7提供的一個新收集器,是當今收集器技術發展的最前沿成果之一。G1是一款面向服務端應用的垃圾收集器。HotSpot開發團隊賦予它的使命是(在比較長期的)未來可以替換掉JDK1.5中發布的CMS收集器。

與其他GC收集器相比,G1具備如下特點:

1、並行與並發:G1能充分利用多CPU、多核環境下的硬件優勢,使用多個CPU(CPU或CPU核心)來縮短Stop-The-World停頓的時間,部分其他收集器原本需要停頓Java線程執行的GC動作,G1收集器仍然可以通過並發的方式讓Java程序繼續執行。

2、分代手機:與其他收集器一樣,分代概念在G1中依然得以保留。雖然G1可以不需要其他收集器配合就能單獨管理整個GC堆,但它能夠采用不同的方式去處理新創建的對象和已經存活了一段時間、熬過多次GC的舊對象已獲得更好的手機效果。

3、空間整合:與CMS的“標記-清理”算法不同,G1收集器從整體上看是基於“標記-整理”算法實現的,從局部(兩個Region之間)上看是基於“復制”算法實現的,但無論如何,這兩種算法都意味着G1運行期間不會產生內存空間碎片,收集后能提供規整的可用內存。這種特性有利於程序的長時間運行,分配大對象時不會因為無法找到連續內存空間而提前觸發下一次GC。

4、可預測的停頓:這是G1相對於CMS的另一大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾手機上的時間不得超過N毫秒,這幾乎已經是實時Java(RTSJ)的垃圾收集器的特征了。

11、各種回收算法

1.對象是否“已死”算法——引用計數器算法

  對象中添加一個引用計數器,如果引用計數器為0則表示沒有其它地方在引用它。如果有一個地方引用就+1,引用失效時就-1。看似搞笑且簡單的一個算法,實際上在大部分Java虛擬機中並沒有采用這種算法,因為它會帶來一個致命的問題——對象循環引用。對象A指向B,對象B反過來指向A,此時它們的引用計數器都不為0,但它們倆實際上已經沒有意義因為沒有任何地方指向它們。所以又引出了下面的算法。

2.對象是否“已死”算法——可達性分析算法

  這種算法可以有效地避免對象循環引用的情況,整個對象實例以一個樹呈現,根節點是一個稱為“GC Roots”的對象,從這個對象開始向下搜索並作標記,遍歷完這棵樹過后,未被標記的對象就會判斷“已死”,即為可被回收的對象。

3、標記-清除算法

  這個方法是將垃圾回收分成了兩個階段:標記階段和清除階段。

            在標記階段,通過跟對象,標記所有從跟節點開始的可達的對象,那么未標記的對象就是未被引用的垃圾對象。

            在清除階段,清除掉所以的未被標記的對象。

            這個方法的缺點是,垃圾回收后可能存在大量的磁盤碎片,准確的說是內存碎片。因為對象所占用的地址空間是固定的。對於這個算法還有改進的算法,就是我后面要說的算法四。

4、標記-整理算法

  在算法三的基礎上做了一個改進,可以說這個算法分為三個階段:標記階段,壓縮階段,清除階段。標記階段和清除階段不變,只不過增加了一個壓縮階段,就是在做完標記階段后,將這些標記過的對象集中放到一起,確定開始和結束地址,比如全部放到開始處,這樣再去清除,將不會產生磁盤碎片。但是我們也要注意到幾個問題,壓縮階段占用了系統的消耗,並且如果標記對象過多的話,損耗可能會很大,在標記對象相對較少的時候,效率較高。

  對於新生代,大部分對象都不會存活,所以在新生代中使用復制算法較為高效,而對於老年代來講,大部分對象可能會繼續存活下去,如果此時還是利用復制算法,效率則會降低。標記-壓縮算法首先還是“標記”,標記過后,將不用回收的內存對象壓縮到內存一端,此時即可直接清除邊界處的內存,這樣就能避免復制算法帶來的效率問題,同時也能避免內存碎片化的問題。老年代的垃圾回收稱為“Major GC”。

5、復制算法(Java中新生代采用)

  核心思想是將內存空間分成兩塊,同一時刻只使用其中的一塊,在垃圾回收時將正在使用的內存中的存活的對象復制到未使用的內存中,然后清除正在使用的內存塊中所有的對象,然后把未使用的內存塊變成正在使用的內存塊,把原來使用的內存塊變成未使用的內存塊。很明顯如果存活對象較多的話,算法效率會比較差,並且這樣會使內存的空間折半,但是這種方法也不會產生內存碎片。

  此GC算法實際上解決了標記-清除算法帶來的“內存碎片化”問題。首先還是先標記處待回收內存和不用回收的內存,下一步將不用回收的內存復制到新的內存區域,這樣舊的內存區域就可以全部回收,而新的內存區域則是連續的。它的缺點就是會損失掉部分系統內存,因為你總要騰出一部分內存用於復制。

6、分代法(Java堆采用)

主要思想是根據對象的生命周期長短特點將其進行分塊,根據每塊內存區間的特點,使用不同的回收算法,從而提高垃圾回收的效率。

Java 中的堆是 JVM 所管理的最大的一塊內存空間,主要用於存放各種類的實例對象。在 Java 中,堆被划分成兩個不同的區域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分為三個區域:Eden、From Survivor、To Survivor。這樣划分的目的是為了使 JVM 能夠更好的管理堆內存中的對象,包括內存的分配以及回收。

JVM 每次只會使用 Eden 和其中的一塊 Survivor 區域來為對象服務,所以無論什么時候,總是有一塊 Survivor 區域是空閑着的。因此,新生代實際可用的內存空間為 9/10 ( 即90% )的新生代空間。新生代垃圾回收采用復制算法,清理的頻率比較高。如果新生代在若干次清理(可以進行設置)中依然存活,則移入老年代,有的內存占用比較大的直接進入老年代。老年代使用標記清理算法,清理的頻率比較低。

7、分區算法

這種方法將整個空間划分成連續的不同的小區間,每個區間都獨立使用,獨立回收,好處是可以控制一次回收多少個小區間。

12、OOM錯誤,stackoverflow錯誤,permgen space錯誤

1, OutOfMemoryError異常

除了程序計數器外,虛擬機內存的其他幾個運行時區域都有發生OutOfMemoryError(OOM)異常的可能,

Java Heap 溢出

一般的異常信息:java.lang.OutOfMemoryError:Java heap spacess

java堆用於存儲對象實例,我們只要不斷的創建對象,並且保證GC Roots到對象之間有可達路徑來避免垃圾回收機制清除這些對象,就會在對象數量達到最大堆容量限制后產生內存溢出異常。

出現這種異常,一般手段是先通過內存映像分析工具(如Eclipse Memory Analyzer)對dump出來的堆轉存快照進行分析,重點是確認內存中的對象是否是必要的,先分清是因為內存泄漏(Memory Leak)還是內存溢出(Memory Overflow)。

如果是內存泄漏,可進一步通過工具查看泄漏對象到GC Roots的引用鏈。於是就能找到泄漏對象時通過怎樣的路徑與GC Roots相關聯並導致垃圾收集器無法自動回收。

如果不存在泄漏,那就應該檢查虛擬機的參數(-Xmx與-Xms)的設置是否適當。

2, 虛擬機棧和本地方法棧溢出

如果線程請求的棧深度大於虛擬機所允許的最大深度,將拋出StackOverflowError異常。

如果虛擬機在擴展棧時無法申請到足夠的內存空間,則拋出OutOfMemoryError異常

這里需要注意當棧的大小越大可分配的線程數就越少。

3, 運行時常量池溢出

異常信息:java.lang.OutOfMemoryError:PermGen space

如果要向運行時常量池中添加內容,最簡單的做法就是使用String.intern()這個Native方法。該方法的作用是:如果池中已經包含一個等於此String的字符串,則返回代表池中這個字符串的String對象;否則,將此String對象包含的字符串添加到常量池中,並且返回此String對象的引用。由於常量池分配在方法區內,我們可以通過-XX:PermSize和-XX:MaxPermSize限制方法區的大小,從而間接限制其中常量池的容量。

4, 方法區溢出

方法區用於存放Class的相關信息,如類名、訪問修飾符、常量池、字段描述、方法描述等。

異常信息:java.lang.OutOfMemoryError:PermGen space

方法區溢出也是一種常見的內存溢出異常,一個類如果要被垃圾收集器回收,判定條件是很苛刻的。在經常動態生成大量Class的應用中,要特別注意這點。

5、本機直接內存溢出

直接內存並不是虛擬機運行時數據區的一部分,也不是java虛擬機規范中定義的內存區域,是jvm外部的內存區域,這部分區域也可能導致OutOfMemoryError異常。

由DirectMemory導致的內存溢出,一個明顯的特征是在Heap Dump文件中不會看見明顯的異常,如果發現OOM之后Dump文件很小,而程序中又直接或間接使用了NIO,那就可以考慮檢查一下是不是這方面的原因。


 轉載至鏈接:https://my.oschina.net/demons99/blog/1936827。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM