GC回收机制与分代回收策略

堆和方法区考虑回收的问题,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的就是这部分内存。

什么是垃圾

所谓垃圾就是内存中已经没有用的对象。 既然是”垃圾回收”,那就必须知道哪些对象是垃圾。Java 虚拟机中使用一种叫作”可达性分析”的算法来决定对象是否可以被回收。可达性分析通过一组名为”GC Root”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,最后通过判断对象的引用链是否可达来决定对象是否可以被回收。

GCRoot对象

在Java中,有以下几种对象可以作为GCRoot:

  1. Java虚拟机栈(局部变量表)中的引用的对象;
  2. 方法区中静态引用指向的对象;
  3. 仍处于存活状态中的线程对象;
  4. Native 方法中 JNI 引用的对象。

什么时候回收

不同的虚拟机实现有着不同的GC实现机制,但是一般情况下每一种GC实现都会在以下两种情况下触发垃圾回收。

  1. AllocationFailure:在堆内存中分配时,如果因为可用剩余空间不足导致对象内存分配失败,这时系统会触发一次GC。

  2. System.gc():在应用层,Java 开发工程师可以主动调用此 API 来请求一次 GC。

    JVM分代回收策略

    Java虚拟机根据对象存活的周期不同,把堆内存划分为几块,一般分为新生代、老年代,这就是JVM的内存分代策略。注意:在HotSpot中除了新生代和老年代,还有永久代

    分代回收的中心思想就是:对于新创建的对象会在新生代中分配内存,此区域的对象生命周期一般较短。如果经过多次回收仍然存活下来,则将它们转移到老年代中。

    年轻代(YoungGeneration)

    新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低,在新生代中,常规应用进行一次垃圾收集一般可以回收70%~95%的空间,回收效率很高。新生代中因为要进行一些复制操作,所以一般采用的 GC 回收算法是复制算法。

    新生代又可以继续细分为 3 部分:Eden、Survivor0(简称 S0)、Survivor1(简称S1)。这 3 部分按照 8:1:1 的比例来划分新生代。这 3 块区域的内存分配过程如下:

    1. 绝大多数刚刚被创建的对象会存放在 Eden区。当 Eden 区第一次满的时候,会进行垃圾回收。首先将 Eden 区的垃圾对象回收清除,并将存活的对象复制到 S0,此时 S1 是空的。

    2. 下一次 Eden 区满时,再执行一次垃圾回收。此次会将 Eden 和 S0 区中所有垃圾对象清除,并将存活对象复制到 S1,此时 S0 变为空。

    3. 如此反复在 S0 和 S1之间切换几次(默认 15 次)之后,如果还有存活对象。说明这些对象的生命周期较长,则将它们转移到老年代中。

      年老代(OldGeneration)

      一个对象如果在新生代存活了足够长的时间而没有被清理掉,则会被复制到老年代。老年代的内存大小一般比新生代大,能存放更多的对象。如果对象比较大(比如长字符串或者大数组),并且新生代的剩余空间不足,则这个大对象会直接被分配到老年代上。

      注意:对于老年代可能存在这么一种情况,老年代中的对象有时候会引用到新生代对象。这时如果要执行新生代GC,则可能需要查询整个老年代上可能存在引用新生代的情况,这显然是低效的。所以,老年代中维护了一个 512 byte 的 card table,所有老年代对象引用新生代对象的信息都记录在这里。每当新生代发生 GC 时,只需要检查这个 card table 即可,大大提高了性能。

      GCLog分析

      为了让上层应用开发人员更加方便的调试Java程序,JVM提供了相应的GC日志。在GC执行垃圾回收事件的过程中,会有各种相应的log被打印出来。其中新生代和老年代所打印的日志是有区别的。

      新生代GC:这一区域的GC叫作MinorGC。因为Java对象大多都具备朝生夕灭的特性,所以MinorGC非常频繁,一般回收速度也比较快。

      老年代 GC:发生在这一区域的 GC 也叫作 Major GC 或者 Full GC。当出现了 Major GC,经常会伴随至少一次的 Minor GC。

      注意:在有些虚拟机实现中,Major GC 和 Full GC 还是有一些区别的。Major GC 只是代表回收老年代的内存,而 Full GC 则代表回收整个堆中的内存,也就是新生代 + 老年代。

      Java 命令的参数

      引用

      判断对象是否存活我们是通过GCRoots的引用可达性来判断的。但是JVM中的引用关系并不止一种,而是有四种,根据引用强度的由强到弱,他们分别是:强引用(StrongReference)、软引用(SoftReference)、弱引用(Weak Reference)、虚引用(Phantom Reference)

      四种引用做简单对比

平时项目中,尤其是Android项目,因为有大量的图像(Bitmap)对象,使用软引用的场景较多。所以重点看下软引用SoftReference的使用,不当的使用软引用有时也会导致系统异常。

软引用隐藏问题

GC overhead“,之所以会抛出这个错误,是由于虚拟机一直在不断回收软引用,回收进行的速度过快,占用的cpu过大(超过98%),并且每次回收掉的内存过小(小于2%),导致最终抛出了这个错误。

优化方法,合适的处理方式是注册一个引用队列,每次循环之后将引用队列中出现的软引用对象从cache中移除。

参考链接:https://kaiwu.lagou.com/course/courseInfo.htm?courseId=67