【朝花夕拾】Android性能篇之(三)Java內存回收

前言html

       轉載請聲明,轉自【https://www.cnblogs.com/andy-songwei/p/songzheweiwang.html】,謝謝!java

        在上一篇日誌(【朝花夕拾】Android性能篇之(二)Java內存分配)中有講到,JVM內存由程序計數器、虛擬機棧、本地方法棧、GC堆,方法區五個部分組成。其中GC堆是一塊多線程的共享區域,它存在的做用就是存放對象實例。本節中所要講述的各類場景,就發生在這塊區域,垃圾回收也主要發生在GC堆內存中。本章內容爲高質量面試中幾乎是必問的知識點,尤爲是其中GC Root、分代算法、引用類型等方面的知識點,能夠很好地體現程序員的內功。本文主要是在相關文章的基礎上進行蒐集和整理而成,也包含了本身的一些理解和總結,其中涉及到的代碼,貼出運行結果的,都是本身親自運行過的。另外,本文內容爲筆者一字一句敲上去的,若是能對讀者有幫助並被轉載了,請註明一下,若是對本文中內容有異意的,也請不吝賜教。程序員

       本章主要內容以下:面試

                   

 

1、什麼是垃圾回收算法

       垃圾回收,即GC:Garbage Collection。在Java中,當原先分配給某對象的內存再也不被任何對象指向時,該內存便被廢棄成爲垃圾。這部分無用的內存空間須要在適當的時候被回收,以供新的對象實例使用。垃圾回收就是這種回收無用內存空間,並使其對將來實例可用的過程。編程

 

2、爲何要進行垃圾回收緩存

       因爲設備的內存空間是有限的,而程序運行時須要先加載到內存中,若是內存中垃圾過多,可用的空間太小,系統將會卡頓,甚至使得程序沒法正常運行。爲了可以充分利用內存空間,就須要對內存進行垃圾回收。 垃圾回收可以自動釋放內存空間,減輕程序員的編程負擔,JVM的一個系統級線程會自動釋放該內存塊,這就是咱們平時所熟知的,JVM爲程序員自動完成了內存的回收工做。垃圾回收將程序再也不須要的對象的「無用信息」丟棄,以便將這些空間分配給新對象使用。除了清理廢棄的對象,垃圾回收還會清除內存碎片,完成內存整理。多線程

 

