【朝花夕拾】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

相关文章
相关标签/搜索