垃圾回收

垃圾回收

java 语言不同于 c++ 语言需要程序员手动回收内存,这都归功于 jvm 的垃圾回收机制,在此记录一下垃圾回收相关的知识。

如何识别垃圾

引用计数法

对象记录自己被引用的次数,当引用次数为0时即能被回收。无法解决循环引用的问题:a引用了b,b也引用了a,其实它俩都不会被其他对象用到,应该被回收,但是引用计数法不能解决此类问题,所以现代虚拟机都不用这种方法判断对象是否应该被回收。

可达性算法

从一系列GC Root节点开始,遍历其引用节点串出一条引用链,不在GC Root为起点的引用链中的对象会被判断为「垃圾」,会被GC回收。不可达的对象不一定会被回收,GC在回收不可达对象时会判断对象的finalize方法是否执行过,如果没执行过会先执行finalize方法,我们可以在此方法里将当前对象与GC Root关联,。执行之后,GC会重新判断当前对象是否可达,这样就能保证此次GC当前对象不会被回收。注意:对象的finalize方法只会被执行一次,即当一个对象再次不可达导致GC时会忽略finalize方法,直接被GC回收。

可做为GC Root 的对象:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中Native方法引用的对象

垃圾回收主要方法

标记清除算法

第一步先通过可达性算法标记对象是否可回收,第二步对标记了不可达的算法进行回收。缺点:会导致大量的内存碎片,导致空闲的内存没法被合理利用。

复制算法

将堆内存空间分为两部分,一部分分配对象,一部分不分配对象。通过可达性算法标记可达的(存活)对象,然后将存活的对象依次复制到另一部分空间中,最后再清理原来那部分。缺点:空间利用率低,最多只能用一半的内存空间。适合在存活率低,少复制的情况下使用,例如新生代的GC。

标记整理算法

前两步和标记清除法一样,不同之处是在标记清除的基础上又添加了一个整理的过程,即将所有存活的对象都向一端移动,紧临排列,最后再清理掉另一端的区域。缺点:每次GC都需要大量迁移存活的对象,效率太低。适合在存活率高的场景下使用,例如老年代的GC。

分代收集算法

分代收集算法整合了以上算法,综合了它们的优点,最大程度避免了它们的缺点。或者说这是一种策略,根据对象存活周期的不同将堆内存分为新生代老生代(Java8之前还有永生代),默认比例是1:2,新生代又分为Eden区、from Survivor区(简称S0)、to Survivor区(简称S1),三者的比例为8:1:1。再根据每个区特点的不同采用最合适的垃圾回收算法,我们把新生代发生的GC称为Young GC(也叫Minor GC),老年代发生的GC称为Old GC(也称为Full GC)。

对象在新生代的创建和回收

新创建的对象都在Eden区进行分配空间,当Eden区将满时,会触发Minor GC,会将Eden区和S0(或S1)中存活的对象复制到S1(或S0),并将对象的年龄加一,然后清除Eden区和S0(或S1)中剩余的废弃对象。

对象何时晋升到老年代

  1. 在进行Minor GC时,判断S0(或S1)中存活的对象年龄是否达到设置的阈值,将年龄达到阈值的对象复制到老年代。
  2. 大对象,当某个对象分配需要大量的连续内存时,此时对象的创建会直接分配在老年代。因为如果分配在新生代,后续进行Minor GC时复制迁移的开销太大。
  3. S0(或S1)区相同年龄的对象大小之和大于S0(或S1)空间一半以上时,则年龄大于等于该年龄的对象也会晋升到老年代。

空间分配担保

在发生 MinorGC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果大于,那么Minor GC 可以确保是安全的,如果不大于,那么虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则进行 Minor GC,否则可能进行一次 Full GC。

Stop The World(STW)

如果老年代满了,会触发Full GC,Full GC会同时回收新生代和老年代(即对整个堆进行GC),GC期间只有垃圾回收线程在工作,其他工作线程则被挂起。这个过程也被称为STW,造成挺大的性能开销。

垃圾收集器种类

垃圾收集器

新生代收集器

  • Serial:单线程的垃圾收集器,适合在Client模式下使用。
  • ParNew:ParNew收集器是Serial收集器的多线程版本,适合在Server模式。可以和CMS收集器配合工作。
  • Parallel Scavenge:是一个使用复制算法,多线程的收集器。目标是达到一个可控制吞吐量,适合做后台运算任务的情况下使用。它可以自适应调整新生代3个分区的大小比例,以达到最好的效果。

老年代收集器

  • Serial Old:与Serial对应的单线程老年代垃圾收集器,同样适合在Client模式下使用。
  • Parallel Old:是相对于Parallel Scavenge的老年代版本,使用多线程和标记整理法。
  • CMS:以实现最短STW时间为目标的收集器,适合注重服务相应速度的场景下使用,它和其他收集器不同的是采用的标记清除法,可能会导致大量内存碎片的产生。具体操作包含四个步骤:1、初始标记(STW),2、并发标记,3、重新标记(STW),4、并发清除。
  • G1:面向服务端的垃圾收集器,被称为驾驭一切的垃圾收集器。优点包括:1、像CMS一样,支持和工作线程并发执行,但不像CMS那样牺牲大量的吞吐性,2、整体采用标记整理法,局部(两个Region)上看是基于复制算法实现的,都不会产生内存碎片,3、不需要更大的Java Heap,4、在STW上支持建立可预测的停顿模型,能将停顿时间控制在用户设定的时间内。具体步骤和CMS类似。

总结

应该根据不同的场景选择垃圾收集器组合,如果是运行在桌面环境处于Client模式的,则适合用Serial + Serial Old组合。如果需要响应时间快,注重用户体验场景的适合用ParNew + CMS组合。G1的话需要根据吞吐量等要求适当调整相应的JVM参数。