3、Java GC所用算法ide

       先上思惟導圖,下圖總結了Java GC過程當中所使用的相關算法。此處分爲兩類:(1)判斷對象是否存活的算法;(2)GC不一樣階段使用的算法性能

 

        

    一、判斷對象是否存活的算法

        GC堆內存中存放着幾乎全部的對象實例,垃圾回收器在對該內存進行回收前,首先須要肯定這些對象哪些是「活着」,哪些已經「死去」,其判斷方法主要由以下兩種:

      (1)引用計數法

          該算法因爲沒法處理對象之間相互循環引用的問題,在Java中並未採用該算法,在此不作深刻探究,有興趣的可自行學習。

      (2)根搜索算法(GC ROOT Tracing)

         Java中採用了該算法來判斷對象是不是存活的。

         算法思想:經過一系列名爲「GC Roots」 的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論來講就是從GC Roots到這個對象不可達)時,則證實對象是不可用的,即該對象是「死去」的,同理,若是有引用鏈相連,則證實對象能夠,是「活着」的。以下圖所示:

               

 

          那麼,哪些能夠做爲GC Roots的對象呢?Java 語言中包含了以下幾種:

          1)虛擬機棧(棧幀中的本地變量表)中的引用的對象。

          2)方法區中的類靜態屬性引用的對象。

          3)方法區中的常量引用的對象。

          4)本地方法棧中JNI(即通常說的Native方法)的引用的對象。

          5)運行中的線程

          6)由引導類加載器加載的對象

          7)GC控制的對象

          拓展閱讀:https://www.zhihu.com/question/50381439   劉望舒的《Android進階解密》第10章,java虛擬機

    二、Java GC所用的算法

       在GC堆的不一樣區域,GC的不一樣階段中,會選擇不一樣的垃圾收集器來完成GC,固然,這些不一樣的垃圾回收器也採用了不一樣算法。

     (1)Tracing算法

                                

                                   tracing算法示意圖

        稱標記-清除(mark-and-sweep)算法,顧名思義,就是標記存活的對象,清除死去的對象。如上示意圖所示,該算法基於根搜索方法,從根集合進行掃描,對存活的對象進行標記,標記完畢後,再掃描整個空間中未被標記的對象,進行回收。標記-清除算法不須要進行對象的移動,而且僅對不存活的對象進行處理,在存活對象比較多的狀況下極爲高效,但因爲該算法直接回收不存活的對象,所以會形成內存碎片。其示意圖以下:

    (2)Compacting算法

                                 

                                                                       compacting算法示意圖

        該算法也被稱爲標記-整理(mark-and-compact)算法,顧名思義,就是標記存活的對象,整理回收後的空間。如上示意圖所示,該算法和標記-清除算法同樣先對存活的對象進行標記,而後清除掉沒有標記的對象,即不存活的對象。但與標記-清除算法不一樣的是,該算法多了一個整理的過程,在回收不存活的對象佔用的空間後,會將全部的存活對象往左端空閒空間移動,並更新對應的指針,所以解決了內存碎片的問題,固然,該算法多了一個整理的過程,進行了對象的移動,所以成本更高。在基於Compacting算法的收集器的實現中,通常增長了句柄和句柄表。

    (3)Copying算法

                               

                                                                             copying算法示意圖

       該算法的提出是爲了克服句柄的開銷和解決堆碎片的垃圾回收。如上示意圖所示,它開始時把堆分紅對象面(from space)和空閒面(to space),程序在對象面爲實例對象分配空間,當對象滿了,基於copying算法的垃圾收集器就從根基中掃描存活對象,並將每一個存活對象複製到空閒面,使得存活的對象所佔用的內存之間沒有碎片。這樣空閒面變成了對象面,原來的對象面變成了空閒面,程序會在新的對象面中分配內存。一種典型的基於copying算法的垃圾回收是stop-and-copy算法,它將堆分紅對象面和空閒區域面,在對象面與空閒區域面的切換過程當中,程序暫停執行。該算法的優勢是:不理會非存活的對象,copy數量僅僅取決於存活對象的數量,且在copy的同時,整理了heap空間,消除了內存碎片,空閒區的空間使用始終是連續的,內存使用效率獲得提升。缺點是:劃分了對象面和空閒面,內存的使用率爲1/2。收集器必須複製全部的存活對象,這增長了程序等待時間

    (4)Generation算法

                

                                                        generation算法示意圖

          不一樣的對象的生命週期是不同的,分代的垃圾回收策略正式基於這一點。所以,不一樣生命週期的對象能夠採起不一樣的回收算法,以便提升回收效率。該算法包含三個區域:年輕代(Young Generation)、年老代(Old Generation)、持久代(Permanent Generation)

      1)年輕代(Young Generation)

  • 全部新生成的對象首先都是放在年輕代中。年輕代的目標就是儘量快速地回收哪些生命週期短的對象。
  • 新生代內存按照8:1:1的比例分爲一個Eden區和兩個survivor(survivor0,survivor1)區。Eden區,字面意思翻譯過來,就是伊甸區,人類生命開始的地方。當一個實例被建立了,首先會被存儲在該區域內,大部分對象在Eden區中生成。Survivor區,倖存者區,字面理解就是用於存儲倖存下來對象。回收時先將Eden區存活對象複製到一個Survivor0區,而後清空Eden區,當這個Survivor0區也存放滿了後,則將Eden和Survivor0區中存活對象複製到另一個survivor1區,而後清空Eden和這個Survivor0區,此時的Survivor0區就也是空的了。而後將Survivor0區和Survivor1區交換,即保持Servivor1爲空,如此往復。
  • 當Survivor1區不足以存放Eden區和Survivor0的存活對象時,就將存活對象直接放到年老代。若是年老代也滿了,就會觸發一次Major GC(即Full GC),即新生代和年老代都進行回收。
  • 新生代發生的GC也叫作Minor GC,MinorGC發生頻率比較高,不必定等Eden區滿了纔會觸發。

      2)年老代(Old Generation)

  • 在新生代中經歷了屢次GC後仍然存活的對象,就會被放入到年老代中。所以,能夠認爲年老代中存放的都是一些生命週期較長的對象。
  • 年老代比新生代內存大不少(大概比例2:1?),當年老代中存滿時觸發Major GC,即Full GC,Full GC發生頻率比較低,年老代對象存活時間較長,存活率比較高
  • 此處採用Compacting算法,因爲該區域比較大,並且一般對象生命週期比較長,compaction須要必定的時間,因此這部分的GC時間比較長

      3)持久代(Permanent Generation)

        持久代用於存放靜態文件,如Java類、方法等,該區域比較穩定,對GC沒有顯著影響。這一部分也被稱爲運行時常量,有的版本說JDK1.7後該部分從方法區中移到GC堆中,有的版本卻說,JDK1.7後該部分被移除,有待考證。

        持久代中的垃圾回收,請參考下文中的「方法區的垃圾回收」。

                        

                                  Generaton算法結構思惟導圖

       現代商用虛擬機基本都採用分代收集算法來進行垃圾回收。這種算法沒什麼特別的,就是將上述多種算法結合,根據對象的生命週期的不一樣將內存劃分爲幾塊,而後根據各快的特色採用最適合的收集算法。新生代對象存活率低,使用複製算法,在內部不一樣的區內進行復制,複製成本比較低;年老代對象存活率高,沒有額外空間進行分配,採用標記-清理算法或者標記-整理算法。

    (4)參考資料

        http://www.javashuo.com/article/p-mphjinep-n.html    

