垃圾回收

Garbage Collection GC : 自动管理回收不在引用的内存数据
现在使用 GC 技术的语言有 Java , Perl , LM 等

为啥还需要了解GC

目前内存的动态分配和 GC 技术相当成熟,为啥还要了解呢

  1. 当需要排查各种内存泄露,内存溢出的问题时
  2. 当 GC 成为系统达到更高并发量的瓶颈时

这就需要对这些自动化技术实现必要的监听和调节

GC的意义

垃圾回收机制是 Java 中一个显著的特点。可以有效的防止内存泄漏。有效的使用内存空间

内存泄漏: 是指该内存空间使用完毕之后没有回收,在不涉及复杂数据结构的情况下, Java 的内存泄漏表现为一个对象的生命周期超出了程序需要他的时间长度。

GC算法要做的事情

Java 语言没有明确规定 JVM 使用的哪种 GC 算法,但是任何一种 GC 算法一般都需要确定三件事

  1. 哪些内存需要回收?即如何判断对象已经死亡
  2. 什么时候回收?即 GC 发生什么时候,需要了解 GC 策略,与垃圾回收器实现
  3. 如何回收?需要了解 GC 算法,以及算法实现–垃圾回收器

判断对象可回收算法

包括以下两种方法

引用计数算法

也就是Refrence Counting Collector,是GC 的早期算法。

基本思路

  1. 给对象添加一个引用计数器,
  2. 当有新变量引用这个对象的时候,该计数器加一
  3. 当引用失效或者超过声明周期时,计数器减一
  4. 任何时刻计数器为 0 ,则认为该对象不再被使用,可以被当做垃圾收集。

优缺点

  • 优点: 引用计数器可以很快的执行,交至在程序运行中。对程序需要不被长时间打断的环境有利。
  • 缺点:
    1. 无法检测出循环引用。如父对象有一个子对象引用,子对象又一个父对象引用,那么他们的计算器永远不为0
    2. 开销大,频繁并且大量的引用变化,带来大量的额外运算。

主流的 JVM 都没有选取计数算法管理内存

可达性分析算法

基本思路

从离散数学图论引入,把所有的引用看成一张图, 从一个节点的 GC Root 开始找,找到对应引用节点,再从引用节点开始找,继续搜索这个引用节点对应的节点,这样搜索下来,当所有的引用节点寻找完毕,剩余的节点就是被认为没有引用的节点,即无用的节点
搜索所走过的路径称为引用链(Reference chain)

Java 中可以作为 GC Root 的对象有

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

主要执行在上下文中和全局性的引用。

优缺点

  • 优点: 更加精确严谨,可以分析出循环数据结构互相引用情况
  • 缺点:
    1. 实现复杂
    2. 需要分析大量数据,消耗大量时间
    3. 分析过程需要 GC 停顿,因为应用关系不能发生改变,即停顿所有 Java 线程,称为 stop the world 是垃圾回收重点关注的问题

判断对象是生存还是死亡

宣告一个对象真正死亡,至少需要两次标记过程。

  1. 第一次标记
    在发现到 GC Root 没有任何引用链相连的时候,被第一次标记,并且进行第一次筛选,筛选条件就是: 此对象是否必要执行 finalize() 方法:
    • 有必要执行
      • 对象没有覆盖 finalize() 方法
      • finalize()方法已经被 JVM 调用了
        这两种情况都可以认为对象已经死亡,可以被回收
    • 没必要执行
      先放入到F-Queue队列中,稍后在 JVM 自动建立的,优先级低的 Finalizer 线程(可能是多个)中触发这个方法
  2. 第二次标记
    GC 将对F-Queue 队列中的对象进行第二次小规模标记。
    finalize() 方法是对象跳脱死亡的最后一次机会。
    • 如果对象在 finalize() 方法中重新了与引用链中任何一个对象建立关联,第二次标记会把该对象移除”即将回收”集合
    • 如果对象没有,也可以认为对象已死,可以回收。
      一个对象的 finalize() 方法只会被系统自动调用一次,经过 Finaliz() 方法逃脱死亡的对象,第二次不会再调用;

finalize()

上面说过 finalize() 方法与 GC 第二次标记相关,

  • finalize()方法是 Object 类的一个方法,是 Java 刚诞生时候,为了让C/C++程序员容易接受它做的一个妥协,但是不要把这个当成C/C++的析构函数,因为它执行时间不确定,甚至是否执行都不确定(Java 程序的不正常退出),而且运行代价高,无法确保各个对象的调用顺序 ,如果需要释放资源,可以定义显示的终止方法,并且在try-catch-finally的finally{}中保证及时执行,如 File 类的 close 方法
  • 一般情况下,应尽量避免使用它,甚至可以忘掉它

  • finalize()用途
    1. 充当 安全网
      当显示的终止方法没有调用时,在 finalize() 方法中发出警告,但要考虑值得付出这样的代价,如 FileOutStream , FileInStream , Timer 和 Connection 类都有这样应用
    2. 与对象的本地对等体有关。
      本地对等体: 普通对象调用本地方法委托的本地对象 本地对等体不会被 GC 回收,如果本地对等体不拥有关键资源, finalize() 可以回收它,如果拥有关键资源,必须显示终止

