需要回收的区域

JVM的运行时内存可以分为5个区域:程序计数器,虚拟机栈,本地方法栈,方法区,堆区。其中程序计数器,虚拟机栈,本地方法栈都是与线程同生共死的,因此不要进行垃圾回收。垃圾回收主要关注的堆区和方法区。

如何判断一个对象是否还存活

引用计数算法

引用计数是垃圾回收器中的早期策略。这种方法,堆中的每个实例都有一个引用计数。当一个对象被创建的时候,就会为给该实例分配一个计数器,这个计数器的初始值为1.当任何其它变量被赋值为这个对象的引用的时候,计数器加1,但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值的时候,对象实例的引用计数器减1.当计数器的值为0的时候,就可以认为其所对应的对象已经“死亡”了。垃圾回收器就可以对其进行回收了。

优缺点

优点:引用计数器可以很快的执行,交织再程序运行中。
缺点:无法检测出循环引用,循环引用计数器永不为0。

可达性分析

可达性分析算法是从离散数学图论中引入的,程序把所有的引用关系看作一个图,从一个GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即“死亡”的对象。

那么可以被作为GC Roots的对象包括那些呢?

  • 虚拟机栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中引用的对象

这个算法的思路就是沿着一系列的GC roots作为起点向下搜索,当一个对象不可达,那么就判定为可回收的。

当然被可达性分析判定为可回收,需要经历两次标记的过程。如果对象在可达性分析后发现不可达,那它会被第一标记并进行一次筛选,筛选的条件就是此对象是否有需要执行的finalize()方法,如果有,那么会调用一次(最多一次)finalize()方法,可以通过finalize()方法来拯救一次被回收的对象。

两次标记分别是:

  1. 通过GC Roots对象是否可达,如果对象不可达.
  2. 待回收的对象是否需要执行finalize()方法。在finalize()方法中没有于引用链建立关联关系的,将被进行第二次标记。

只有两次标记都成功的对象,才会被真正的回收。

Java中的引用

在Java引用中,将引用分为强引用、软引用、弱引用、虚引用4种。这四种引用的强度依次减弱。

  • 强引用:强引用是程序代码种普遍存在的,类似于Object obj=new Object()。只要强引用还存在,垃圾回收器就永远不会回收掉被引用的对象。
  • 软引用:用来描述一些有用但并非必须的对象。对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收之后还没有足够的内存,才会抛出内存溢出。
  • 弱引用:也是描述非必须对象的,但是它的强度比软引用要弱,被弱引用关联的对象只能生存到下一次垃圾回收器发生之前。当垃圾回收器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
  • 虚引用:虚引用是最弱的一种引用,一个对象是否存在虚引用,完全不会影响其生存时间,也无法通过虚引用来获取一个对象实例。它的作用是能在这个对象被回收器回收时收到一个系统通知。

无论是引用计数器算法还是可达性分析算法,都是针对于强引用的

什么是引用?
如果reference类型的数据中存储的是数值代表的是另一块内存的起始地址,那么称这块内存代表着一个引用。

方法区的垃圾回收

方法区回收的内容主要有两类:一类是废弃的常量,另一类是无用的类。对于废弃的常量可以通过可达性分析来判断是否可以回收。
但是无用的类需要同时满足以下几个条件:

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
  • 加载该类的ClassLoader(类加载器)已经被回收
  • 该类对应的java.lang.Class对象无法在任何地方被引用,无法在任何地方通过反射来访问该类的方法。

堆中的垃圾回收

Java堆中存放着几乎所有的对象实例,也是垃圾回收器工作的主要目标。

常用的垃圾回收算法、

引用计数器法

这个算法在之前已经介绍过来。这种算法实现简单,效率高可以于程序交织运行,但是不能解决循环依赖的问题。因此主流的JVM并没有采用引用计数器法来管理内存。

标记-清除算法(Mark-Sweep)

标记清除算法分为两个阶段:分别为标记阶段和清除阶段。
标记阶段:
采用可达性分析算法来判断对象的存活情况,并对需要清除的对象进行标记。
清除阶段:
清除阶段扫描整个堆内存,清除掉被标记的对象。

