GC堆

GC的过程?
  • 大部分情况,对象都会首先在Eden区域分配,等到Eden区域满了会进行Minor GC,将存活的对象放入s0或者s1,同时对象的年龄会加1,当年龄增加到一定程度(大于15)的时候就会晋升到老年代;
  • 进行Minor GCEden区域的存活对象会被复制到 "to",“from”区域的对象也会复制到“to”区域、或者达到阈值晋升到老年代,同时Eden与“From”的区域被清空,“From”和“To”会交换他们的角色;
  • Minor GC会一直重复这样的过程,在这个过程中,可能导致“From”区域或者“To”区域的空间不够用,会提前把对象放到老年代中。


新生代晋升为老年代的情况?

  1. 分配担保:当Eden区满时,进行Minor GC,当Eden和Survivor区中存活的对象无法装入到另一个Survivor区中,会通过分配担保机制提前转移到老年代;
  2. 超大对象:新生代无法容纳这个对象,会直接到达老年代分配;
  3. 长期存活的对象:当通过Minor GC,对象年龄达到阈值(大于15)的时候,会晋升到老年代;
  4. 动态对象年龄判定:如果在Survivor区中相同年龄的对象的所有大小之和大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代。



为什么新生代到老年代阈值是15?

  • 对象年龄存储在Mark Word中,32bit的空间中,4bit用于存储对象分代年龄;也就是说对象分代年龄占4位,最大为1111  也就是15


为什么会有年轻代?

  • 优化GC性能,如果没有分代,对象都在一起就会对堆中的所有区域进行扫描;
  • 分代的话,回收对象就会降低gc时间,同时也会腾出很大的空间;
  • 进一步划分的目的是更好地回收内存,或者更快地分配内存。


为什么需要Survivor区?

  • 减少送到老年代的对象,通过不断的复制清空的过程,等到存活的对象达到16岁才会进入老年代;
  • 进而减少Old GC的发生由于老年代的内存空间远大于新生代,所以Old gc消耗的时间比Minor gc长得多。


为什么需要两个Survivor区?

  • 解决了碎片化
  • 刚刚建立的对象在Eden区中,一旦Eden满了会触发Minor gc,这时Eden区存活的对象就会被移动到Survivor区。等到下一次Eden满了,此时Eden区与Survivor区都存活一些对象,由于只有一个Survivor区需要把Eden存活的对象硬放到Survivor区,进而导致内存不连续,出现内存碎片化。

增加Eden区,Minor GC的间隔变长了,会不会导致Minor GC的时间增加?

  • 单次Minor GC包含两部分:扫描新生代时间  +  复制存活对象;
  • 增加Eden区,相当于增加了扫描新生代的时间,减少了复制存活对象的时间;而一般情况下,复制对象的成本要远高于扫描成本;
  • 如果存在较多长期存活的对象,增加Eden空间,反而会增加Minor GC的时间;
  • 如果存在较多短期存活的对象,增加Eden空间,Minor GC时间不会显著增加;
  • 总结:Minor GC的时间取决于 GC 后存活对象的数量,而不是Eden区的大小。

补充:不完全都在堆上分配空间,创建对象还有一些对象通过逃逸分析(如果方法中对象的引用被外面使用或者没有被返回则会在栈中开辟空间),对象会在栈中开辟空间

如图所示:




主流的垃圾回收器

总结:

  • 新生代:Serial收集器、ParNew收集器、Parallel Scavenge收集器;
  • 老年代:CMS收集器;
  • both:G1收集器;
  • 默认的垃圾回收器:Parallel Scavenge + Parallel Old。

Serial收集器:单线程收集器

  • 只有一条垃圾线程去完成垃圾收集工作,在进行垃圾收集工作的时候必须暂停其他所有工作的线程(stop the word),直到它收集结束。
  • 新生代采用标记-复制算法
  • 简单高效、因为没有线程的交互开销,适用于Client模式下。
  • 新生代收集器

ParNew收集器:多线程收集器

  • 其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等)和Serial收集器完全一样。
  • 新生代采用标记-复制算法
  • 适用于Server模式下。
  • 新生代收集器

Parallel Scavenge收集器:多线程收集器

  • 关注点在于吞吐量(高效的利用CPU),提供了很多参数供用户找到最合适的停顿时间或者最大吞吐量,使得Parallel Scavenge收集器配合自适应调节策略
  • 新生代采用标记-复制算法

CMS:并发标记清除

  • 是一种以获取最短回收停顿时间为目标的收集器。非常符合在注重于用户体验的应用上使用
  • 采用标记-清除算法实现
  • 老年代收集器
步骤:
  1. 初始标记:暂停所有其他的线程,并记录下直接与root相连的对象
  2. 并发标记:同时开启GC和用户线程,用一个闭包结构去记录可达对象。但是这个阶段结束,这个闭包结构并不能保证包含所有的可达对象。因为用户线程可能会不断的更新引用域,所以GC线程无法保证可达性分析的实时性。所以这个算法会跟踪记录这些发生引用更新的地方。
  3. 重新标记:为了修正并发标记期间因为用户线程继续运行而导致标记产生变动的那一部分对象的标记记录。
  4. 并发清除:开始用户线程,同时GC线程开始对未标记的区域做清扫。 

优点:并发收集、低停顿。
缺点:
  • 对CPU资源敏感
  • 无法处理浮动垃圾
  • 使用标记-清除算法会导致收集结束时会有大量的空间碎片产生。

G1收集器

是一款面向服务器的垃圾收集器,主要针对多颗处理器即大容量内存的机器。以极高概率满足GC停顿时间要求的同时还具备高吞吐量性能
特点:
  • 并发:充分利用CPU、多核环境下的硬件优势。部分收集器原本需要停顿java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行
  • 分代收集:G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。
  • 空间整合:与CMS的标记-清除不同,G1从整体来看是基于标记-整理算法实现的,从局部来看是基于标记-复制算法实现的
  • 可预测的停顿:除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内。
步骤:
  1. 初始标记
  2. 并发标记
  3. 最终标记
  4. 筛选回收

CMS与G1收集器的区别?

  • CMS收集器是老年代收集器,需要配合新生代Serial或者ParNew收集器使用。而G1收集器收集范围是老年代和新生代,所以不需要结合其他收集器使用。
  • CMS收集器以最小停顿时间为目标的收集器。而G1收集器可预测垃圾回收的停顿时间(建立可预测的停顿时间模型)。
  • CMS收集器采用标记-清除算法进行垃圾回收,容器产生内存碎片。而G1收集器采用标记-整理算法,进行空间整合,降低了内存空间碎片。
  • CMS收集器的回收过程:

  • G1收集器的回收过程:


垃圾收集算法

  • 垃圾收集算法:标记-清除算法、标记-复制算法、标记-整理算法、分代收集算法。
  • 总结来讲:
              分代收集算法:新生代(标记-复制算法)、老年代(标记-整理算法)

              标记-整理算法是对标记-清除算法的改进、避免了产生不连续的空间。