1. 如何判断对象是否是垃圾?

1.1 引用计数法

  • 如果一个对象被其他变量所引用,则让该对象的引用计数+1,如果该对象被引用2次则其引用计数为2,依次类推
  • 某个变量不再引用该对象,则让该对象的引用计数-1,当该对象的引用计数变为0时,则表示该对象没用被其他变量所引用,这时候该对象就可以被作为垃圾进行回收
  • 循环引用时,两个对象的引用计数都为1,导致两个对象都无法被释放回收。最终就会造成内存泄漏!

1.2 可达性分析

  1. 先要找到GC Root对象,与GC Root有关联的直接引用或者间接引用均不能被垃圾回收,反之则可以进行回收。
  2. 可以作为GC Roots的对象包括:

    1. 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用的参数、局部变量表、临时变量表等
    2. 在方法区中类静态属性引用的对象,譬如java类的引用类型静态变量
    3. 在方法区中常量引用的对象,譬如串池(String Table)中的引用
    4. 在本地方法栈中JNI引用的对象
    5. Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(如NullPointException、OutOfMemoryError)等,还有系统的类加载器
    6. 所有被同步锁(synchronized)持有的对象
    7. 反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调,本地代码缓存等

      1.3 再谈引用

  3. 强引用

    1. Object obj = new Object(); 这种引用关系,只要存在则垃圾收集器永远不会回收
  4. 软引用

    1. 当GC Roots指向软引用对象时,若内存不足,则会回收软引用所引用的对象
    2. 软引用的回收则需要引用队列进行回收,在软引用引用的对象被回收后,会将软引用加入到引用队列中,只需要判断引用队列中是否存在数据,然后将其出队。

      image.png

  5. 弱引用

    1. 在垃圾回收时,无论内存是否充足都会对弱引用所引用的对象进行回收,用Weakreference实现
  6. 虚引用
    1. 为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时受到一个系统的通知。
    2. 虚引用的一个体现是释放直接内存所分配的内存,当引用的对象ByteBuffer被垃圾回收以后,虚引用对象Cleaner就会被放入引用队列中,然后调用Cleaner的clean方法来释放直接内存

2 垃圾收集算法

2.1 标记-清除

image.png

  1. 定义:首先标记出需要回收的对象,然后统一回收掉所有被标记的对象
  2. 缺点:执行效率不稳定,如果java中包含大量对象,而其中大部分需要被回收的,这时必须进行大量标记和清除的动作,导致清除和标记两个过程的执行效率都随对象的数量增长而降低;其次是内存空间的碎片化问题,标记清除后会产生大量的不连续的内存碎片,空间碎片太多会导致以后程序中需要分配大对象时无法找到连续的内存空间而不得不提前触发另一次垃圾收集
  3. 这里的腾出内存空间并不是将内存空间的字节清 0,而是记录下这段内存的起始结束地址,下次分配内存的时候,会直接覆盖这段内存

2.2 标记-整理

image.png

  1. 老年代一般会采用这样的算法进行垃圾回收
  2. 将不被GC Roots引用的对象回收,清除占用的内存空间,然后整理剩余的对象,可以有效的避免因内存碎片而导致的问题,但是Stop The World ,停顿的时间比较长。

2.3 复制

image.png

  1. 年轻代一般采用这样的清除算法进行处理
  2. 把新生代分为一块较大的Eden区和两块较小的Survivor区(From/To),Eden:Survivor=8:1 ,每次分配内存时只使用Eden区域和其中的一块Survivor。发生垃圾收集时,将Eden和From区中仍然存活的对象全部复制到To区域,然后直接清理到Eden和From区域,并将From和To区互换位置
  3. 分配担保,当To区域不足以容纳一次MinorGC之后存活的对象,就需要依赖其他内存区域(老年代)进行分配担保

2.4 分代收集

  1. Minor GC :指目标只是新生代的垃圾收集
  2. Major GC :指目标只是老年代的垃圾收集 ,目前只有CMS存在单独收集老年代的行为
  3. Mixed GC :指目标是收集整个新生代以及部分老年代的垃圾收集,目前只要G1收集器由这样的行为
  4. Full GC :收集整个Java堆和方法区的垃圾收集

3 垃圾收集器

3.1 新生代收集器

3.1.1 Serial收集器

  1. 单线程,适用于内存较小的个人电脑(CPU核数较少)
  2. 开启:-XX:+UseSerialGC = Serial + SerialOld
  3. 采用复制算法

