# 垃圾回收相关概念概述

# System.gc () 的理解

  • 在默认情况下,通过 System.gc () 或者 Runtime.getRuntime ().gc () 的调用,会显式触发 Full GC, 同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存
  • 然而 System.gc () 调用附带一个免责声明,无法保证对垃圾回收器的调用
  • JVM 实现者可以通过 System.gc () 调用来决定 JVM 的 GC 行为。而一般情况下,垃圾回收应该是自动进行的,无需手动触发,否则就太过于麻烦了。在一些特殊情况下,如我们正在编写一个性能基准,我们可以在运行之间调用 System.gc ()

# 内存溢出与内存泄漏

# 内存溢出 (OOM)

  • 内存溢出相对于内存泄漏来说,尽管更容易被理解,但同样的,内存溢出也是印方程序崩溃的罪魁祸首之一
  • 由于 GC 一直在发展,所有一般情况下,除非应用程序的占用的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,否则不太容易出现 OOM 的情况
  • 大多数情况下,GC 会进行各种年龄段的垃圾回收,实在不行了就来一次独占式的 Gull GC 操作,这时候会回收大量的内存,供应用程序继续使用
  • JavaDoc 中对 OutOfMemoryError 的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存

首先说没有空闲内存的情况:说明 Java 虚拟机的堆内存不够。原因有二 :

  1. Java 虚拟机的堆内存设置不够

    比如:可能存在内存泄漏问题;也很可能就是堆空间的大小不合理,比如我们要处理比较可观的数据量,但是没有显式指定 JVM 堆大小或者指定数值偏小。可以通过参数 - Xms, -Xmx 来调整

  2. 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集 (存在被引用)

    对于老版本的 Oracle JDK, 因为永久代的大小是有限的,并且 JVM 对永久代垃圾回收 (如常量池回收,卸载不再需要的类型) 非常不积极,所以当我们不断添加新类型的时候,永久代出现 OutOfMemoryError 也非常常见,尤其是在运行时存在大量动态类型生成的场合;类似 intern 字符串缓存占用太多空间,也会导致 OOM 的问题

# 内存泄漏 (Memroy Leak)

也称作 “存储渗漏”. 严格来说,只有对象不会再被使用了,但是 GC 又不能回收他们的情况,才叫内存泄漏

但实际情况很多时候一些不太好的实践 (或疏忽) 会导致对象的生命周期变得很长甚至导致 OOM, 也可以叫做宽泛意义上的 “内存泄漏”

尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现 OutOfMemory 异常,导致程序崩溃

注意,这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小

** 举例 : **

  1. 单例模式

    单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄露的产生

  2. 一些提供 close 的资源未关闭导致内存泄漏

    数据库连接 (dataSource.getConnection ()), 网络连接 (socket) 和 io 连接必须手动 close, 否则是不能被回收的

# Stop The World

  • Stop-The-World, 简称 STW, 指的是 GC 事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿被称为 STW
    • 可达性分析算法中枚举根节点 (GC Roots) 会导致所有 JAva 执行线程停顿
      • 分析工作必须在一个能确保一致性的快照中运行
      • 一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上
      • 如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证
  • 被 STW 终端的应用程序线程会在完成 GC 之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样,所以我们需要减少 STW 的发生

  • STW 事件和采用哪款 GC 无关,所有的 GC 都有这个事件
  • 哪怕是 G1 也不能完全避免 STW 情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间
  • STW 是 JVM 在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉
  • 开发中不要用 System.gc (); 会导致 STW 的发生

# 垃圾回收的并发与并行

  • 并行:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态
    • 如 ParNew, Parallel Scavenge, Parallel Old
  • 串行
    • 相较于并行的概念,单线程执行
    • 如果内存不够,则程序暂停,启动 JVM 垃圾回收器进行垃圾回收。回收完,再启动程序的线程
  • 并发:之用户线程与垃圾收集线程同时执行 (但并不一定是并行的,可能会交替执行), 垃圾回收线程在执行时不会停顿用户线程的执行
    • 用户程序在继续运行,而垃圾收集程序线程运行于另一个 CPU 上
    • 如: CMS, G1

# 安全点与安全区域

# 安全点 (Safepoint)

程序执行时并非在所有地方都能停顿下来开始 GC, 只有在特定位置才能停顿下来开始 GC, 这些位置称为 “安全点 (Safepoint)”

Safe Point 的选择很重要,如果太少可能导致 GC 等待的时间很长,如果太频繁可能导致运行时的性能问题。大部分指令的执行时间都非常短暂,通常会根据 “是否具有让程序长时间执行的特征” 为标准。比如:选择一些执行时间比较长的指令作为 Safe Point, 如方法调用,循环跳转和一场跳转等

如何在 GC 发生时,检查所有线程都跑到最近的安全点停顿下来呢

  • 抢先式中断: (目前没有虚拟机采用)

    首先中断所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点

  • 主动式中断:

    设置一个中断标志,各个线程运行到 Safe Point 的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起

# 安全区域 (Safe Region)

Safepoint 机制保证了程序执行时,在不太长的时间就会遇到可进入 GC 的 Safepoint. 但是如果当线程处于 Sleep 状态或 Blocked 状态,这时候线程无法响应 JVM 的中断请求,“走” 到安全点去中断挂起,JVM 也不太可能等待线程被唤醒。对于这种情况,就需要安全区域

安全区域是在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始 GC 都是安全的。我们也可以把 Safe Region 看作是被扩展了的 Safe Point

** 实际执行时: **

  1. 当线程运行到 Safe Region 的代码时,首先标识已经进入 Safe Region, 如果这段时间内发生 GC, JVM 会忽略标识为 Safe Region 状态的线程
  2. 当线程即将离开 Safe Region 时,会检查 JVM 是否已经完成 GC, 如果完成了,则继续运行,否则线程必须等待直到收到可以安全离开 Safe Region 的信号为止