安卓复习之旅—JavaGC 机制

alan-kim 3年前
   <p>概述因为在之前的内存优化 博客中已经提到了Java的内存区域,而垃圾回收是针对堆内存而言的,所以就把堆内存再深入的讲一下,然后再讲GC机制;</p>    <p><strong>堆内存模型</strong></p>    <p>堆内存由垃圾回收器的自动内存管理系统回收,分为两大部分:新生代和老年代。老年代主要存放应用程序中生命周期长的存活对象。新生代又分为三个部分:一个Eden区和两个Survivor区,Eden区存放新生的对象,Survivor存放每次垃圾回收后存活的对象。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/4234a7f1394ed44f3d6d451acf7143d9.png"></p>    <p><strong>可回收对象的判定</strong></p>    <p>讲算法之前,我们先要搞清楚一个问题,什么样的对象是垃圾(无用对象),需要被回收?</p>    <p>目前市面上有两种算法用来判定一个对象是否为垃圾。</p>    <p>1. 引用计数算法<br> 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。</p>    <p style="text-align: center;"><br> <img src="https://simg.open-open.com/show/13df96bd23cd39780365022757f7cbeb.png"><br> 优点是简单,高效。<br> 缺点是很难处理循环引用,比如图中相互引用的两个对象则无法释放。</p>    <p>2. 可达性分析算法(根搜索算法)<br> 为了解决上面的循环引用问题,Java采用了一种新的算法:可达性分析算法。<br> 从GC Roots(每种具体实现对GC Roots有不同的定义)作为起点,向下搜索它们引用的对象,可以生成一棵引用树,树的节点视为可达对象,反之视为不可达。</p>    <p style="text-align: center;"><br> <img src="https://simg.open-open.com/show/e2fda69611f49310de73506a868d5f2d.png"></p>    <p><br> OK,即使循环引用了,只要没有被GC Roots引用了依然会被回收,完美!<br> 但是,这个GC Roots的定义就要考究了,Java语言定义了如下GC Roots对象:</p>    <pre>  <code class="language-java">虚拟机栈(帧栈中的本地变量表)中引用的对象。  方法区中静态属性引用的对象。  方法区中常量引用的对象。  本地方法栈中JNI引用的对象。</code></pre>    <p>因为垃圾回收的时候,需要整个的引用状态保持不变,否则判定是判定垃圾,等我稍后回收的时候它又被引用了,这就全乱套了。所以,GC的时候,其他所有的程序执行处于暂停状态,卡住了。</p>    <p>幸运的是,这个卡顿是非常短(尤其是新生代),对程序的影响微乎其微 (关于其他GC比如并发GC之类的,在此不讨论)。</p>    <p>所以GC的卡顿问题由此而来,也是情有可原,暂时无可避免。</p>    <h3>几种垃圾回收算法</h3>    <p>1. 标记清除算法 (Mark-Sweep)</p>    <p>标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。</p>    <p>优点是简单,容易实现。</p>    <p>缺点是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/8a9c4638185df466cd7bb09f4c6af808.png"></p>    <p>2. 复制算法 (Copying)<br> 复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。<br> 优缺点就是,实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。<br> 从算法原理我们可以看出,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。</p>    <p style="text-align: center;"><br> <img src="https://simg.open-open.com/show/0f943181b75506ba455acc2cdd3f15af.png"></p>    <p>3. 标记整理算法 (Mark-Compact)<br> 该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。<br> 所以,特别适用于存活对象多,回收对象少的情况下。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/a3b097d663d745607c7fb85c7048734c.png"></p>    <p>4. 分代回收算法<br> 分代回收算法其实不算一种新的算法,而是根据复制算法和标记整理算法的的特点综合而成。这种综合是考虑到java的语言特性的。<br> 这里重复一下两种老算法的适用场景:</p>    <pre>  <code class="language-java">复制算法:适用于存活对象很少。回收对象多  标记整理算法: 适用用于存活对象多,回收对象少</code></pre>    <p>刚好互补!不同类型的对象生命周期决定了更适合采用哪种算法。</p>    <p>于是,我们根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Old Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。</p>    <p>这就是分代回收算法。</p>    <p>现在回头去看堆内存为什么要划分新生代和老年代,是不是觉得如此的清晰和自然了?</p>    <p>我们再说的细一点:</p>    <pre>  <code class="language-java">1.对于新生代采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,采用Copying算法效率最高。  2.由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法。</code></pre>    <p>Eden空间和两块Survivor空间的工作流程</p>    <p>现在假定有新生代Eden,Survivor A, Survivor B三块空间和老生代Old一块空间。</p>    <pre>  <code class="language-java">// 分配了一个又一个对象  放到Eden区  // 不好,Eden区满了,只能GC(新生代GC:Minor GC)了  把Eden区的存活对象copy到Survivor A区,然后清空Eden区(本来Survivor B区也需要清空的,不过本来就是空的)  // 又分配了一个又一个对象  放到Eden区  // 不好,Eden区又满了,只能GC(新生代GC:Minor GC)了  把Eden区和Survivor A区的存活对象copy到Survivor B区,然后清空Eden区和Survivor A区  // 又分配了一个又一个对象  放到Eden区  // 不好,Eden区又满了,只能GC(新生代GC:Minor GC)了  把Eden区和Survivor B区的存活对象copy到Survivor A区,然后清空Eden区和Survivor B区  // ...  // 有的对象来回在Survivor A区或者B区呆了比如15次,就被分配到老年代Old区  // 有的对象太大,超过了Eden区,直接被分配在Old区  // 有的存活对象,放不下Survivor区,也被分配到Old区  // ...  // 在某次Minor GC的过程中突然发现:  // 不好,老年代Old区也满了,这是一次大GC(老年代GC:Major GC)  Old区慢慢的整理一番,空间又够了  // 继续Minor GC  // ...  // ...</code></pre>    <p>触发GC的类型</p>    <p>了解这些是为了解决实际问题,Java虚拟机会把每次触发GC的信息打印出来来帮助我们分析问题,所以掌握触发GC的类型是分析日志的基础。</p>    <pre>  <code class="language-java">GC_FOR_MALLOC: 表示是在堆上分配对象时内存不足触发的GC。  GC_CONCURRENT: 当我们应用程序的堆内存达到一定量,或者可以理解为快要满的时候,系统会自动触发GC操作来释放内存。  GC_EXPLICIT: 表示是应用程序调用System.gc、VMRuntime.gc接口或者收到SIGUSR1信号时触发的GC。  GC_BEFORE_OOM: 表示是在准备抛OOM异常之前进行的最后努力而触发的GC。</code></pre>    <p><strong>程序如何与GC进行交互</strong></p>    <p>Java2 增强了内存管理功能,增加了一个java.lang.ref包,其中定义了三种引用类。这三种引用类分别为SoftReference、 WeakReference和 PhantomReference.通过使用这些引用类,程序员可以在一定程度与GC进行交互,以便改善GC的工作效率。这些引用类的引用强度介于可达对象和不可达对象之间。</p>    <p>创建一个引用对象也非常容易,例如如果你需要创建一个Soft Reference对象,那么首先创建一个对象,并采用普通引用方式(可达对象);然后再创建一个SoftReference引用该对象;最后将普通引用设置为null.通过这种方式,这个对象就只有一个Soft Reference引用。同时,我们称这个对象为Soft Reference 对象。</p>    <p>Soft Reference的主要特点是据有较强的引用功能。只有当内存不够的时候,才进行回收这类内存,因此在内存足够的时候,它们通常不被回收。另外,这些引用对象还能保证在Java抛出OutOfMemory 异常之前,被设置为null.它可以用于实现一些常用图片的缓存,实现Cache的功能,保证最大限度的使用内存而不引起OutOfMemory.以下给出这种引用类型的使用伪代码;</p>    <pre>  <code class="language-java">//申请一个图像对象           Image image=new Image();//创建Image对象           …           //使用 image           …           //使用完了image,将它设置为soft 引用类型,并且释放强引用;           SoftReference sr=new SoftReference(image);           image=null;            …            //下次使用时            if (sr!=null) image=sr.get();            else{            //由于GC由于低内存,已释放image,因此需要重新装载;            image=new Image();           sr=new SoftReference(image);           }</code></pre>    <p>Weak 引用对象与Soft引用对象的最大不同就在于:GC在进行回收时,需要通过算法检查是否回收Soft引用对象,而对于Weak引用对象,GC总是进行回收。Weak引用对象更容易、更快被GC回收。虽然,GC在运行时一定回收Weak对象,但是复杂关系的Weak对象群常常需要好几次 GC的运行才能完成。Weak引用对象常常用于Map结构中,引用数据量较大的对象,一旦该对象的强引用为null时,GC能够快速地回收该对象空间。</p>    <p>Phantom 引用的用途较少,主要用于辅助finalize函数的使用。Phantom对象指一些对象,它们执行完了finalize函数,并为不可达对象,但是它们还没有被GC回收。这种对象可以辅助finalize进行一些后期的回收工作,我们通过覆盖Reference的clear()方法,增强资源回收机制的灵活性。</p>    <p>一些Java编程的建议根据GC的工作原理,我们可以通过一些技巧和方式,让GC运行更加有效率,更加符合应用程序的要求。一些关于程序设计的几点建议:</p>    <p>1.最基本的建议就是尽早释放无用对象的引用。大多数程序员在使用临时变量的时候,都是让引用变量在退出活动域(scope)后,自动设置为 null.我们在使用这种方式时候,必须特别注意一些复杂的对象图,例如数组,队列,树,图等,这些对象之间有相互引用关系较为复杂。对于这类对象,GC 回收它们一般效率较低。如果程序允许,尽早将不用的引用对象赋为null.这样可以加速GC的工作。</p>    <p>2.尽量少用finalize函数。finalize函数是Java提供给程序员一个释放对象或资源的机会。但是,它会加大GC的工作量,因此尽量少采用finalize方式回收资源。</p>    <p>3.如果需要使用经常使用的图片,可以使用soft应用类型。它可以尽可能将图片保存在内存中,供程序调用,而不引起OutOfMemory.</p>    <p>4.注意集合数据类型,包括数组,树,图,链表等数据结构,这些数据结构对GC来说,回收更为复杂。另外,注意一些全局的变量,以及一些静态变量。这些变量往往容易引起悬挂对象(dangling reference),造成内存浪费。</p>    <p>5.当程序有一定的等待时间,程序员可以手动执行System.gc(),通知GC运行。</p>    <p> </p>    <p>来自:http://blog.csdn.net/lin_t_s/article/details/53557188</p>    <p> </p>