3.1.2 ParNew收集器

  1. 多线程,Serial收集器的并行版本
  2. 默认开启的收集线程数与处理器核心数量相同,可以使用 -XX:ParallelGCThreads 参数限制垃圾收集器的线程数
  3. 开启:-XX:+UseParNewGC

    3.1.3 Parallel Scavenge收集器

  4. 吞吐量优先收集器(运行用户代码时间/(运行用户代码时间+运行垃圾收集时间))

    1. XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间,收集器尽量将垃圾收集时间控制在这个范围内
    2. XX:GCRatio 直接设置吞吐量的大小,大于0小于100的整数,默认值为99,即允许最大1%(即1/(1+99))的垃圾收集时间
  5. 复制算法
  6. 自适应的调节策略:Parallel Scavenge收集器可设置-XX:+UseAdptiveSizePolicy参数。当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略


3.2 老年代收集器

3.2.1 Serial Old收集器

  1. 单线程收集器
  2. 标记整理算法

3.2.2 Parallel Old收集器

  1. 是Parallel Scavenge收集器的老年代版本
  2. 支持多线程并发收集
  3. 标记整理算法

3.2.3 CMS收集器

  1. 以获取最短回收停顿时间为目标的老年代收集器
  2. 基于标记清除算法实现
    1. 初始标记(STW):标记GC Roots能直接关联到的对象,速度很快
    2. 并发标记:从GC Roots直接关联对象开始遍历整个对象图的过程,耗时长,无需用户线程停顿
    3. 重新标记(STW):修正并发标记导致的有问题的标记,用时远比并发标记短
    4. 并发清除:清理掉标记已死的对象
  3. 并发收集,低停顿,但是会产生内存碎片

    3.3 G1 收集器

  4. Garbage First,JDK 9以后默认使用,而且替代了CMS 收集器

  5. 整体上是标记整理算法,两个区域之间是标记复制算法
  6. 堆被分成一块块大小相等的heap region ,一般有2000多块,每个Region的大小是固定相等的,大小可以通过-XX:G1HeapRegionSize进行设置,取值范围是1M到32M,且是2 的指数。
  7. G1的记忆集在存储结构上是一种哈希表,Key是别的Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号(卡表是“我指向谁”,这种结构还记录了“谁指向我”),G1中的YGC不需要扫描整个老年代,只需要扫描Rset就可以知道老年代引用了哪些新生代中的对象。
  8. G1的运行过程:
    1. 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
    2. 并发标记( Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,并发时有引用变动的对象会产生漏标问题,G1中会使用SATB(snapshot-at-the-beginning)算法来解决,后面会详细介绍。
    3. 最终标记(Final Marking):对用户线程做一个短暂的暂停,用于处理并发标记阶段仍遗留下来的最后那少量的SATB记录(漏标对象)。
    4. 筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多个收集器线程并行完成的。
  9. 三色标记 :
    1. 黑色:表示根对象,或者该对象与它引用的对象都已经被扫描过了
    2. 灰色:该对象本身已经被标记,但是它引用的对象还没有扫描完
    3. 白色:未被扫描的对象,如果扫描完所有对象之后,最终为白色的为不可达对象,也就是垃圾对象
    4. 漏标问题在CMS和G1收集器中有着不同的解决方案
      1. CMS:采用IncrementalUpdate(增量更新)算法,在并发标记阶段时如果一个白色对象被一个黑色对象引用时,会将黑色对象重新标记为灰色,让垃圾收集器在重新标记阶段重新扫描
      2. G1:采用SATB(snapshot-at-the-beginning),在初始标记时做一个快照,当B和C之间的引用消失时要把这个引用推到GC的堆栈,保证C还能被GC扫描到,在最终标记阶段扫描STAB记录
      3. SATB算法关注的是引用的删除
      4. Incremental Update算法关注的是引用的增加,需要重新扫描,效率低
  10. 记忆集与卡表 :
    1. 记忆集(RSet,Remembered Set):用来记录从其他Region中的对象到本Region的引用,是一种抽象的数据结构。每一个Region都设有一个RSet,有了这个数据结构,在回收某个Region的时候,就不必对整个堆内存的对象进行扫描了,它使得部分收集成为了可能
    2. RSet究竟是怎么辅助GC的呢?在做YGC的时候,只需要选定年轻代的RSet作为GC ROOTs,这些RSet记录了old->young的跨代引用,避免了扫描整个老年代。 而mixed gc的时候,老年代中记录了old->old的RSet,young->old的引用从Survivor区获取(老年代回收之前,会先对年轻代进行回收,存活的对象放在Survivor区),这样也不用扫描全部老年代,所以RSet的引入大大减少了GC的工作量