
一、 垃圾回收的核心思想可达性分析垃圾回收GCGarbage Collection到底是在回收什么它的基本思想可以归纳为一句话考察每一个对象的“可触及性”。如何判断“活”的JVM设定了一些“根对象GC Roots”如栈帧中的本地变量、静态变量等。从这些根节点出发只要能够顺着引用链一路追踪到的对象就说明它正在被使用是活着的。如何判断“死”的如果从所有的根节点出发都无法访问到某个对象说明这个对象已经“与世隔绝”可以被当作垃圾回收了。二、 四大核心垃圾回收算法演进虚拟机并没有使用单一算法而是根据不同场景采用了不同策略。我们来看看它们各自的优劣。1. 引用计数法被淘汰的“古老方法”这是最简单粗暴的方法每个对象配一个计数器有人引用就1引用失效就-1。计数器为0就回收。优点实现极其简单实时性好。致命缺点无法解决“循环引用”这是它被Java抛弃的根本原因。比如对象A引用了BB引用了A但除此之外没别人引用它们了。此时A和B的计数器都是1它们都认为对方还活着导致垃圾无法被回收造成内存泄漏。性能消耗每次赋值都要加减计数器对系统性能有损耗。结论Java 虚拟机从未采用引用计数法。2. 标记清除法Mark-Sweep现代算法的基础这是最基础的算法分为两个阶段标记通过根节点把能访问到的存活对象统统打上标记。清除把没有标记的对象直接抹掉释放空间。优点不需要额外的空间实现起来相对直接。致命缺点内存碎片化。回收后的内存空间是不连续的。就像一块巨大的奶酪被挖得千疮百孔。如果你想要分配一个大对象比如一个大数组因为找不到连续的空间明明总空间够却分配失败触发后续的垃圾回收严重降低效率。3. 复制算法Copying新生代的“救星”既然标记清除会有碎片那我们把内存一分为二每次只用一半如何划分把内存分成大小相等的两块 A 和 B。只使用 A。回收时把 A 中存活的对象一口气全部复制到 B 中。然后直接把 A 全部清空。交换交换 A 和 B 的角色下次只使用 B。优点绝对无碎片对象是紧密复制到新空间的排列整齐。效率极高特别适合“垃圾多、存活对象少”的场景。需要拷贝的只有那么几个幸存者。致命缺点内存利用率低直接打折。只能使用总内存的一半另一半总是闲置对内存开销太大。 小贴士因为在新生代中90%的对象都会“朝生夕灭”活不过第一次回收所以复制算法在新生代大放异彩。4. 标记压缩法Mark-Compact老年代的“终极武器”那如果在老年代对象存活率高用复制算法岂不是要把大量的对象搬来搬去成本极高于是标记压缩法应运而生它也被称为Mark-Sweep-Compact标记-清除-压缩。标记和标记清除一样先找出所有存活对象。压缩关键不直接清除垃圾。而是将所有的存活对象往内存的一端移动挤在一起。清除清理掉边界以外的所有内存空间。优点既解决了碎片化问题又不需要像复制算法那样浪费一半的内存。缺点移动对象需要暂停应用线程Stop-The-World如果存活对象太多移动成本较高。三、 实战组合分代算法Generational Collection既然每种算法都有优缺点聪明的JVM设计者提出了“分代假设”“绝大多数对象都是朝生夕死的熬过多次回收的对象则很难死去。”基于这个假设JVM将堆内存划分为新生代和老年代分别使用不同的算法。 新生代Young Generation - 使用【复制算法】特点对象存活率极低。内存分配并不是简单的 1:1 分割。为了优化空间利用率JVM 将新生代划分为Eden伊甸园占 80%。新对象诞生地。Survivor 0 (From)占 10%。Survivor 1 (To)占 10%。回收过程Minor GC回收时Eden 和 From 区中的存活对象会一股脑复制到To区。如果对象特别大或者 To 区满了就直接晋升到老年代。清理完 Eden 和 From 后交换From 和 To 的角色。 为什么这样设计这样既保留了复制算法“无碎片、效率高”的优点又将空间浪费从50%降低到了10%只浪费一个Survivor区。 老年代Old Generation - 使用【标记压缩法】特点存活率高生命周期长。回收过程Major GC / Full GC使用标记压缩法效率更高但同时产生的停顿时间也明显长于新生代回收。 冷知识卡表Card Table - 解决跨代引用性能瓶颈老年代的对象有可能引用新生代的对象。如果在新生代回收时要遍历整个老年代去检查有没有引用那速度慢得不可想象。JVM 使用了一个巧妙的位图数据结构——卡表工作原理将老年代划分成许多 4KB 的小块。卡表中的一个 bit 就代表这块 4KB 区域。0表示这个 4KB 区域内的老年代对象没有任何一个引用了新生代对象。1表示这个 4KB 区域内至少有一个对象引用了新生代对象。效果在新生代 GC 时只需查看卡表。如果卡表位是 0直接跳过该区域。这大大减少了扫描对象的时间加速了垃圾回收。四、 现代趋势分区算法G1收集器的核心说完分代我们再来看一种近年来非常火热的思想——分区算法主要应用于 G1 垃圾收集器。痛点分代回收中一旦触发 Full GC针对整个老年代/堆如果内存巨大停顿时间STW会非常久严重影响业务。解决办法放弃物理上的新生代/老年代划分把整个堆空间划分成若干大小相等的“小区间”Region。每一个小区间都可以独立使用、独立回收。优势由于区间很小GC 时不需要回收整个堆而是根据目标停顿时间每次回收一部分比如 20 个区间。这就把原来几秒钟的大停顿拆解成了几十毫秒的小停顿避免了“全屋大扫除”带来的业务阻塞。五、 技术总结与面试避坑指南算法名称核心操作优点缺点适用区域标记清除标记清除垃圾简单、空间利用率高产生内存碎片老年代CMS收集器复制算法存活对象复制到新空间无碎片、速度快空间浪费严重需双倍空间新生代Eden/Survivor标记压缩标记压缩存活对象无碎片、空间利用率高移动对象耗时停顿时间长老年代分代算法结合各算法优点针对性强整体效率最佳需处理跨代引用卡表整堆主流的宏观策略分区算法分小块独立回收控制停顿时间精准实现难度高G1/Garbage-First 收集器