什么时候回收

  1. Allocation Failure:在堆内存中分配时,如果因为可用剩余空间不足导致对象内存分配失败,这时系统会触发一次 GC。
  2. System.gc():在应用层,Java 开发工程师可以主动调用此 API 来请求一次 GC。
    注意: 调用Syste.gc(),也只是建议系统进行 GC ,但是系统是否会采纳你的建议,就不一定了。

GC 算法

详情参考 扫盲系列 - GC 算法

GC执行机制

由于对象进行了分代处理,因此 GC 区域,时间不一样, GC 有两种类型, Scavenge GC 和Full GC

Scavenge GC

一般情况下,当新对象生成,并且在 eden 申请对象失败时,就会触发 Scavenge GC ,对 Eden 区进行 GC ,清除非存活对象,并且把尚存活对象复制到 Survivor0 区,然后整理两个 survivor 区,这种方式对年轻代的 eden 区域进行,不会影响到老年代。因为大部分对象都是从 eden 区开始的,同事 eden 区不会分配很大,所以 eden 区的 GC 频繁发生,因此这里需要速度快,效率高的算法,使 Eden 区域尽快空闲出来

Full GC

对整个堆进行整理,包括年轻代,老年代和持久代,因为需要对整个堆处理,所以比 Scavenge GC 慢,因此尽可能减少 Full GC 次数,在对 JVM 调优中,很大一部分工作就是对 Full GC 的调节,有如下原因导致full GC

  1. 年老代被写满
  2. 持久代被写满
  3. System.gc()被调用
  4. 上一次 GC 后 heap 的各域分配策略动态变化

Java 有 GC 依旧造成内存泄漏

  1. 静态集合类像 HashMap Vector 等使用最容易造成内存泄漏,这些静态变量的生命周期和应用程序一样,所有的对象也就不能被释放,因为他们也将一直被 HashMap , Vector 等引用着
  2. 各种连接,数据库连接,网络连接, IO 连接等没有显示调用 close 方法关闭,不被 GC 回收导致内存泄漏
  3. 监听器的使用,在释放对象的同时没有相应删除监听器的时候也导致内存泄漏。

垃圾收集器

GC 算法的具体实现,不同的商家,不同的版本的 JVM 所提供的垃圾收集器可能会有差别,这里主要说的是 Hospot VM 中的垃圾收集器 HotSpot VM 中的7中垃圾收集器包括: Serial , ParNew , Parallel Scavenage , Serial Old , Parallel Old , CMS ,G1

垃圾收集器组合

在JDK7/8后, Hotpost VM 所有垃圾收集器及组合(连线),如下图

解释:

  • 图中展示了七种不同分代垃圾收集器:Serial, ParNew , Parallel Scavenge , Parallel Old , Serial Old , CMS ,G1
  • 他们所在区域,表明他们是属于那个年代,
    1. Serial, ParNew , Parallel Scavenge 属于新生代收集器
    2. CMS, Serial Old , Parallel Old , 属于老年代收集器
    3. G1 属于整堆收集器
  • 两个收集器直接有连线,表明他们可以组合使用
    Serial/CMS ,Serial/Serial Old,ParNew/CMS ,ParNew/Serial Old,Parallel Scavenge/Serial Old,Parallel Scavenge/Parallel Old,CMS/Serial Old

  • 其中 Serial Old 作为 CMS 出现 Concurrent Mode Failure 失败后的预备方案

并发和并行收集的区别

  • 并发 Parallel 指多条垃圾收集器并行工作,但此时用户线程等待,例如 ParNew , Parallel Scavenge ,Parallel Old
  • 并发 Concurrent 指用户线程和垃圾收集器同时进行,但是不一定并行,可能交叉进行,用户进程在继续执行,垃圾收集器在运行在另外一个 CPU 上,如 CMS ,G1(也有并行)

Mnior GC 和Full GC

  • Mnior(次要) GC 又叫新生代 GC ,指发生在新生代的垃圾动作,因为大多数对象都是朝生夕死,所以 Mnior GC 发生频繁,一般回收速度较快
  • Full GC 又称 Major(主要) GC或老年代 GC ,只发生在老年代的 GC ,出现 Full GC 一般会伴随一个Mnior GC (但是不是绝对的, Parallel Scavenge 可以选择设置 Major GC 策略),速度比 Mnior GC 慢 10 倍以上

垃圾收集器介绍

没有最好的垃圾收集器,更没有万能的垃圾收集器
选择的只能是适合具体应用场景的收集器

Serial

最基本的,发展历史最悠久的收集器,在JDK 1.3.1 之前是 Hotpost VM 新生代的唯一收集器

特点

针对新生代,采用 Copy 算法,单线程收集 但是 进行垃圾收集时,必须暂停所有的工作线程,直到完成。即会 Stop the World


搬运地址:

深入理解 Java 垃圾回收机制—-

Java虚拟机垃圾回收(一) 基础:回收哪些内存/对象 引用计数算法 可达性分析算法 finalize() 方法 HotSpot 实现分析

Java虚拟机垃圾回收(二) 垃圾回收算法:标记-清除算法 复制算法 标记-整理算法 分代收集算法 火车算法