4、垃圾收集器

                     

       垃圾收集器就是上一節中垃圾收集算法理論的具體實現。不一樣的虛擬機所提供的垃圾收集器可能會有很大差異,咱們平時開發用的是HotSpot虛擬機,上圖中就是該款虛擬機中所包含的全部收集器。如上一節所說,現代商業虛擬機中基本都採用了分代收集算法,上圖中就展現了在內存的不一樣Generation中,各垃圾收集器的使用狀況,在對象的不一樣生命週期中分別採用不一樣的收集器。沒有最好的垃圾收集器,也沒有萬能的收集器,只能選擇對具體應用最合適的收集器,這也是HotSpot爲何要實現這麼多收集器的緣由。

       上圖展現的7款垃圾回收器,在某個生命週期階段或單獨使用,或配合(有連線的)使用,其實現原理也是按照上一節中描述的各類收集算法實現的。至於對每一款垃圾收集器的詳細說明,本文展開講解,有興趣的能夠自行研究。

       擴展閱讀 https://blog.csdn.net/tjiyu/article/details/53983650

 

5、方法區的垃圾回收

        本文開頭就說過,GC主要發生在GC堆內存中,但並非只發生在該部分,方法區也須要進行垃圾回收。方法區和堆同樣,都是現成共享的內存區域,被用於存儲已被虛擬機加載的類信息、即時編譯後的代碼、靜態變量和常量等數據。根據Java虛擬機規範,方法區沒法知足內存分配需求時,也會拋出OutOfMemoryError異常,雖然規範規定能夠不實現垃圾收集,由於和GC堆內存的垃圾回收相比,方法區的回收效率實在過低,可是該部分區域也是能夠被回收的。

       方法區的垃圾回收主要由兩種:廢棄常量回收和無用類回收

       一、當一個常量對象不在任何地方被引用的時候,則被標記爲廢棄常量,這個常量能夠被回收。以字面量常量回收爲例,若是一個字符串"abc"已經進入常量池,可是當前系統沒有任何一個String對象引用了叫作「abc」的字面量,那麼,若是發生GC而且有必要時,"abc"就會被系統移出常量池,常量池中的其餘類(接口)、方法、字段的符號引用也與此相似。

       二、方法區中的類須要同時知足以下三個條件才能被標記爲無用的類:(1) Java堆中不存在該類的任何實例對象;(2) 加載該類的類加載器已經被回收;(3) 該類對應的java.lang.Class對象不在任何地方被引用,且沒法在任何地方經過反射訪問到該類的方法。當知足這三個條件的類才能夠被回收,但並非必定會被回收,須要參數進行控制,HotSpot虛擬機中提供了 -Xnoclassgc參數進行控制是否回收。

       在上一篇文章中講到,運行時常量在jdk1.7以前是存在於方法區的,該部分也被稱爲永久代(Permanence Generation)。在前面章節Generaton算法中,也提到了該永久代。這裏所說的方法區垃圾回收就是對Permanence Generation 的垃圾回收(這一句是筆者我的的理解,沒有權威的資料聲明這一點)。

      參考資料:https://blog.csdn.net/hutongling/article/details/68946263

      

6、引用類型

    Java中提供了四種引用方式,強引用、軟引用、弱引用、虛引用,這樣作有兩個目的:(1)可讓程序員經過代碼的方式決定某些對象的生命週期;(2)有利於JVM進行垃圾回收。

    一、強引用

     強引用是指建立一個對象,並把這個對象賦給一個引用變量。好比:

1 People people = new People();
2 String str = "abc";

       當強引用有引用變量指向時,永遠不會被JVM做爲垃圾回收,系統內存緊張時,JVM寧願拋出OutOfMemory異常,也不會回收強引用對象。好比:

