相关基础知识见:
新生代为什么需要两个 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堆和方法区的垃圾收集。
- 大对象直接在老年代分配-但老年代连续空间放不下时:HotSpot虚拟机提供了-XX:PretenureSizeThreshold 参数,指定 大于该设置值 的对象 直接在老年代 分配,这样做的目的就是避免在Eden 区及两个Survivor区之间来回复制,产生大量的内存复制操作。
- 代码调用 System.gc() 显式触发
- 与上图中 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频率的关键是老年代的相对稳定,这主要取决于应用中绝大多数对象能否符合“朝生夕灭”的原则,即大多数对象的生存时间不应当太长,尤其是不能有成批量的、长生存时间的大对象产生,这样才能保障老年代空间的稳定。