该算法实现简单,在存活对象较多的情况下效率非常的高,但是因为其直接回收对象不会对内存进行整理,因此会造成内存的碎片。

复制算法

复制算法将整个堆内存划分为大小相等的两个部分,每次只使用其中一块。当一块内存用完了,就将还存活的对象赋值到另一块内存中,然后再把已经使用的一半内存之情清理掉。这样就解决了标记-清除算法的产生碎片的问题。

这种算法实现简单,运行高效且很好的解决了内存碎片的问题。但是降低了内存的利用率,因为同一时间只能有效利用一半的内存。

标记-整理(Mark-compact)算法

该算法首先会对需要回收的算法进行一次标记,然后将存活的对象移向一段(整理),移动过后,清理掉边界以外的内存。(先标记再移动最后清除

标记整理算法也解决了标记清除算法的内存碎片问题,但是因为存在对象移动的开销,因此效率较低。

分代收集算法(Generational Collection)

分代收集算法是目前大部分JVM采用的垃圾收集器算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个永久代(Permanet Generation)。

老年代的特点是每次垃圾回收只有少量的对象需要回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收。然后根据不同代的特点来采用合适的收集算法。

对于新生代,目前主流的是采用复制算法,因为新生代中每次都会回收绝大多数的对象,因此需要复制的对象较少。但是不是按照1:1的比例还划分新生代空间的。一般来说将新生代划分为一块较大的Eden空间和两块较大的Survivor空间(8:1:1),每次使用Eden和其中一块Survivor,当进行回收时,将Eden和Survivor中还存活的对象复制到另一个Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。

所有新生成的对象首先存放在年轻代中。年轻代的目标就是尽可能块的收集掉那些生命周期短的对象。回收时,首先将eden区中存活的对象复制到一个survivor0区中,然后清空eden区,当survivor0区满了之后,就会将survivor0区和eden区中存活的对象复制到survivor1区,然后清空eden和这个survivor0区,然后将survivior0区和survivor1区交换。当survivor1区不足以存放eden和survivior0中存活的对象时,就会将存活对象存放到老年代。若是老年代也满了就会触发一次Full GC(Major GC),也就是新生代,老年代都进行回收。新生代中发生的GC也叫做Minor GC,Minor GC发生的频率比较高。

对于老年代,因为每次需要回收的内存的数量较少,因此一般采用标记整理算法。

常见的垃圾回收器

Serial收集器

新生代单线程收集器,标记和清理都是单线程,优点是简单高效。采用的是复制算法。是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定。

Serial Old

老年代单线程收集器,是Serial收集器的老年代版本。采用的是标记-整理算法。

parNew收集器

新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。

Parallel Scavenge收集器

并行收集器,追求高吞吐量,高效利用CPU。采用的是停止-复制算法。适合后台引用等对交互响应要求不高的场景。是Service级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThread=4来指定线程数。

Parallel Old收集器

Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先。

CMS(concurrnet mark sweep)收集器

高并发,低停顿,追求最短GC回收停顿时间,CPU占用比较高,停顿时间短,多和CPU追求高响应时间的选择。采用的是标记-清理算法。

GC是生命时候触发的

由于对对象进行了分代处理,因此垃圾回收的区域,时间也不一样。GC有两种类型: Scavenge GC 和 Full GC。

Scavenge GC

一般情况下,当新对象生成,并在eden申请空间失败是,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种GC仅仅是在年轻代的Eden区进行,不会影响到老年代。因为Eden区空间不大,且大部分对象都是从Eden区开始的,因此Eden区的GC非常的频繁。

Full GC

对整个堆进行整理。因为Full GC需要对整个堆进行整理。因此比Scavenge GC要慢,因此应该要尽可能减少Full GC的次数。导致Full GC的原因有以下几种:

  • 老年代写满
  • 持久代写满
  • system.gc()被显式调用
  • 上一次GC之后堆区各域的分配策略动态变化。