名词定义:部分收集(Partial GC):指不是完整收集 Java 堆的 GC,又分为:1.新生代收集(Minor GC/Young GC)2.老年代收集(Major GC/Old GC)目前只有 CMS 收集器会有单独收集老年代的行为 3. 混合收集(Mixed GC)指目标是收集整个新生代以及部分老年代的GC,目前只有 G1 收集器有这种行为。4. 整堆收集(Full GC)收集整个 Java 堆和方法区的 GC。
哪些内存需要回收?什么时候回收?如何回收?Java 运行时区域的各个部分,其中程序计数器 、VM 栈、本地方法栈 3 个区域随着线程而生而灭。这几个区域的内存分配和回收都具备确定性,在这几个区域不需多考虑回收问题,方法结束或线程结束时,内存会直接回收。而堆和方法区 有显著的不确定性,一个接口的多个实现类需要的内存可能不一样,一个方法所执行的不同条件所需内存也不一样。只有处于运行期 我们才能知道究竟会创建哪些对象,创建多少,这部分内存的分配和回收是动态的,垃圾收集器 所关注的正是这部分内存的如何管理。
如何判断一个对象“已死”?有两种方法①引用计数算法:在每个对象中添加引用计数器,每当有一个地方引用它时,计数器加1,为 0 就可以回收。②可达性分析算法:通过一系列“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索走过的路径为“引用链”,如果某个对象到 GC Roots 间没有任何引用链相连或用图论的话说到达 GC Roots 不可达时,则对象“已死” 。
上图来源 图中ab对象虽互相引用但也会被回收。在Java 体系中,固定可作为 GC Roots 的对象包括以下几种:1.在VM栈中引用的对象 2. 方法区中类静态属性引用的对象 3. 方法区中常量引用的对象 4. 本地方法栈中 JNI 引用的对象等等。
引用:引用计数法和可达性分析算法都用到了“引用”,JDK1.2 之后,引用被分为了强引用、软引用、弱引用和虚引用。强引用时最传统的“引用”定义,指在程序代码中普遍存在的“引用赋值”,即Object o = new Object()这种,强引用下不会被回收。软引用是用来描述一些还有用,但非必需的对象,在内存溢出前这种对象会被二次回收。弱引用下一次GC 就会♻️。虚引用没用。
生存还是死亡?即使在可达性分析算法中判定为不可达的对象,也不是“非死不可”的,这时他们还处于“缓行”期,要真正宣告一个对象的死亡,至少要经历两次标记:没有与 GC Roots 相连的对象会被第一次标记,随后进行一次筛选,筛选的条件是词对象是否有必要执行finalize(),假如对象没有覆盖finalize()或finalize()已经被 VM 调用过 ,那么 VM 视这种情况为没有必要执行。
回收方法区:Java 堆中,尤其是新生代中对常规应用进行一次 Minor GC 通常可回收70%~99%的内存空间 ,相比之下,方法区的回收效率要低得多。方法区的 GC 主要回收两部分内容:废弃的常量和不再使用的类型 。
GC 算法:JVM 主要使用的是追踪式 GC 算法。
分代收集理论:这个理论建立在两个分代假说之上①弱分代假说:绝大多数对象都是朝生夕灭 的。②强分代假说:熬过越多次GC的对象就越难以收集。这两个分代假说共同奠定了前期多款常用的 GC 的一致设计原则:收集器应将 Java 堆划分出不同的区域 ,然后将回收对象依据其年龄分配到不同的区域中存储。 再针对不同区域中存储的对象分别按不同策略回收:①如果一个区域中大多数对象都是朝生夕灭难以熬过一次 GC 的话,那每次回收时只需关注如何保留少量存货而不是区标记那些大量需被回收对象(Minor GC)②如果区域中都是难以消亡的对象,那把它们集中到一起,VM 可以使用较低频率来回收这个却与,同时兼顾了 GC 的时间开销和内存空间的有效利用(Major GC)。③对于年轻代和老年代里相互引用的对象,应该倾向于同时生存或同时消亡的,如新生代对象存在跨代引用的话,如果老年代不死,它也就存活进入了老年代,跨代也就消失了。这叫做跨代引用假说 。但是不应该为了极少数的跨代引用去扫描整个老年代,只需在新生代上建立一个全局的数据结构(“记忆集”) ,这个结构把老年代划分为若干块,标识出老年代的哪一块内存存在跨代引用,Minor GC 时只扫描这一块。 标记-清除算法(Mark- Sweep):首先标记出所有需回收的对象,在标记完成后统一回收,也可以反过来,标记过程就是对象是否是垃圾的判定过程。之所以说它基础是因为后续的 GC 算法大多是基于它的。Mark- Sweep 算法缺点:执行效率不稳定和内存空间碎片化问题。 标记-复制算法(Mark- Copy):基于“半区复制”的GC 算法,它将可分配内存按容量划分为大小相等的两块,每次只使用其中一块,当这一块用完了,就将还存活着的对象全复制到另一块上面 ,然后再把已使用过的内存空间全清理掉。这种算法实现简单,运行高效,不过其缺点也很明显,将原可用的内存缩小一半,空间浪费太大。现在的商用 JVM 大多优先采用这种算法回收新生代,新生代中的对象有 98% 熬不过第一轮收集,因此并不需要按照 1:1 的比例来划分新生代的内存空间 。后来,更优化的半区复制分代策略是“Appel 回收”,hotspot VM 的 Serial ParNew 等新生代收集器均采用了这种策略来设计新生代的内存布局。Appel 式回收的具体做法是把新生代分为一块较大的 Eden 空间和两块较小的 Survivor 空间 ,每次分配内存只使用 Eden 和其中一块 Survivor 区,发生 GC 时,将 Eden 和 Survivor 中仍存活的对象一次性复制到另外一块 Survivor 上,然后清理掉前两块。HotSpot 默认它们的比例是 8:1:1,当 Survivor 空间不足以容纳一次 Minor GC 之后存活的对象时,就需依赖其他内存区域(大多时候是老年代)进行分配担保。(这一段是面试常问的) 标记-整理算法(Mark- Compact):针对老年代对象的存亡特征,出现了这种算法,其中的标记过程和标记-清除一样,但后续不是直接堆可回收对象进行清理,而是让所有存活对象都向一侧移动,然后直接清理掉边界以外的内存 。标记清除和标记整理的本质差异是前者是一种非移动式回收算法,后者是移动式的。如果移动存活对象,尤其是老年代这种每次回收都得移动大量对象,将会是极为负重的操作,而且这种对象移动操作必须全程暂停用户线程才能进行(STW),但如果不移动和整理的话就会导致大量空间碎片化。基于上述,是否移动对象都有弊端,所以不同的收集器采用了不同或多种算法来实现对老年代的收集。