当前位置: 首页 > 科技观察

Android之java的GC垃圾回收机制详解-层层分析步步深入

时间:2023-03-18 14:09:56 科技观察

JavaforAndroidGC垃圾回收机制详解——层层剖析循序渐进程序员写程序时再也不用考虑内存管理问题了。由于垃圾回收机制,Java中的对象不再有“作用域”的概念,只有对象引用才有“作用域”。垃圾回收可以有效防止内存泄露,有效利用空闲内存;每个人都应该了解垃圾收集机制。既是面试的常客,也是Java系统中非常重要的知识点。深入理解Java的GC机制,不仅可以帮助我们在开发过程中提升程序的性能,也有在面试官面前炫耀自己技能的资本。本文将全面深入地剖析JVM的垃圾回收机制并进行讲解。对象的创建由JVM完成。当对象被创建时,JVM会在Java堆中开辟一块空间来存放对象。当对象“死亡”时,也是由JVM处理的。JVM处理“死亡”的对象进程就是垃圾回收机制。一、GC机制1、堆内存区划分堆内存区的划分实际上是由垃圾回收器的特性决定的。为了方便JVM为了更好的管理和回收对象,Java设计者将Java堆内存分为两大块,分别是:YoungGeneration和OldGeneration。根据新生代的特点,新生代分为一个较大的Eden区和两个较小但equal-sizedSurvivorareas。至于新时代和旧时代这两个区域,就是我们今天讨论的重点。垃圾回收的特点。垃圾回收器进行垃圾回收时,可能是部分回收ParticalGC)orfullheapcollection(FullGC).部分收集可以分为新生代收集(MinorGC/YoungGC)和老年代收集(MajorGC/OldGC).由于有这样的师,采集者回收区的规则是在什么条件下确定的?JDK6之后,对于回收区的规则是:只要老年代的连续空间大于新生代对象的总大小或者之前提升的平均大小,就会进行MinorGC,否则会进行FullGC被执行。对象通常是在Eden区创建的,JVM会为每个对象定义一个年龄(Age)计数器,保存在对象头中。如果对象在第一次MinorGC后还活着,并且可以容纳在Survivor区,则将对象移动到Survivor区,并将对象的年龄设置为1岁。接下来,该对象会经历多次垃圾回收,Survivor区的对象每经历一次MinorGC,其年龄就会增加一年。如果对象增加到一定年龄(默认15,可以通过-XX:MaxTenuringThreshold参数设置),就会被移到老年代。当然,为了更好的适应不同程序的内存情况,HotSpot虚拟机并不绝对要求对象到了年龄就转移到老年代。一半的Survivor空间,年龄大于等于这个年龄的对象可以直接进入老年代。②对于大对象,可以通过-XX:PretenureSizeThreshold参数设置HotSpot虚拟机。当对象内存大于设定值时,对象会绕过Eden区,直接分配给老年代。2、永久代(PermanentGeneration)在JDK7及之前的版本中,HotSpot虚拟机还有另外一个存储区域,叫做永久代(PermanentGeneration)。该区域不属于堆内存,而是方法区的实现。主要用来存放Class和Meta(元数据)信息,Class在加载时就被放入永久代。永久代不同于存放实例的堆内存区,GC在主程序运行过程中不会清理永久代。因此,这也导致永久代的区域会随着加载类的增多而被占满,最终抛出OOM异常。虽然叫做永久代,但是这块内存区域也是被垃圾回收的。永久代的垃圾回收主要包括过时的常量和无用的类(被类加载器卸载的类)。永久代触发垃圾回收的条件比较难,需要同时满足以下三点:①该类的所有实例都已被回收,即Java中没有该类的实例堆;②加载这个类的ClassLoader已经被回收了;③这个类对应的java.lang.Class对象没有被任何地方引用,任何地方都无法通过反射访问到这个类的方法。3、元空间(MetaSpace)由于永久代可能会出现内存溢出问题。之后永久代不复存在,取而代之的是MetaSpace。元空间的本质和永久代类似,但是元空间和永久代最大的区别是元空间不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受限于本地内存,但可以通过-XX:MetaspaceSize参数指定初始空间大小。当达到最大值时,会触发垃圾回收进行类型卸载,GC会对该值进行调整:如果释放大量空间,则应适当减小该值;如果释放少量空间,应适当增加该值,不要超过MaxMetaspaceSize。可以使用-XX:MaxMetaspaceSize来设置元空间可以使用的最大内存,默认是无限制的。除了上面两个指定大小的选项外,还有两个GC相关的属性:-XX:MinMetaspaceFreeRatio,GC后,最小Metaspace剩余空间容量的百分比,减少为分配空间引起的垃圾回收  -XX:MaxMetaspaceFreeRatio,GC后最大Metaspace剩余空间容量的百分比,减少释放空间引起的垃圾回收。垃圾回收相关的区域如下图所示:图中的PermanetGeneration区域在Jdk8中被MetaSpace区域代替。2.垃圾收集的标记算法垃圾收集器的第一步是确定哪些对象可以被收集。回收。因此,JVM会扫描堆内存中的所有对象,并对可以回收的对象进行标记。垃圾回收的标记算法有两种:1.引用计数算法引用计数算法为每个对象添加一个计数器,当有对它的引用时,计数器的值会加1;计数器的值会减1。当计数器的值为0时,可以认为该对象不再被使用。因此,对于引用计数算法,垃圾回收器只需要回收计数器为0的对象即可。引用计数算法的优点是效率很高,不需要遍历所有对象。但是它有一个致命的缺点,就是不能解决对象之间的循环引用问题。比如对象A引用了对象B,对象B也引用了对象A。另外,A和B这两个对象从来没有在别处被引用过。此时对象A和对象B的计数器都不为0,所以对象A和B都不能被回收。因此,目前商用的Java虚拟机都没有使用引用计数算法进行标记。2.可达性分析算法可达性分析算法也称为寻根算法。该算法的基本思想是使用一系列“GCRoots”根对象作为起始节点集。从这些节点开始,按照引用关系向下查找。搜索过程所经过的路径称为“引用链”。(参考链)。如果一个对象与“GCRoots”没有任何引用链连接,则证明这个对象可能不再被使用。如下图,灰色部分的对象没有关联引用链,此时这些对象会被判断为可回收对象。哪些对象可以作为GCRoots?主要包括以下几种:①虚拟机栈中引用的对象(栈帧中的局部变量表)。②方法区中类静态属性引用的对象。③方法区引用的对象,如字符串常量池(StringTable)中的引用JNI在本地方法栈中引用的对象④Java虚拟机内部的引用,如基本数据类型对应的Class对象和一些常驻异常对象等⑤所有同步锁持有的对象、反映Java虚拟机内部情况的JMXBean、注册在JVMTI中的回调、本地代码缓存等三、垃圾收集算法1、标记-清除算法(Mark-Sweep)Mark-Sweep算法是最早也是最基本的垃圾回收算法。该算法分为“标记”和“清除”两个阶段。标记阶段就是上面提到的垃圾标记。首先通过可达性分析算法对所有需要回收的对象进行标记,然后统一回收所有标记的对象。mark-clear算法的执行过程如下图所示:图中深灰色区域为可回收区域,标记完成后直接清理深灰色区域。该算法简单易懂,实现方便,但也有两个缺点:①.执行效率会随着对象数量的增加而降低。如果Java堆中包含大量需要回收的对象。这需要大量的标记和清除操作。这样一来,标记和清除这两个过程都需要大量的时间,降低了执行效率,造成严重的内存碎片问题。标记和清除后会产生大量不连续的内存空间,可能导致需要分配大对象时无法找到足够的连续空间,进而触发GC2。标记复制算法(Copying)标记复制算法也称为复制算法。它是对标记-扫描算法的改进。复制算法将内存分成大小相等的两块,分配对象时只使用其中一块。当这块内存用完后,将存活的对象复制到另一块,然后一次性清理掉这块内存。复制算法的执行过程如下图所示:虽然复制算法解决了mark-clear算法的一些问题。但是它的缺陷也很明显,直接导致可用内存变成原来的一半,内存使用率太低;3.标记-紧凑算法(Mark-Compact)标记-紧凑算法在标记完存活对象后,会将所有存活对象移动到内存的一端,然后直接清空边界外的内存。该算法的示意图如下图所示:移动幸存对象并更新所有移动对象的引用是一个比较耗时的操作。而且,移动对象时必须暂停所有用户线程(这种操作有一个专有名词叫“StopTheWorld”,简称STW),拖累了用户程序的执行效率;4.分代收集(GenerationalCollection)分代收集不能看成是一种算法。它会根据堆内存的不同区域采用不同的收集算法,因地制宜。比如我们上面说过,在G1收集器之前,所有的收集器都是将Java堆分为新生代和老年代。由于新生代的对象存活率比较低,新时代使用优化复制。算法。在HotSpot虚拟机中,Eden和Survivor的大小按照8:1的比例进行划分。分配的对象只使用Eden和其中一个Survivor区域。标记完成后,将存活对象复制到另一个Survivor空间,然后清空Eden。并使用了一块幸存者。这样新生代的空间利用率达到90%。对于年老代,每次垃圾回收中存活的对象较多,因此该区域使用标记-排序算法进行垃圾回收。四、垃圾收集器垃圾收集器其实就是上述原理的实现,但是在Java的发展史上,有过几代的垃圾收集器,新一代的垃圾收集器都是对上一代的垃圾收集器。弥补收藏家的不足。直到几天前(2020年9月15日),OracleJDK15引入了新的垃圾收集器Shenandoah。可见,直到今天,Java的设计者还在优化收集器。几个经典的垃圾收集器,图中的连接表示这两个收集器可以一起使用1.新生代收集器①串行收集器串行收集器是最基础的收集器,发展历史也最悠久。它是一个单线程收集器。对于早期的单核处理器或者处理器核数较少的情况下,Serial收集器由于没有线程交互开销,收集效率比较高。但是Serial收集器整个收集过程都需要STW。这也是早期Java程序运行缓慢的主要原因之一。新一代的Serial收集器使用的是mark-copy算法,运行过程如下图所示②ParallelScavenge收集器ParallelScavenge收集器也是新一代的收集器,也是基于mark-copy算法实现的。它也是一个可以并行收集的多线程收集器。从表面上看,它与ParNew非常相似。ParallelScavenge收集器的目标是实现一个可控的吞吐量(Throughput);吞吐量=(运行用户代码的时间)/(运行用户代码时间+运行垃圾回收时间)ParallelScavenge收集器的运行过程如下图所示:2.Oldgenerationcollector①SerialOldcollectorSerialOld是旧的串行收集器的生成版本,它是一个单线程收集器。SerialOld使用标记-整理算法。它的主要意义也是提供客户端模式下HotSpot虚拟机的使用。②ParallelOld收集器ParallelOld是ParallelScavenge收集器的老版本,支持多线程并发收集,基于mark-sort算法。该收集器从JDK6开始可用。③CMS收集器CMS(ConcurrentMarkSweep)收集器是一个划时代的收集器。前面我们提到的几个收集器在整个工作过程中都需要STW,而CMS首次实现了垃圾收集的并发处理。因此,该收集器可以有效减少垃圾收集时的停顿时间。CMS收集器是基于mark-sweep算法实现的。下面我们来详细了解一下CMS的工作过程:(1)初始标记:从GCRoots开始标记所有直接子节点的过程,这个阶段就是STW。由于GCRoot的数量很少,这个阶段通常需要很短的时间。(2)并发标记:并发标记阶段是指从GCRoots开始,对堆中的对象进行可达性分析,找出存活对象。这个阶段是并发的,即应用线程和GC线程可以同时处于活动状态。并发标记需要比较长的时间,但是因为不是STW,所以我们不太在意这个阶段的时间长短。(3)Remark:对那些在并发标记阶段发生变化的对象进行Remark。这个阶段是STW。(4)不清洗:并行清洗,启动用户线程,同时GC线程开始清洗标记区域。从上面的描述可以看出,CMS是可以并发收集的,有效的减少了停顿时间。但是CMS并不是一个完美的垃圾收集器,否则不会在JDK15中被移除。其缺点主要有以下几点:(1)并发收集占用CPU资源。虽然并发阶段不会导致用户暂停,但并发收集线程会占用部分CPU资源,拖慢应用程序并降低吞吐量。(2)无法处理漂浮垃圾。在CMS的并发标记和清理阶段,用户线程不断运行,这期间必然会产生新的垃圾对象。对于已经被回收的区域,CMS已经不能再回去处理了,只能等到下一次垃圾回收的时候再进行清理。(3)在并发清理阶段,需要保证足够的内存。由于在垃圾回收阶段用户线程还在运行,所以必须预留足够的空间给用户线程使用。因此,CMS收集器需要在垃圾收集开始时预留足够的内存。在JDK5的默认设置下,当老年代使用了68%的空间时,垃圾收集将被激活。虽然可以通过参数-XX:CMSInitiatingOccupancyFraction增加CMS的触发百分比,但是这会导致CMS运行时预留内存不足。这时CMS会出现“ConcurrentModeFailure”(并发模式失败),虚拟机不得不启动备份计划,停止用户线程的执行,启动SerialOld收集器重新收集晚年的垃圾。(4)产生大量碎片空间。由于CMS采用“标记-清除”算法,会造成大量空间碎片。总结:本文从堆的产生到垃圾回收算法再到垃圾回收器都做了比较详细的分解,一步步分析,以期让老手们多了解gc。