GC

Minor GC触发条件:当Eden区满时,触发Minor GC。

Full GC触发条件:

(1)调用System.gc时,系统建议执行Full GC,但是不必然执行(虽然你可以调用 System.gc() 或者 Runtime.getRuntime().gc(),但是没有办法保证 GC 的执行。)

(2)老年代空间不足

(3)方法区空间不足

(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存

(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

Jvm怎么判断对象可以回收了

  1. 对象没有引用
  2. 作用域发生未捕获异常
  3. 程序在作用域正常执行完毕
  4. 程序执行了System.exit()
  5. 程序发生意外终止(被杀进程等)

gc算法

  1. 计数器算法
    1. 难以解决对象之间循环引用的问题
  2. 可达性分析算法
    在Java语言中,GC Roots包括:
    1. 虚拟机栈中引用的对象(本地变量表)
    2. 方法区中静态属性引用的对象
    3. 方法区中常量引用的对象
    4. 本地方法栈中引用的对象(Native对象)

可达性算法中不可到达的对象需要经历2次标记过程:

1、第一次标记并进行一次筛选。

筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize方法,或者finzlize方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”,对象被回收。

需要执行finalize方法的对象呗放到F-Queue中

2、第二次标记

在GC前执行F-Queue中的finalize方法,如果还是不可到达,则再次标记,之后会被收集。

方法区(Hotspot中的永久代)的回收条件非常苛刻,只有同时满足以下三个条件才会被回收!

  1. 所有实例被回收
  2. 加载该类的ClassLoader被回收
  3. Class对象无法通过任何途径访问(包括反射)

即使满足了上面3个条件,也不一定必然回收。HotSpot提供参数来控制对类的回收。

在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

垃圾收集算法

1、标记-清除算法

mage-20180321144534

标记-清除算法采用从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如上图所示。

标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片!

2、复制算法

mage-20180321144602

复制算法采用从根集合扫描,并将存活对象复制到一块新的,没有使用过的空间中,这种算法当控件存活的对象比较少时,极为高效,但是带来的成本是需要一块内存交换空间用于进行对象的移动。也就是我们前面提到的s0 s1等空间。

当回首时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间中,最后清理Eden和Survivor。HotSpot默认Eden和Survivor的大小比为8 : 1。当Survivor空间不足,需要老年代进行担保。

3、标记-整理算法

mage-20180321144618

整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。

我们知道,JVM为了优化内存的回收,进行了分代回收的方式,对于新生代内存的回收(minor GC)主要采用复制算法,对于老年代的回首采用“标记-清理”或“标记-整理”算法。下图展示了minor GC的执行过程。

安全点详解

垃圾收集器

mage-20180321150145

新生代 老年代
Serial Serial Old
Serial CMS
ParNew Serial Old
ParNew CMS
Parralell Scavenge Serial Old
Parralell Scavenge Parralell Old

Serial收集器

mage-20180321144904

串行收集器是一个单线程收集器,当JVM需要进行垃圾回收的时候,需要中断所有的用户线程,知道它回收结束为止,因此又号称“Stop The World” 的垃圾回收器。注意,JVM中文名称为java虚拟机,因此它就像一台虚拟的电脑一样在工作,而其中的每一个线程就被认为是JVM的一个处理器,因此大家看到图中的CPU0、CPU1实际为用户的线程,而不是真正机器的CPU,大家不要误解哦。

串行回收方式适合低端机器,是Client模式下的默认收集器,对CPU和内存的消耗不高,适合用户交互比较少,后台任务较多的系统。

Serial收集器默认新旧生代的回收器搭配为Serial+SerialOld

ParNew收集器

ParNew收集器其实就是多线程版本的Serial收集器,其运行示意图如下

mage-20180321145019

同样有Stop The World的问题,他是多CPU模式下的首选回收器(该回收器在单CPU的环境下回收效率远远低于Serial收集器,所以一定要注意场景哦),也是Server模式下的默认收集器。

Parallel Scavenge

ParallelScavenge又被称为是吞吐量优先的收集器,器运行示意图如下

mage-20180321145126

ParallelScavenge所提到的吞吐量=程序运行时间/(JVM执行回收的时间+程序运行时间),假设程序运行了100分钟,JVM的垃圾回收占用1分钟,那么吞吐量就是99%。在当今网络告诉发达的今天,良好的响应速度是提升用户体验的一个重要指标,多核并行云计算的发展要求程序尽可能的使用CPU和内存资源,尽快的计算出最终结果,因此在交互不多的云端,比较适合使用该回收器。

ParallelOld

ParallelOld是老生代并行收集器的一种,使用标记整理算法、是老生代吞吐量优先的一个收集器。这个收集器是JDK1.6之后刚引入的一款收集器,我们看之前那个图之间的关联关系可以看到,早期没有ParallelOld之前,吞吐量优先的收集器老生代只能使用串行回收收集器,大大的拖累了吞吐量优先的性能,自从JDK1.6之后,才能真正做到较高效率的吞吐量优先。其运行示意图如下

mage-20180321145239

SerialOld

SerialOld是旧生代Client模式下的默认收集器,单线程执行;在JDK1.6之前也是ParallelScvenge回收新生代模式下旧生代的默认收集器,同时也是并发收集器CMS回收失败后的备用收集器。其运行示意图如下

mage-20180321145308

CMS

CMS又称响应时间优先(最短回收停顿)的回收器,使用并发模式回收垃圾,使用标记-清除算法,CMS对CPU是非常敏感的,它的回收线程数=(CPU+3)/4,因此当CPU是2核的时候,回收线程将占用的CPU资源的50%,而当CPU核心数为4时仅占用25%。他的运行示意图如下

mage-20180321145322

CMS模式主要分为4个过程

  1. 初始标记:仅仅标记一下GC Roots能直接关联到的对象,速度很快,需要中断所有用户线程
  2. 并发标记:进行GC Roots Tracing的过程
  3. 重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分的标记记录
  4. 并发清除:清除操作

初始标记和重新标记仍然需要 stop the world。

缺点:

1、对CPU资源非常敏感。

2、在并发标记阶段,用户线程和标记线程并发执行,而在这个过程中,随着内存引用关系的变化,可能会发生原来标记的对象被释放,进而引发新的垃圾,因此可能会产生一系列的浮动垃圾,不能被回收。

CMS 为了确保能够扫描到所有的对象,避免在InitialMarking 中还有未标识到的对象,采用的方法为找到标记了的对象,并将这些对象放入Stack 中,扫描时寻找此对象依赖的对象,如果依赖的对象的地址在其之前,则将此对象进行标记,并同时放入Stack 中,如依赖的对象地址在其之后,则仅标记该对象。

在进行ConcurrentMarking 时minor GC 也可能会同时进行,这个时候很容易造成旧生代对象引用关系改变,CMS 为了应对这样的并发现象,提供了一个Mod UnionTable 来进行记录,在这个Mod Union Table中记录每次minor GC 后修改了的Card 的信息。这也是ParallelScavenge不能和CMS一起使用的原因。

CMS产生浮动垃圾的情况请见如下示意图

mage-20180321145549

在运行回收过后,c就变成了浮动垃圾。

由于CMS会产生浮动垃圾,当回收过后,浮动垃圾如果产生过多,同时因为使用标记-清除算法会产生碎片,可能会导致回收过后的连续空间仍然不能容纳新生代移动过来或者新创建的大资源,因此会导致CMS回收失败,进而触发另外一次FULL GC,而这时候则采用SerialOld进行二次回收。

同时CMS因为可能产生浮动垃圾,而CMS在执行回收的同时新生代也有可能在进行回收操作,为了保证旧生代能够存放新生代转移过来的数据,CMS在旧生代内存到达全部容量的68%就触发了CMS的回收!

3、大量空间碎片的产生

GarbageFirst(G1)

g1 gc

回收策略

1、优先在Edon上分配对象

mage-20180321150127

对于新生代和旧生代,JVM可使用很多种垃圾回收器进行垃圾回收,下图展示了不同生代不通垃圾回收器,其中两个回收器之间有连线表示这两个回收器可以同时使用。

mage-20180321150145

而这些垃圾回收器又分为串行回收方式、并行回收方式合并发回收方式执行,分别运用于不同的场景。如下图所示

mage-20180321150200

2、大对象直接进入老生代

3、年长者(长期存活对象)进入老生代

4、群体效应(大批中年对象进入老生代)

5、担保GC(担保minorGC)

担保GC就是担保minorGC能够满足当前的存储空间,而无需触发老生代的回收,由于大部分对象都是朝生夕死的,因此,在实际开发中这种很起效,但是也有可能会发生担保失败的情况,当担保失败的时候会触发FullGC,但是失败毕竟是少数,因此这种一般是很划算的

  1. Partial GC:并不收集整个GC堆的模式
    1. Young GC:只收集young gen的GC
    2. Old GC:只收集old gen的GC。只有CMS的concurrent collection是这个模式
    3. Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式
  2. Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。
  1. Full GC定义是相对明确的,就是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC;
  2. Minor GC和Major GC是俗称,在Hotspot JVM实现的Serial GC, Parallel GC, CMS, G1 GC中大致可以对应到某个Young GC和Old GC算法组合;
  3. 最重要是搞明白上述Hotspot JVM实现中几种GC算法组合到底包含了什么。
    1. Serial GC算法:Serial Young GC + Serial Old GC (敲黑板!敲黑板!敲黑板!实际上它是全局范围的Full GC);
    2. Parallel GC算法:Parallel Young GC + 非并行的PS MarkSweep GC / 并行的Parallel Old GC(敲黑板!敲黑板!敲黑板!这俩实际上也是全局范围的Full GC),选PS MarkSweep GC 还是 Parallel Old GC 由参数UseParallelOldGC来控制;
    3. CMS算法:ParNew(Young)GC + CMS(Old)GC (piggyback on ParNew的结果/老生代存活下来的object只做记录,不做compaction)+ Full GC for CMS算法(应对核心的CMS GC某些时候的不赶趟,开销很大);
    4. G1 GC:Young GC + mixed GC(新生代,再加上部分老生代)+ Full GC for G1 GC算法(应对G1 GC算法某些时候的不赶趟,开销很大);
  4. 搞清楚了上面这些组合,我们再来看看各类GC算法的触发条件。简单说,触发条件就是某GC算法对应区域满了,或是预测快满了。比如,
    1. 各种Young GC的触发原因都是eden区满了;
    2. Serial Old GC/PS MarkSweep GC/Parallel Old GC的触发则是在要执行Young GC时候预测其promote的object的总size超过老生代剩余size;
    3. CMS GC的initial marking的触发条件是老生代使用比率超过某值;
    4. G1 GC的initial marking的触发条件是Heap使用比率超过某值,跟4.3 heuristics 类似;
    5. Full GC for CMS算法和Full GC for G1 GC算法的触发原因很明显,就是4.3 和 4.4 的fancy算法不赶趟了,只能全局范围大搞一次GC了(相信我,这很慢!这很慢!这很慢!);
      5 题主说的 “Full GC会先触发一次Minor GC” - 指的应该是
    6. (说错了,我删了)
    7. PS MarkSweep GC/Parallel Old GC(Full GC)之前会跑一次Parallel Young GC;原因就是减轻Full GC 的负担。哇~整个picture 是有点乱,希望我整理的还算清楚:)

对象分配规则

  • 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
  • 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
  • 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,直到达到阀值对象进入老年区。
  • 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
  • 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。

如何通过参数来控制个各个内存区域
参考此文章:JVM(2):JVM内存结构