java垃圾回收全了解

简介

说起垃圾回收,很多人会自动和java联系上,认为是java的伴生产物,事实上,GC的历史远比java久远。很久以前,人们就开始思考GC需要完成的3件事情

  1. 哪些内存需要回收
  2. 什么时候回收
  3. 如何回收
    当然这也是java一直思考和完善的三个问题。我们知道,java内存运行时区域主要有这几个部分:pc寄存器、虚拟机栈、本地方法栈、堆、方法区。其中,pc寄存器、虚拟机栈、本地方法栈随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出有条不紊的执行入阵和出栈操作,每个栈帧分配多少内存基本是类结构确定下来时就已知的,因此这些区域的内存分配和回收都具备确定性,这些区域不需要考虑回收的问题。而堆和方法区则不一样,每个类占用的内存空间不一样,我们只有在程序运行时才会知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾回收器关注的就是这部分内存。

对象真的死了么

判断对象是否死去的依据是这个对象不可能再被任何途径使用

引用计数法

很多教科书都会这么讲:给对象添加一个引用计数器,每当一个地方引用他时,计数器值加一,当引用失效时,计数器减一,任何时刻当计数器为0时的对象就是不可用的。表现上看,引用计数法实现简单,判定效率也高,但是目前来看,主流java虚拟机没有使用引用计数法来管理内存,最主要原因就是其难以处理对象之间相互循环引用的问题。
对象之间互相循环引用指:比如两个对象A和B,A中有B的引用,B中有A的引用,除此之外,两个对象再无别的引用,那么,实际上A和B都不能再被使用,但因为两者互相引用,计数值始终为1,GC收集器也就永远无法回收他们。实际上,我们可以在写程序验证,详见深入理解java虚拟机P63,可以发现,主流的jvm没有使用该方法。

可达性分析算法

