一、概述

   垃圾收集器简称GC,如今内存回收技术已经相当成熟,去了解垃圾收集回收的目的很明确,当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就必须对这些“自动化”的技术实施必要的监控和调节。

   JAVA运行时区域中的,程序计数器、虚拟机栈、本地方法栈,这几个区域随着方法结束或者线程结束时,内存自然而然就跟随着回收了,内存回收相对比较确定。而JAVA堆和方法区这两个区域则具有不确定性:一个接口的多个实现类的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有处于运行期间,我们才能知道程序创建了哪些对象,创建了多少个对象,这部分内存的分配和回收是动态的。垃圾收集器锁关注的正式这部分内存该如何管理。

二、对象是否还被使用

2.1引用计数算法

  在对象中添加一个计数器,每当有个地方引用地时候,计数器就+1,当引用失效地时候,计数器就-1。在任何时刻计数器为0地对象就是不可能再被使用地。   客观地说,引用计数器算法原理简单,判断效率也很高,但是在主流的JAVA虚拟机里面都没有采用引用计数器算法来管理虚拟机内存,因为这个看似简单的算法,必须要额外大量的处理才能保证正确的工作。譬如单纯的引用计数,就很难处理互相循环引用的问题。

2.2可达性分析

  这个算法通过一些列称为 "GC Root"的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走的路径被称为"引用链",如果某个对象到GC Root间没有任何引用链相连,即从GC Root到这个对象不可达,则证明此对象是不可能再被使用的。   在JAVA技术体系里面,固定可以作为GC Root的对象包括以下:

  • 在虚拟机(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  • 在方法区类静态属性引用(static修饰)的对象,譬如JAVA类的引用类型静态变量。
  • 在方法区中常量引用(final修饰)的对象,譬如字符床常量池里的引用。
  • 在本地方法栈中的JNI(即通常所说的Native方法)引用的对象。
  • JAVA虚拟机内部的引用,如基本数据类型对应的class对象,一些常驻的异常对象,类加载器等。
  • 所有被同步锁(synchronized关键字)持有的对象。
  • 反应JAVA虚拟机内部情况的JMXBean等。

  除了这些固定的GC Root集合外,根据用户选用的垃圾收集器,及其当前回收的内存区域不同,还有可能其他对象"临时性"地加入。这边"临时性"加入的原因,是因为某些对象即便是GC Root不可达,但是确确实实是有被引用的,而这些对象不能被GC,之所以会有这种情况,起始和选用的垃圾收集器有关,G1之前的垃圾收集器一般采用分代收集,G1后采用局部收集,即分区收集物理上不再有分代的概念,所以就必须考虑到一个问题,某个区域里面的对象完全有可能被堆中的其他区域对象所引用,这时候就需要将这些关联区域的对象也一并加入到GC Root集合里面去,才能保证可达性分析的正确性。

三、引用的类型

  在JDK1.2及之前引用是很传统的定义:如果reference类型的数据存储的数值代表的是另外一块内存的起始地址,就称该reference数据是代表某块内存、某个对象的引用。**这种要么被引用和未被引用的状态过于狭隘,很难符合某些应用场景,譬如,当内存充足的时候,这个对象可以被保留,但是如果内存在进行垃圾收集后仍然十分紧张,那就可以抛弃这些对象。**所以在JDK1.2之后,JAVA对引用进行了扩充,将引用的概念进行了扩充,引用被分为四种:强引用、软引用、弱引用、虚引用。四种引用强度逐级递减。

  • 强引用: 程序中的赋值引用,即类"Object obj = new Object()",在这种情况下,无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉这类引用对象。

  • 软引用(SoftReference): 软引用是用来描述一些还有用,但非必需的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常之前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。

  • 弱引用(WeakReference): 若引用也是用来描述那些非必须的对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

  • 虚引用(PhantomReference): 虚引用对对象而言是无感知的,对象有虚引用跟没有是完全一样的,使用虚引用的目的就是为了得知对象被GC的时机,所以可以利用虚引用来进行销毁前的一些操作,比如说资源释放等。虚引用不会影响对象的生命周期。

四、堆的回收

#### 4.1年轻代: #####   4.1.1 **Eden、Survivor**   **Eden**: 大多数情况下,对象再新生代Eden区中分配。当Eden区没有足够空间进行分配的时候,虚拟机将发起一次Minor GC。Eden区默认占年轻代80%的空间。

  Survivor:Survivor细分为S0和S1,二者没有本质上的区别,作用:当Eden区发生第一次轻GC(Minor GC)的时候会将Eden区还存活并符合条件的对象复制到S0区。当发生第二次轻GC的时候,会把Eden区和S0区还存活并符合条件的对象都复制到S1区。同理,当再次发生轻GC的时候,会把Eden区和S1区的复制到S0区,并以此类推下去。以上是一种比较简单和理想化的状态,真实场景中的GC更为复杂。

  4.1.2 Minor GC

  Minor GC又称为轻GC,只针对Eden区的垃圾收集,即Eden区间满了,或连续空间不足以分配新的对象时,会出发Minor GC,而Survivor区满不会触发Minot GC。

  4.1.3 对象存活的年龄

  虚拟机为了决策那些存活对象应当放在新生代,那些存活对象放在老年代中,虚拟机给每个对象定义了一个对象年龄计数器,存储在对象头里面。对象通常在Eden区诞生,如果经过一次轻GC后,仍然存活并且能被Survivor所容纳的话,对象便会被移动到Survivor空间中,并且将其对象年龄设定为1岁。对象在Survivor没熬过一次轻GC,年龄就增加1岁,当年龄达到一定程度(默认15岁)就会被移动到老年代中。年龄的阈值可以通过 -XX:MaxTenuringThreshold = xx 来设置。

  4.1.4 空间分配担保

  在Minor GC的时候,Survivor没有足够的空间存放上一次新生代收集下来的对存活对象,这些对象将会通过分配担保机制进入到老年代。   所以在发生Minor GC之前,虚拟机必须检查老年代最大可用连续空间是否大于新生代所有对象的总空间,如果条件成立,那么这次的Minor GC是安全的。如果不成立,虚拟机会先查看 -XX:HandlePromotionFailure参数是否允许担保失败;如果允许,那么会继续检查老年代最大连续可用空间是否大于历次晋升到老年代对象总和的平均大小,如果大于,则尝试进行一次Minor GC,尽管这次GC是有风险的(实际晋升到老年代的对象可能超出老年代最大连续可用空间);如果小于,或者-XX:HandlePromotionFailure不允许则这次GC改为Full GC, Full GC 清理的区域为年轻代,老年代,永久代。
  上文提到的风险指的是:新生代使用复制算法,但是为了内存的利用率,只使用了一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活,这时候会把Survivor无法容纳的对象放入老年代,因此出现一种极端情况,即Minor GC后,年轻代的所有对象都还存活,老年代要担保本身还有容纳这些对象的剩余空间。当最大连续可用空间小于年轻代对象,就需要取之前晋升老年代对象的平均值作为经验值,并且结合配置来决定是否需要Full GC。取历史平均值其实也是一种概率事件,当某次Minor GC后实际存活的对象远大于历史平均值超出老年代所能容纳范围,出现老年代担保分配失败,只能重新发起一次Full GC。但是,通常情况下我们还是会把-XX:HandlePromotionFailure配置打开,避免Full GC过于频繁。

4.2老年代:

  4.2.1 Magor GC/Full GC

  CMS收集器中,当老年代满时会触发 Major GC。目前,只有CMS收集器会有单独收集老年代的行为。其他收集器均无此行为,只能通过触发Full GC收集老年代(JDK8之前)。   Full GC 对收集整堆(新生代、老年代)和方法区的垃圾收集。一下几种情景会导致Full GC:

  • 老年代空间不足
  • 方法区空间不足
  • -XX:HandlePromotionFailure配置为不开启,即不开启空间分配担保配置,当在发生Minor GC之前,如果老年代最大可用连续空间小于新生代所有对象总空间,那么会触发Full GC;第二种情况是,如果开启空间分配担保,如果发老年代最大连续可用空间大于历次晋升到老年代对象的平均值,便会尝试冒险Minor GC,极端情况下,Minor GC后发现,存活对象远高于历史平均,并且老年代已无法容纳,这时候便会出现担保失败的情况,在这种情况下只能重新发起一次Full GC。最后一种情况是,如果开启空间分配担保,并且老年代最大连续可用空间小于历次晋升到老年代对象的平均值,那么就直接Full GC。

五、方法区的回收

 5.1 概论

  首先《JAVA虚拟机规范》中并不要求虚拟机在方法区实现垃圾回收,其次,方法区垃圾收集的性价比通常比较低:在JAVA堆中,尤其是在新生代中 ,对常规应用的一次垃圾回收能达到70%~99%的内存空间,相比之下,方法区回收囿于苛刻的判断条件,其区域的垃圾回收成果往往远低于此。

 5.2 方法区什么时候被回收?

  方法区回收,即永久代回收,只有发生Full GC的时候才会回收永久代,但是在回收永久代的时候,能被视为垃圾回收的对象却十分苛刻,具体条件见5.3。
  在JDK8之后起,就越来越淡化分代的思想,取而代之的是分区,因此如果说的是JDK8及其以上的垃圾回收,再用分代垃圾回收便不太合适了。

 5.3 方法区的垃圾回收主要分两个部分:

  • 废弃的常量: 通过可达性分析,某个常量(final修饰)不再被应用,这时候发生垃圾回收,而且垃圾收集器判断确实有必要回收,这时候该对象就会被当成垃圾处理掉。

  • 不再使用的类型: 被判定为"不再使用"的条件比较苛刻,需要同时满足以下3个条件:
    1.该类所有的实例都已经被回收。
    2.加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,否则很难达成。
    3.该类对应的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

  那么什么时候建议需要对永久代进行垃圾回收呢?
  在大量使用反射、动态代理、CGLIB等字节码框架,动态生成JSP的场景中,通常都需要JAVA虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。

PS:
  方法区的实现方式在JDK8之前是通过永久代来实现,JDK8之后是通过元空间。一般垃圾回收指的是堆里面的垃圾回收,堆里面又分为"年轻代"、"老年代",这里的"XX代"区域划分仅仅是一部分垃圾收集器的共同特性或者是设计风格,而非某个JAVA虚拟机具体是新的固有内存布局,更不是JAVA堆的进一步划分。
  在G1垃圾收集器出现之前的JVM虚拟机,内部的垃圾收集器全部基于经典的分代来设计,因此需要新生代、老年代收集器搭配才能工作。但是到了今天,尤其是JDK9中的G1垃圾收集器的出现,就已经取消了分代设计的思想,采用分区(Region)的方式来进行垃圾收集。