Java記憶體回收機制
1.java的記憶體
java的記憶體結構分為
- 堆 (是gc的主要區域) 線程共享,主要是用于配置設定執行個體對象和數組
- 棧 線程私有,它的生命周期和線程相同,又分成 虛拟機棧和本地方法棧,隻有它會報 StackOverFlowError,棧深度超标
- 方法區 線程共享 用于儲存被虛拟機加載的類的資訊,靜态變量 常量和編譯後的.class位元組碼
-
程式計數器 線程私有,線程之間不互相影響,獨立存取;
以上部分,線程私有是不會發生gc.并且他們是随線程生随線程滅,即程式計數器 本地方法棧和虛拟機棧
2.GC回收機制--判斷是否可以gc
-
-
引用計數算法
原理:通過一個計數器對對象進行計數,對象被引用時+1,引用失效時-1;當計數為0時則說明可以被回收;
缺點:很難解決對象的互相循環引用問題
-
可達性分析算法
Java虛拟機所采用的算法;
原理:通過一些列稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鍊,當一個對象到GC Roots沒有任何引用鍊相連時,則證明此對象是不可用的。
那麼哪些對象可以被稱為gc roots呢----虛拟機棧(棧中的本地變量清單)/方法區靜态屬性/方法區常量引用/本地方法棧中JNI 所引用的的對象都是可以作為 gc roots的
-
深入了解分代回收算法 Survivor(幸存者) Eden (谷歌翻譯為伊甸園)
- 複制算法中記憶體劃分其實并不是按照1:1來劃分老年代和新生代,,而是按照8:1:1分一個大的Eden區和兩個小的survivor的空間
- 為什麼需要2個Survivor區 新生代一般經曆15次Gc就可以移到老年代.當第一次gc時,我們可以把Eden的存活對象放入Survivor A空間,第二次Gc時,Survivor A也要使用複制算法,存活對象放到Survivor B上,第三次gc時,又将Survivor B對象複制到Survivor A上如此循環往複;
- 為什麼Eden這麼大,因為新生代中存活的對象,需要轉移的Survivor 的對象不多,算是緩解了複制算法的缺點;
4.GC回收機制--gc的執行機制
-
-
Scavenge GC
當新對象生成并且在Eden申請空間失敗時就會觸發Scavenge GC;Eden區的gc會比較頻繁
-
Full GC
是對整個堆進行清理,要比Scavenge GC要慢,什麼情況要進行Full GC呢,如下四種:
持久代被寫滿
System.gc調用
老年代被寫滿
上一次GC之後Heap的各域配置設定政策動态變化
-
Java虛拟機記憶體原型
寄存器:我們在程式無法控制
棧:存放基本類型的資料和對象的引用,但對象本身不存放在棧中,而是堆中
存取速度比堆塊,僅次于寄存器,棧資料可以共享,棧的資料大小與生存期必須是确定的,缺乏靈活性。
堆:存放new産生的資料
可以動态配置設定記憶體大小,生存期也不必事先告訴編譯器,因為它在運作時動态配置設定記憶體,Java的垃圾收集器會自動收走這些不再使用的資料,但缺點是,由于在運作時配置設定記憶體,存取速度較慢
靜态域:存放在對象中用static定義的靜态成員
常量池:存放常量(利用final關鍵字修飾的)
非RAM存儲:硬碟等永久存儲空間
Java引用的種類
>對象在記憶體中狀态
對于JVM的垃圾回收機制來說,如果一個對象,沒有一個引用指向它,那麼它就被認為是一個垃圾。那該對象就會回收。可以把JVM記憶體中對象引用了解成一種有向圖,把引用變量、對象都當成有向圖的頂點,将引用關系當成圖的有向邊(注意:有向邊總是從引用變量指向被引用的Java對象)
1、可達狀态
當一個對象被建立後,有一個或一個以上的引用變量引用它,則這個對象在程式中處于可達狀态,程式可以通過引用變量來調用該對象的方法和屬性。
2、可恢複狀态
如果程式中某個對象不再有任何引用變量引用它,它就進入了可恢複狀态,在這個狀态下,系統的垃圾回收機制準備回收該對象所占用的記憶體空間,在回收該對象之前,系統會調用所有對象的finalize方法進行資源的清理,如果系統在調用finalize方法重新讓一個引用變量引用該對象,則這個對象會再次變為激活狀态,否則該 對象狀進入不可達狀态。
3、不可達狀态
當對象與所有引用變量的關聯都被切繼,且系統已經調用所有對象的finalize方法依然沒有該對象變成可達狀态,那這個對象将永久性地失去引用,最後變成不可達狀态,隻有當一個對象處于不可達狀态時統才會真正回收該對象所占有的資源。
*對象的狀态轉換圖如下:
>強引用
強引用是Java程式設計中使用廣泛的引用類型,被強引用所引用的Java對象絕不會被垃圾回收,當記憶體空間不足,Java虛拟機甯願抛出OutOfMemoryError錯誤,使程式異常終止,也不會靠随意回收具有強引用的對象來解決記憶體不足的問題,是以強引用是造成Java記憶體洩漏的主要原因之一
*代碼示例
class Person { String name; int age; public Person(String name,int age) { this.name=name; this.age=age; } @Override public String toString() { return "Person [name=" + name + ", age=" + age + "]"; } } public class ReferenceTest { public static void main(String[] args) { //建立一個長度為10000的強引用數組,來儲存10000個Person對象 Person[] person=new Person[10000]; //依次初始化 for(int i=0;i<person.length;i++) { person[i]=new Person("名字"+i,(i+1)*2%100); } System.out.println(person[1]); System.out.println(person[3]); //通知系統進行垃圾回收 System.gc(); System.runFinalization(); System.out.println(person[1]); System.out.println(person[3]); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
我們來把修改java虛拟機記憶體,把堆記憶體減少到2m
(操作:程式右鍵選屬性->run/debug settings->選中應用程式->編輯->Arguments->VM arguments輸入框輸入 -Xmx2m -Xms2m )
再運作(程式因為記憶體不足而中止)>軟引用
軟引用通過SoftReference類來實作,當系統記憶體空間足夠,軟引用的對象不會被系統回收,程式也可以使用該對象,當系統記憶體不足時,系統将會回收// 建立一個長度為10000的弱引用數組,來儲存10000個Person對象 SoftReference<Person>[] person = new SoftReference[10000]; // 依次初始化 for (int i = 0; i < person.length; i++) { person[i] = new SoftReference<Person>(new Person("名字" + i, (i + 1) * 2 % 100)); }
>弱引用
弱引用與軟引用有點相似,差別是弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描它所管轄的記憶體區域的過程中,一旦發現了隻具有弱引用的對象,不管目前記憶體空間足夠與否,都會回收它的記憶體。不過,由于垃圾回收器是一個優先級很低的線程,是以不一定會很快發現那些隻具有弱引用的對象。
弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被垃圾回收,Java虛拟機就會把這個弱引用加入到與之關聯的引用隊列中。
//建立一個字元串對象 String str=new String("瘋狂Java講義"); //建立一個弱引用,讓該弱引用引用到“瘋狂Java講義”字元串對象 WeakReference<String> wr=new WeakReference<String>(str); //切斷str引用變量和“瘋狂Java講義”字元串對象之間的引用關系 str=null; //取出弱引用所引用的對象 System.out.println(wr.get()); //強制垃圾回收 System.gc(); System.runFinalization(); //再次取出弱引用所引用的對象 System.out.println(wr.get());
>虛引用
虛引用通過PhantomReference類實作,類似沒有引用,主要作用是跟蹤對象被垃圾回收的狀态,程式可以通過檢查與虛引用關聯的引用隊列中是否已經包含指定的虛引用,進而了解虛引用所引用的對象是否即将被回收,虛引用不能單獨使用,必須要和引用隊列(ReferenceQueue)聯合使用//建立一個字元串對象 String str=new String("這是虛引用"); //建立一個引用隊列 ReferenceQueue<String> rq=new ReferenceQueue<String>(); //建立一個虛引用,讓該虛引用引用到“這是虛引用”字元串對象 PhantomReference<String> pr=new PhantomReference<String>(str,rq); //切斷str引用與"這是虛引用"字元串之間的引用 str=null; //取出虛引用所引用的對象 System.out.println(pr.get()); //強制垃圾回收 System.gc(); System.runFinalization(); //取出引用隊列中最先進入隊列中引用與pr進行比較 System.out.println(rq.poll()==pr);
Java的記憶體洩漏
無用對象(不再使用的對象)持續占有記憶體或無用對象的記憶體得不到及時釋放,進而造成的記憶體空間的浪費,就是記憶體洩漏
1、靜态變量引起記憶體洩露:
根據分代回收機制(後面有講),JVM會将程式中obj引用變量存在Permanent代裡,這導緻Object對象一直有效,進而使obj引用的Object得不到回收
例:
class Person { static Object obj=new Object(); }
2、當集合裡面的對象屬性被修改後,再調用remove()方法時不起作用。
例:
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正常回收,導緻記憶體洩露,考慮下面的例子:
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... }
垃圾回收機制
垃圾回收的基本算法
标記壓縮法
先從根節點開始對所有可達對象做一次标記,但之後,它并不簡單地清除未标記的對象,而是将所有的存活對象壓縮到記憶體的一端之後,清理邊界外所有的空間。這種方法既避免了碎片的産生,又不需要兩塊相同的記憶體空間,是以,其成本效益比較高。标記回收法
從“GC Roots”(GC Roots指垃圾收集器的對象,GC會收集那些不是GC roots且沒有被GC roots引用的對象)集合開始,将記憶體整個周遊一次,保留所有被GC Roots直接或者間接引用到的對象,而剩下的對象都當作垃圾對待并回收,這個算法需要中斷程序内其他元件的執行并且可能産生碎片化複制回收法
将記憶體分為大小相等的兩部分(假設A、B兩部分),每次呢隻使用其中的一部分(這裡我們假設為A區),等這部分用完了,這時候就将這裡面還能活下來的對象複制到另一部分記憶體(這裡設為B區)中,然後把A區中的剩下部分全部清理掉。這樣記憶體碎片的問題就解決了分代回收法
根據對象的生命周期将記憶體劃分,然後進行分區管理,在Java虛拟機分代垃圾回收機制中,應用程式可用的堆空間可以分為年輕代與老年代,年輕代有被分為Eden區,From區與To區 分代回收法更詳細連結javascript:void(0)>堆記憶體的分代回收
>與垃圾回收的附加選項
下面兩個選項用于設定java虛拟機記憶體大小
-Xms :設定java虛拟機堆記憶體的最大容量如java -Xmx256m XxxClass
-Xms :設定java虛拟機堆記憶體的初始容量,如java -Xms128m XxxClass
下面選項都是關于java垃圾回收的附加選項
-xx:MinHeapFreeRatio =40 :設定java堆記憶體最小的空閑百分比,預設為40,如java -xx:MinHeapFreeRadio = 40 XxxClass
-xx:MaxHeapFreeRatio=70 :設定Java堆記憶體最大的空閑百分比,預設為70,如java -XX:MaxHeapFreeRatio =70 XxxClass
-xx:NewRatio=2 ;設定Yonng/Old記憶體的比例,如java -XX:NewRatio=1 XxxClass
-xx:NewSize=size:設定Yonng代記憶體的預設容量,如java -XX:Newsize=64m XxxClass
-xx:SurvivorRatio = 8;設定Yonng代中eden/survivor的比例,如java -xx:MaxNewSize=128m XxxClass
注意 當設定Young代的記憶體超過了-Xmx設定的大小時,Young設定的記憶體大小将不會起作用,JVM會自動将Young代記憶體設定為與-Xmx設定的大小相等。
-XX:PermSIze=size;設定Permnanent代記憶體的預設容量,如java –XX:PermSize=128m XxxClass
-XX:MaxPermSize=64m;設定Permanent代記憶體的最大容量,如java -XX:MaxPermSize=128m XxxClass
>常見垃圾回收器
-
串行回收器(Serial Garbage Collector)
Serial Garbage Collector通過暫停所有應用的線程來工作。它是為單線程工作環境而設計的。它中使用一個線程來進行垃圾回收。這種暫停應用線程來進行垃圾回收的方式可能不太适應伺服器環境。它最适合簡單的指令行程式。
通過 -XX:+UseSerialGC 參數來選用Serial Garbage Collector。
- Parallel Garbage Collector
Parallel Garbage Collector也被稱為吞吐量收集器(throughput collector)。它是Java虛拟機的預設垃圾收集器。與Serial Garbage Collector不同,Parallel Garbage Collector使用多個線程進行垃圾回收。與Serial Garbage Collector相似的地方時,它也是暫停所有的應用線程來進行垃圾回收。
3. CMS Garbage Collector
Concurrent Mark Sweep (CMS) Garbage Collector使用多個線程來掃描堆記憶體來标記需要回收的執行個體,然後再清除被标記的執行個體。CMS Garbage Collector隻有在如下兩種情景才會暫停所有的應用線程:
當标記永久代記憶體空間中的對象時; 當進行垃圾回收時,堆記憶體同步發生了一些變化。
相比Parallel Garbage Collector,CMS Garbage Collector使用更多的CPU資源來確定應用有一個更好的吞吐量。如果配置設定更多的CPU資源可以獲得更好的性能,那麼CMS Garbage Collector是一個更好的選擇,相比Parallel Garbage Collector。
通過 XX:+USeParNewGC 參數來選用CMS Garbage Collector。
記憶體管理的小技巧
>盡量使用直接量
當需要使用字元串,還有Byte,Short、integer、Long、Float、Double、Boolean、Character包裝類的執行個體時,不應該采用new的方式來建立對象,而應該使用直接量來建立它們
應該是
而不是String str="hello";
後者除了在建立一個緩存在字元串緩沖池的“hello”字元串,str所引用的String對象底層還包含一個存放了h、e、l、l、o的char[ ]數組String str=new String("hello");
>使用StringBuilder和StringBuffer進行字元串連接配接
String代表字元序列不可變的字元串,StringBuilderheStringBuffer都代表字元序列可變的字元串
建議
StringBuilder st = new StringBuilder(); c = st.append(a).append(b);
String c = a+b;
因為這樣會在運作的時候生成大量臨時字元串,這些字元串會儲存在記憶體中進而導緻程式性能下降
如果使用少量的字元串操作,使用 (+運算符)連接配接字元串;
如果頻繁的對大量字元串進行操作,則使用
1:全局變量或者需要多線程支援則使用StringBuffer;
2:局部變量或者單線程不涉及線程安全則使有StringBuilder
>盡早釋放無用對象的引用
>盡量少用靜态變量
>避免在經常調用的方法、循環中建立Java對象
>緩存經常使用的對象
>盡量不要使用finalize方法
>考慮使用SoftReference
參考:《瘋狂java 突破程式員基本功的16課》
-
-
-
-
-
class Person { String name; int age; public Person(String name,int age) { this.name=name; this.age=age; } @Override public String toString() { return "Person [name=" + name + ", age=" + age + "]"; } } public class ReferenceTest { public static void main(String[] args) { //建立一個長度為10000的強引用數組,來儲存10000個Person對象 Person[] person=new Person[10000]; //依次初始化 for(int i=0;i<person.length;i++) { person[i]=new Person("名字"+i,(i+1)*2%100); } System.out.println(person[1]); System.out.println(person[3]); //通知系統進行垃圾回收 System.gc(); System.runFinalization(); System.out.println(person[1]); System.out.println(person[3]); } }
// 建立一個長度為10000的弱引用數組,來儲存10000個Person對象 SoftReference<Person>[] person = new SoftReference[10000]; // 依次初始化 for (int i = 0; i < person.length; i++) { person[i] = new SoftReference<Person>(new Person("名字" + i, (i + 1) * 2 % 100)); }
//建立一個字元串對象 String str=new String("瘋狂Java講義"); //建立一個弱引用,讓該弱引用引用到“瘋狂Java講義”字元串對象 WeakReference<String> wr=new WeakReference<String>(str); //切斷str引用變量和“瘋狂Java講義”字元串對象之間的引用關系 str=null; //取出弱引用所引用的對象 System.out.println(wr.get()); //強制垃圾回收 System.gc(); System.runFinalization(); //再次取出弱引用所引用的對象 System.out.println(wr.get());
//建立一個字元串對象 String str=new String("這是虛引用"); //建立一個引用隊列 ReferenceQueue<String> rq=new ReferenceQueue<String>(); //建立一個虛引用,讓該虛引用引用到“這是虛引用”字元串對象 PhantomReference<String> pr=new PhantomReference<String>(str,rq); //切斷str引用與"這是虛引用"字元串之間的引用 str=null; //取出虛引用所引用的對象 System.out.println(pr.get()); //強制垃圾回收 System.gc(); System.runFinalization(); //取出引用隊列中最先進入隊列中引用與pr進行比較 System.out.println(rq.poll()==pr);
class Person { static Object obj=new Object(); }
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); } }
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... }
當标記永久代記憶體空間中的對象時; 當進行垃圾回收時,堆記憶體同步發生了一些變化。
String str="hello";
String str=new String("hello");
StringBuilder st = new StringBuilder(); c = st.append(a).append(b);
String c = a+b;
-