Tips: 没有单独注明的情况下, 本文主要讨论HotSpot虚拟机.

一般来讲, JVM 的 GC 是由JVM自己进行调控的, 但是当内存溢出频发、GC时机成为高并发的瓶颈时, 就需要对这个 "自行调控" 的环节进行必要的人为干涉.

主要回收 – 堆

  • Java 的内存管理分两方面:1. 针对对象内存的回收; 2. 对象内存的分配

  • 堆内存的每个区域可以选择不同的垃圾回收算法

  • 相关堆结构详情可见 📃Java内存区分析, 这里放一张堆内存的结构图方便本文阅读.

内存分配

优先在Eden区分配

对象首先会在Eden区进行分配, 当Eden区没有足够的空间分配时, 虚拟机将发起 Minor GC (新生代GC).

大对象直接进老年代

需要大量连续内存空间存储的大对象(字符串、数组等), 会被直接分配进老年代, 防止分配进新生代时, 因为新生代空间不够导致触发分配担保机制 产生的对象复制降低效率, 因为反正大对象大概率是无法被留下的, 不如直接存入老年代.

内存分配担保机制

Minor GC时,JVM会检测老年代的剩余空间是否大于新生代的总空间, 如果大于, 则直接发生MinorGC. 如果小于, 就会发生内存担保.

此时, JVM首先会看设置中是否允许担保失败:

  1. 允许: 会继续检查老年代最大连续可用空间是否大于历次晋升到老年代的对象的平均大小, 如果大于, 则会进行一次 Minor GC, 换句话就是说, 如果老年代里面有这么一端连续空间, 能够至少装下老年代对象的平均值大小, 老年代会尝试赌一把, 能不能承受的住这次 Minor GC.
  2. 不允许: 或者上面的 "赌博式" Minor GC失败了, 就会进行一次Full GC.

这里就有一个问题, 为什么要进行内存分配担保? 这是因为新生代采用复制收集算法, 假如大量对象在Minor GC后仍存在, 导致Survivor空间无法承载如此多的对象, 就需要把多出来的部分放到老年代, 但是老年代本身也是有限的, 不一定能容纳的下, 所以需要将 每次回收被存入老年代的对象大小老年代的剩余空间 进行对比, 来决定是否要进行Full GC. 简单说, 就是减少Full GC的次数.

长期存活的对象进入老年代

这里有一个概念需要先明确: 如果鉴别一个对象是否长期存活?

在设计中, 虚拟机给每个对象设定了一个年龄计数器. 当对象在Eden区被建立, 经过一次Minor GC后仍然存活, 且能在Survivor容纳, 会被移入到Survivor中(s0或者s1), 这之后, 对象的年龄变为1;

而后, 对象在Survivor中每经历过一次Minor GC而没有被回收或者被移除Survivor, 年龄+1, 当年龄增长到超过了设定好的阈值(XX:MaxTenuringThreshold <最大任期门槛, 意为超过这个限额就要 "退休" 去老年代了> HotSpot默认15), 就会被晋升到老年代中.

Tips: 除了 MaxTenuringThreshold 外, HosSpot还会在遍历所有对象时, 如果某个年龄的累计占用内存大小超过了 survivor 区的设定阈值时 (XX:TargetSurvivorRatio <目标幸存者比率, 意为超过这个比率的数据就不应该被留下> 默认 50%), 就会取这个年龄和 MaxTenuringThreshold 中的更小值, 作为新的 MaxTenuringThreshold.

Tips: CMS 的默认 MaxTenuringThreshold 是 6

回收规则