目前主流商用程序语言(java,C#)都是用的可达性分析来判断对象是否存活的。可达性分析是指通过一系列成为GC Roots的对象作为起点,从这些起点向下搜索,搜索走过的路径叫引用链,当一个对象到GC Roots没有任何引用链
时,则该对象是不可用的。GC Roots有这几种:

  1. 虚拟机栈中引用的对象
  2. 本地方法栈中引用的对象
  3. 方法区中类静态属性引用的对象
  4. 方法区中常量池引用的对象
    引用的类型
    上面的分析中,我们一直在说引用,不管是引用计数还是可达性分析,引用都是判断对象存活最重要的判断依据,如果单纯的使用一种引用类型,那么,一个对象只有被引用和没有被引用两种状态,这对于某些情况可能不太适合,比如我们想要这样的引用类型:虽然没有很强的引用,但是我们还是希望,在还没有达到内存不足的情况时,尽可能的保留它。因此,java对引用的概念进行了扩充,将引用氛围强引用、软引用、弱引用、虚引用。
  5. 强引用,就是我们最经常使用的,只要有强引用存在,对象就不会被回收
  6. 软引用,当对象没有强引用但是有软引用时,还是会尽可能的保留它,直到内存不足,将要发生内存溢出时才会回收。
  7. 弱引用,当对象没有强引用但是有弱引用时,对象只能生存到下次垃圾收集之前,当垃圾收集器工作时,无论当前内存是否足够,都会回收
  8. 虚引用,虚引用不会对对象的生存时间构成影响,为一个对象设置虚引用的唯一目的就是能在这个对象被回收时收到一个系统通知
    finalize
    可达性分析中不可达的对象也并不是非死不可的,至少还要经历两次标记过程:
  9. 第一次是对象到GC-Roots没有引用链时,会第一次打标并进行筛选,筛选出来所有实现了finalize方法的对象
  10. 所有实现了finalize方法的对象分两种,一种是已经执行过了finalize方法,这种会被视为没有必要再执行finalize,会被打标,另一种是需要执行finalize
  11. 需要执行finalize方法的对象会放置在F-Queue队列中,并由一个优先级较低的线程执行这个方法,这里的执行指虚拟机会触发这个方法,但不保证会等待他运行结束,因为如果某个对象的finalize导致了死循环,可能会导致其他对象永久等待,这样内存回收系统会崩溃
    根据上面的介绍,finalize是对象拯救自己的最后机会,在这里把对象跟某引用链接即可保证不被回收,但是记住,只能拯救一次,因为第二次系统发现finalize已经执行过,就不会再执行了。

    垃圾收集算法

    几种常见垃圾收集算法
    标记-清除算法
  12. 标记,标记出所有要回收的对象
  13. 清除对象所占内存空间
    缺陷,效率低,内存碎片,内存碎片多,导致为大内存对象分配内存时得多次触发垃圾回收
    复制算法

    为了解决效率问题,出现了一种复制算法,将可用内存划分为大小相等的两块,每次只使用其中的一块,当这一块用完了,就将还存活着的对象复制到另一块内存上,然后把这边的内存全部清理掉。这样避免的内存碎片问题,代价是可用内存减少一半。
    现在的商业虚拟机都是这种算法来回收新生代,IBM研究表明,98%的对象朝生夕死,所以不用1:1分配,将内存分为一块Eden区和两块survivor区,Eden区与survivor区大小比为8:1,,每次回收时,将Eden区和survivor区中还存活的对象全都复制到另一个survivor区,最后清理掉Eden区和用过的survivor区。
    标记-整理算法

    与标记清除算法类似,但是清理过程有区别
  14. 标记,标记出所有要回收的对象
  15. 把所有活着的对象向一端移动,然后清理掉剩下的内存,适合老年代的收集
    分代收集
    分为新生代和老年代,针对对象存活周期的特点,采用不同的垃圾收集方式,提供效率

    HotSpot算法实现

    枚举根节点:可达性分析必须在一个能保证一致性的快照中进行,即在一个所有执行线程都停顿的时间点进行,这就是Stop the world。实际中,并不需要检查完所有执行上下文和全局的引用位置,使用OopMap达到这个目的。
    安全点:OopMap可以快速的完成GC Roots枚举,但是有个问题随之而来,OopMap内容变化的指令很多,为每一个指令生成oopmap需要大量空间。gc成本也变高。
    实际上hotspot也没有为每个指令生成oopmap,而是设定了安全点,只有到达安全点才能开始GC,只有达到安全点才能暂停。
    那么怎么保证所有线程都能做到跑到最近的安全点并停顿呢?两种方案:1. 抢先式中断,GC发生时,中断所有线程,如果发现有线程不在安全点,就恢复线程,跑到安全点再停顿;2. 主动式中断,设置一个标志,线程跑到安全点时,轮询这个标志,发现为真就中断,轮询标志的点和安全点重合

安全区域:线程处于sleep或者block状态时无法响应中断,但是这样的状态并不影响枚举根节点,这种就是安全区域,可以认为安全区域是安全点的扩展,当线程进入安全区域,标识自己进入安全区域,当GC发生时就不会有影响,当要离开安全区域时,先检查系统根节点枚举是否完成,如果没有完成,要等到收到可以离开安全区域的信号为止。

垃圾收集器

如果说手机算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

Serial与Sserial old收集器

单线程收集器,仅使用一个cpu或一个收集线程,进行收集工作时,必须暂停其他所有线程,直到收集结束。

  • 新生代使用复制算法(Serial)
  • 老年代使用标记整理算法(Serial old)
    ParNew收集器
  • Serial收集器的多线程版本,适用于新生代,使用复制算法
  • 与Serial old配合
  • 与CMS收集器配合
    Parallel Scavenge与Parallel old收集器
  • Parallel Scavenge是并行多线程新生代收集器,使用复制算法
  • Parallel old是并行多线程老年代收集器,使用标记整理算法
  • 主要用于控制吞吐量:吞吐量= 运行用户代码时间/(运行用户代码时间 + 垃圾收集时间)
    CMS收集器
  • 以获取最短回收停顿时间为目标
  • 并发
  • 基于标记清除算法
    G1收集器

内存分配与回收

新生代GC叫Minor GC,老年代GC叫Major GC

  • 对象优先在Eden区分配
  • 大对象直接进入老年代
  • 长期存活的对象将进入老年代:新生代每次GC年龄加一,年龄大于阈值(默认15)就进入老年代
  • 动态对象年龄判断:如果Survivor空间相同年龄对象占用空间大于survivor空间一半,大于等于该年龄的对象直接进入老年代
  • 空间分配担保:发生minor GC之前,虚拟机先检查老年代最大连续空间是否大于新生代所有对象总空间,如果大于,则认为这次minor GC是安全的,如果不是,则检查设置HandlePromotionFailure是否允许担保失败,如果允许,则检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,则进行一次minor GC,虽然是有风险的,如果小于,或者设置HandlePromotionFailure不允许冒险,则进行一次Major GC。