安卓面试必备的JVM虚拟机制详解,看完之后简历上多一个技能( 三 )


GC在垃圾收集器回收对象时 , 先要判断对象是否已经不再使用了 , 有引用计数法和可达性分析两种 。
引用计数及可达性分析引用计数法就是给对象添加一个引用计数器 , 每当有一个地方引用时就加一 , 引用失效时就减一 。 引用计数实现简单 , 判断效率也很高 , 但是 JVM 并没有采用引用计数来管理内存 , 其中最主要的原因是它很难解决对象之间的相互循环引用问题 。 可达性分析的思路是通过一系列称为 GC Roots 的对象作为起始点 , 从这些起始点出发向下搜索 , 当有一个对象到 GC Roots 没有任何引用链时 , 即不可达 , 则说明此对象是不可用的 。 在 Java 中 , 可作为 GC Roots 的对象有虚拟机栈和本地方法栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象等 。
但这也并不是说引用计数一无是处 , 在 Android 的 Framework Native 层用的智能指针 。 智能指针就是一种能够自动维护对象引用计数的技术 , 它是一个对象而不是一个指针 , 但是它引用了一个实际使用的对象 。 简单来说 , 就是在智能指针构造时 , 增加它所引用的对象的引用计数;而在智能指针析构时 , 就减少它所引用对象的引用对象 。 但是它是怎样解决相互引用问题的呢?其实是通过强弱引用来实现 , 也就是将对象的引用计数分为强引用计数和弱引用计数两种 , 其中 , 对象的生命周期只受强引用计数控制 。 比如在解决对象 A 和 B 相互引用时 , 把 A 看成父 B 看成子 , 对象 A 通过强引用计数来引用 B , B 通过弱引用计数来引用 A 。 在 A 不再使用时 , 由于 B 是通过弱引用来引用它的 , 因此 A 的生命周期是不受 B 影响的 , 所以 A 可以安全的释放 , 在释放 A 时 , 同时也会释放它对 B 的强引用 , 这时 B 也可以被安全的回收了 。 在 Android 中 , 是使用 sp 来表示强引用 , wp 表示弱引用 。
Java 中的引用可以分为四类 , 强引用、软引用、弱引用和虚引用 。 强引用在程序中普遍存在 , 类似 new 的这种操作 , 只要有强引用存在 , 即使 OOM JVM 也不会回收该对象 。 软引用是在内存不够用时 , 才会去回收 , JDK 提供了 SoftReference 类来实现软引用 。 弱引用是在 GC 时不管内存够不够用都会去回收的 , 可以使用 WeakReference 类来实现弱引用 。 虚引用对对象的生命周期没有影响 , 只是为了能在对象回收时收到一个系统通知 , 可以使用 PhantomReference 类来实现虚引用 。
接下来就是要讲垃圾回收算法了 。
垃圾回收算法垃圾回收算法主要有标记清除、复制算法、标记整理 。 标记清除是先通过 GC Roots 标记所存活的对象 , 然后再统一清除未被标记的对象 , 它的主要问题是会产生内存碎片 。 老年代使用的 CMS 收集器就是基于标记清除算法 。 复制算法是把内存空间划分为两块 , 每次分配对象只在一块内存上进行分配 , 在这一块内存使用完时 , 就直接把存活的对象复制到另外一块上 , 然后把已使用的那块空间一次清理掉 , 但是这种算法的代价就是内存的使用量缩小了一半 。 现代虚拟机都采用复制算法回收新生代 , 不过是把内存划分为了一个 Eden 区和两个 Survivor 区 , 比例是 8:1:1 , 每次使用 Eden 和其中一块 Survivor 区 , 也就是只有 10% 的内存会浪费掉 。 如果 Survivor 空间不够用 , 需要依赖其他内存比如老年代进行分配担保 。 复制算法在对象存活率比较高时效率是比较低下的 , 所以老年代一般不使用复制算法 。 标记整理算法即是在标记清除之后 , 把所有存活的对象都向一端移动 , 然后清理掉边界以外的内存区域 。
最后就是讲垃圾回收算法的具体应用了 , 也就是垃圾收集器 。
G1 及 ZGCGarbage First(G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果 , 它开创了收集器面向局部收集的设计思路和基于 Region 的内存布局形式 。 它和 CMS 同样是一款主要面向服务端应用的垃圾收集器 , 不过在 JDK9 之后 , CMS 就被标记为废弃了 , G1 作为默认的垃圾收集器 , 在 JDK 14 已经正式移除 CMS 了 。 在 G1 收集器出现之前的所有其他收集器 , 包括 CMS 在内 , 垃圾收集的目标范围要么是整个新生代(Minor GC) , 要么就是整个老年代(Major GC) , 在要么就是整个 Java 堆(Full GC) 。 而 G1 是基于 Region 堆内存布局 , 虽然 G1 也仍是遵循分代收集理论设计的 , 但其堆内存的布局与其他收集器有非常明显的差异:G1 不再坚持固定大小以及固定数量的分代区域划分 , 而是把连续的 Java 堆划分为多个大小相等的独立区域(Region) , 每一个 Region 都可以根据需要 , 扮演新生代的 Eden 空间、Survivor 空间或者老年代 。 收集器根据 Region 的不同角色采用不同的策略去处理 。 G1 会根据用户设定允许的收集停顿时间去优先处理回收价值收益最大的那些 Region 区 , 也就是垃圾最多的 Region 区 , 这就是 Garbage First 名字的由来 。


推荐阅读