相关基础知识见:

深入理解 JVM -- 垃圾收集器与内存分配策略

 

新生代为什么需要两个 Survivor 区?

如果只有一个 Eden 区加一个 Survivor 区,那么 Minor GC 后 Eden 区还存活下来的对象复制到 Survivor 区。而 Survivor 区里的对象在这次 Minor GC 中,既有这次 GC 没活下来的,还有这次 GC 后活下来的,这时我们没有第二块 Survivor 区存放这块 Survivor 区上活下来的对象,那么就不能采取标记-复制,只能采取标记-清理算法,造成 Surviver 区内存碎片的产生。

如果有第二块 Survivor 区记为 S1,另一块记作 S0。那么一次 Minor GC 后,Eden 区和 S0 上存活下来的都复制到 S1 上,S0 和 Eden 清空,之后在 Eden 分配新对象;再下次 Minor GC,Eden 和 S1 上存活下来的就复制到空白的 S0 上,Eden 和 S1 清空,之后在 Eden 分配新对象........这样 S0 和 S1 轮换使用。

HotSpot虚拟机默认 Eden 和 Survivor 的大小比例是8∶1。

Minor GC 时会 stop the world 吗?

只要垃圾收集时会移动存活对象的做法都必须 Stop the world

现在的商用 Java 虚拟机大多都优先采用了 标记-复制 收集算法回收新生代,所以大多数收集器新生代(包括G1)都会完全 Stop the world

同理,多用于回收老年代的 标记-清除 算法(CMS)在清除阶段不需要 Stop the world,但会有内存碎片降低吞吐量;标记-整理 算法(Serial Old,Parallel Old)没有内存碎片,但在整理阶段需要 Stop the world

Minor GC 什么时候触发?

新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。

HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%,剩下 10% 是给第一次回收后复制仍然存活的对象的)。

大多数情况下,对象在新生代Eden区分配当Eden区没有足够空间进行分配时,虚拟机将发起 一次Minor GC。

什么是分配担保机制?

进行 Minor GC时有分配担保机制,GC 完之后剩的对象可能过多,一块 survivor 区放不下,这时就需要老年代来进行担保,担保失败可能 full GC

Full GC 什么时候触发?

整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

  1. 大对象直接在老年代分配-但老年代连续空间放不下时:HotSpot虚拟机提供了-XX:PretenureSizeThreshold 参数,指定 大于该设置值 的对象 直接在老年代 分配,这样做的目的就是避免在Eden 区及两个Survivor区之间来回复制,产生大量的内存复制操作。
  2. 代码调用 System.gc() 显式触发
  3. 与上图中 Minor GC 时分配担保机制相关的:
    • 参数不允许担保失败。直接 Full GC
    • 老年代最大可用连续空间 小于 历次晋升到老年代对象的平均大小。直接 Full GC
    • 进行 Minor GC 中途发现老年代空间不够,担保失败时。进行 Full GC

可以看出,老年代一般收集都是在 Full GC 中的,而触发了 Full GC 的条件,不论是大对象在老年代直接分配但放不下,还是担保失败。基本都是老年代不足了】才会进行收集。

控制Full GC频率的关键是老年代的相对稳定,这主要取决于应用中绝大多数对象能否符合“朝生夕灭”的原则,即大多数对象的生存时间不应当太长,尤其是不能有成批量的、长生存时间的大对象产生,这样才能保障老年代空间的稳定。

会单独回收老年代吗?Major GC

老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。

另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指, 读者需按上下文区分到底是指老年代的收集还是整堆收集。

标记-整理 的内存碎片有什么问题?

  • 没有内存碎片时,只要移动堆顶指针,按顺序分配。而内存碎片 会导致分配内存操作更加复杂,只能依赖更为复杂的内存分配器和内存访问器来解决。内存的访问是用户程序最频繁的操作,增加了内存访问的负担,降低了吞吐量;
  • 内存碎片严重时,即使剩余空间多,大对象也无法分配不得不提前触发一次Full GC。

吞吐量 和 垃圾收集停顿时间 不可兼得?

吞吐量 是处理器用于 运行用户代码的时间 与 处理器总消耗时间(用户代码时间+垃圾收集停顿时间) 的比值

垃圾收集停顿时间缩短 是以 牺牲吞吐量 和 新生代空间 为代价换取的:

  • 新生代空间大,那么一次收集耗时长,但是垃圾收集相对没那么频繁;
  • 新生代空间小,那么一次收集耗时更短,但是垃圾收集也更频繁,吞吐量降低。

另外老年代可以采用的清除算法在这两者上的影响也是矛盾的:

  • 标记-清除 算法 不需要 Stop the world,但会有内存碎片,导致吞吐量下降。
  • 标记-整理 算法 没有内存碎片,但需要 Stop the world,增大了时延。

不同的垃圾收集器在 吞吐量和停顿时间 这两个矛盾的目标之间有取舍:

关注吞吐量(标记-整理):

  • Parallel Scavenge:新生代收集器 标记-复制,多线程,提供了两个参数用于精确控制吞吐量:控制最大垃圾收集停顿时间的 MaxGCPauseMillis 间接影响吞吐量,以及直接设置吞吐量大小的GCTimeRatio。还有参数可以开启 自适应的调节策略可以自适应调节新生代大小 Eden 区比例等。
  • Parallel Old:Parallel Scavenge 的老年代版本,标记-整理,整个过程完全STW

关注停顿时间:

  • CMS:以获取最短回收停顿时间为目标,老年代收集器,标记-清除,因此清除阶段可与用户线程并发,但要预留老年代空间给浮动垃圾
  • G1用户可控制停顿时间。可以混合收集不同代,整体 标记-整理,局部 标记-复制。

G1 怎么实现用户可控制停顿时间?

JVM 内存越大越好吗?

回收大内存的Java堆,一次Full GC的停顿时间会非常长,回收12GB的Java堆,一次Full GC的停顿时间就高达14秒。

所以前提是必须把应用的Full GC 频率控制得足够低。控制Full GC频率的关键是老年代的相对稳定,这主要取决于应用中绝大多数对象能否符合“朝生夕灭”的原则,即大多数对象的生存时间不应当太长,尤其是不能有成批量的、长生存时间的大对象产生,这样才能保障老年代空间的稳定。