複製代碼
1 public class ReferenceDemo {
 2 
 3     public static void main(String[] args) {
 4         ReferenceDemo demo = new ReferenceDemo();
 5         demo.test();
 6     }
 7 
 8     public void test() {
 9         People people = new People();
10         People[] peopleArr = new People[1000];
11     }
12 }
13 
14 class People {
15     public String name;
16     public int age;
17 
18     public People() {
19         this.name = "zhangsan";
20         this.age = 20;
21     }
22 
23     public People(String name, int age) {
24         this.name = name;
25         this.age = age;
26     }
27 
28     @Override
29     public String toString() {
30         return "[name:" + name + ",age:" + age + "]";
31     }
32 }
複製代碼

       當運行到People[] peopleArr = new People[1000];這句時, 若是內存不足,JVM會拋出OOM錯誤也不會回收object指向的對象。不過,要注意的是,當test運行完後,people和peopleArr都會不復存在,因此它們指向的對象都會被JVM回收。

       若是想中斷強引用和某個對象之間的關聯,能夠顯示地將引用賦值爲null,這樣,JVM在合適的時候就會回收該對象。好比Vector類的clear()方法中,就是經過將引用賦值爲null來實現清理工做的。

    二、軟引用(SoftReference)

    (1)使用SoftReference實現軟引用

        若是一個對象具備軟引用,內存空間足夠,垃圾回收器就不會回收它。只有當內存空間不足了,纔會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就能夠被程序使用。軟引用可用來實現內存敏感的高速緩存,好比網頁緩存、圖片緩存等。使用軟引用可以防止內存泄漏,加強程序的健壯性。

        SoftReference的特色是它的一個實例保存了對一個Java對象的軟引用,該軟引用的存在不妨礙垃圾收集線程對該Java對象的回收。一旦SoftReference保存了對一個Java對象的軟引用後,在垃圾線程對這個Java對象回收前,SoftReference類所提供的get()方法返回Java對象的強引用。另外,一旦垃圾線程回收該Java對象以後,get()方法將返回null。

     

1 People people = new People();
2 SoftReference softRef = new SoftReference<>(people);

此時,對於這個People()對象,有兩個引用路徑,一個是來自mPeople的強引用,一個是來自SoftReference的軟引用。因此這裏的這個People()對象是強可及對象。隨即,能夠經過以下方式結束mPeople對這個People對象的強引用:

1 people = null;

此後,這個People()對象成爲了軟引用對象。若是垃圾收集線程進行內存垃圾收集,並不會由於有一個SoftReference對該對象的引用而始終保留該對象。

        JVM的垃圾收集線程對軟可及對象和其餘通常Java對象進行了區別對待:軟可及對象的清理是由垃圾收集線程根據其特定算法按照內存需求決定的。垃圾收集線程會在虛擬機中拋出OutOfMemoryError以前回收軟可及對象,並且虛擬機會盡量優先回收長時間閒置不用的軟可及對象,對那些剛剛構建的或剛剛使用過的「新」軟可及對象會被JVM儘量保留。在回收這些對象以前,咱們能夠經過:

1 People softRefPeople =  (People) softRef.get();

從新得到對該實例的強引用。若是軟可及對象也被回收了,則mSoftRef.get()也只能獲得null了。

以下代碼演示了SoftReference的使用:

複製代碼
1 public class ReferenceDemo {
 2 
 3     public static void main(String[] args) {
 4         People people = new People();//來自people的強引用
 5         SoftReference softRef = new SoftReference<>(people);//來自SoftReference的軟引用
 6         people = null;//結束people對People實例的強引用。
 7         People softRefPeople = (People) softRef.get();//經過get()從新得到對People的強引用。
 8         System.out.println(softRefPeople.toString());
 9     }
10 }
複製代碼

運行結果以下:

1 [name:zhangsan,age:20]

    (2)使用ReferenceQueue清除失去了軟引用對象的SoftReference

       SoftReference對象除了具備保存軟引用的特殊性以外,它也是一個Java對象,也具備Java對象的通常特性。因此,當軟可及對象被回收以後,這個SoftReference對象的get()方法返回null,已經再也不具備存在的價值了,須要一個適當的清除機制,避免大量SoftReference對象帶來的內存泄漏。在java.lang.ref包裏還提供了ReferenceQueue。若是在建立SorftReference對象的時候,使用了一個ReferenceQueue對象做爲參數提供給SoftReference的構造方法,如:

People people = new People();
ReferenceQueue queue = new ReferenceQueue<>();
SoftReference softRef2 = new SoftReference(people,queue);

        當這個SoftReference所軟引用的people被垃圾收集器回收的同時,softRef2所強引用的SoftReference對象被列入ReferenceQueue。也就是說,ReferenceQueue中保存的對象是Reference對象,並且是已經失去了它所軟引用對象的Reference對象。另外,從ReferenceQueue這個名字能夠看出,它是一個隊列,當咱們調用它的poll()方法的時候,若是這個隊列中不是空隊列,那麼將返回隊列中第一個Reference對象。在任什麼時候候,咱們均可以調用ReferenceQueue的poll()方法來檢查是否有它所關心的非強可及對象被回收。若是隊列爲空,將返回一個null,不然該方法返回隊列中第一個Reference對象。利用這個方法,咱們能夠檢查哪一個SoftReference所軟引用的對象已經被回收。因而咱們能夠把這些失去所軟引用對象的SoftReference對象清除掉。

1 SoftReference softRef3 = null;
2 while ((softRef3 = (SoftReference)queue.poll())!=null) {
3     //清除ref
4 }

    三、弱引用(WeakReference)

      弱引用也是用來描述非必需對象的,當JVM進行垃圾回收時,不管內存是否充足,都會回收被弱引用關聯的對象。在Java中,用java.lang.ref.WeakReference類來表示。使用方法以下:

複製代碼
1 public class ReferenceDemo {
2 
3     public static void main(String[] args) {
4         WeakReference<People> weakRef = new WeakReference<People>(new People());
5         System.out.println(weakRef.get());//獲取到弱引用保護的對象。
6         System.gc();//通知JVM進行垃圾回收
7         System.out.println(weakRef.get());
8     }
9 }
複製代碼

      運行結果以下:

1 [name:zhangsan,age:20]
2 null

第二個結果爲null,說明只要JVM進行垃圾回收,被弱引用關聯的對象一定會被回收掉。不過要注意的是,這裏所說的被弱引用關聯的對象,是指只有弱引用與之關聯。若是存在強引用同時與之關聯,則進行垃圾回收時也不會回收該對象(軟引用也是如此)。好比,將代碼作一點小改動:

複製代碼
1 public class ReferenceDemo {
 2 
 3     public static void main(String[] args) {
 4         People people = new People();
 5         WeakReference<People> weakRef = new WeakReference<People>(people);
 6         System.out.println(weakRef.get());
 7         System.gc();
 8         System.out.println(weakRef.get());
 9     }
10 }
複製代碼

      運行結果以下:第二行結果有了很大的變化,再也不是null,說明沒有被回收。

1 [name:zhangsan,age:20]
2 [name:zhangsan,age:20]

       弱引用也能夠和引用隊列ReferenceQueue聯合使用,若是弱引用所引用的對象被JVM回收,這個弱引用就會被加入到與之關聯的引用隊列中,其使用方法同軟引用中的使用。

       在使用軟引用和弱引用的時候,咱們能夠顯示地經過System.gc()來通知JVM進行垃圾回收,可是要注意的是,雖然發出了通知,JVM不必定會馬上執行,也就是說,這句代碼是沒法確保此時JVM必定會進行垃圾回收的,可能會在發出通知後,在某個合適的時間進行回收。

       另外,在Android開發中,經常與Handler聯合使用,來避免內存泄漏的發生。

    四、虛引用(PhantomReference)

       在Java中,用java.lang.PhantamReference類表示。若是一個對象與虛引用關聯,則跟沒有引用與之關聯同樣,在任什麼時候候均可能被垃圾回收器回收。須要注意的是,虛引用必須和引用隊列關聯使用,當垃圾回收器準備回收一個對象時,若是發現它還有虛引用,就會把這個虛引用加入到與之關聯的引用隊列中(這裏和前面的軟引用和弱引用有所不一樣,這二者加入隊列是在對象被回收之時)。程序能夠經過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。若是程序發現某個虛引用已經被加入到引用隊列,那麼就能夠在所引用的對象的內存被回收以前採起必要的行動。

複製代碼
1 public class ReferenceDemo {
2 
3     public static void main(String[] args) {
4         ReferenceQueue<People> queue = new ReferenceQueue<>();
5         PhantomReference<People> phanRef = new PhantomReference<People>(new People(), queue);
6         System.out.println(phanRef.get());
7     }
8 }
複製代碼

      運行結果以下:

1 null

該結果驗證了前面所說的,「若是一個對象與虛引用關聯,則跟沒有引用與之關聯同樣,在任什麼時候候均可能被垃圾回收器回收」。

    五、小結

            

    六、參考資料

         http://www.javashuo.com/article/p-gdbaaain-a.html

相關文章
相關標籤/搜索