黑马 JVM ---2 --- 垃圾回收

								垃圾回收
  1. 如何判断对象可以 回收
  2. 垃圾回收算法
  3. 分代垃圾回收
  4. 垃圾回收器
  5. 垃圾回收调优

                              1. 如何判断对象可以 回收

1.1引用计数法

1.1.1 定义
给对象添加 一个 引用计数器 ,每当有 一个地方 引用它的时候 ,计数器就会加1 ;
当 引用 失效, 计数器 就会 减 1;
任何时候 计数器 为 0 的 对象 就是 不可能 再被 使用的。

存在弊端 : 循环引用 造成内存泄露
在这里插入图片描述

1.2 可达性 分析 算法
(java 虚拟机 所使用)

判断 当前对象 是否 直接引用 或者 间接的 被根对象 引用, 如果没有, 可 进行 垃圾回收

这个算法 的 思想 就是 通过 一系列 称为 “GC Roots” 的 对象 作为 起点 ,
从 这些 节点 开始 向下 搜索 , 节点所走过的 路 称为 引用链 ,
当 一个 对象 到 GC Root 没有 任何 引用链 相连 的 话 ,则 证明此 对象 是 不可用的。

  • java 虚拟机 中的 垃圾回收器 采用 可达性分析 来 探索 所有存活的 对象

  • 扫描 堆中的 对象, 看 是否能够 沿着 GC Root 对象 为 起点 的 引用链 找到 该 对象,找不到,
    表示可以 回收

  • 哪些 对象 可以 作为 GC Root ? (MAT工具)

    • 1.对象静态变量
    • 2.常量
    • 3.栈本地变量表中的对象有引用

    【具体:System Class , Native Stack ,Thread ,Busy, Monitor 】

在这里插入图片描述

8


		                    1.3 四种引用
  1. 强引用
  • 强引用就是指在程序代码之中普遍存在的 如 object对象

  • 只有 所有 GC Roots 对象 都不通过 【强引用】 引用该对象 ,该 对象 才能 被 垃圾回收

  1. 软引用 (SoftReference)
  • 仅有 软引用 引用 对象时, 在 垃圾回收 后 ,内存 仍 不足 时 会 再次 出发 垃圾回收 , 回收 软引用对象
  • 可以配合 引用队列 来 释放 软引用 自身
  1. 弱引用 (WeakReference)
  • 仅有 弱引用 引用 对象时, 在 垃圾回收 时, 无论 内存 是否 充足, 都会 回收 弱引用对象
  • 可以配合引用队列 来 释放 弱引用自身
  1. 虚引用 (PhantomReference)
  • 必须配合 引用队列 使用 , 主要 配合 ByteBuffer 使用 , 被 引用对象 回收 时 , 会 将 虚引用入队, 由 Reference Handler 线程 调用虚引用相关方法释放直接内存
  1. 终结器引用 (
  • 无需手动编码 , 但 其 内部配合 引用队列 使用 , 在 垃圾回收时 ,
    终结器 引用入队 (被 引用对象 暂时没有 被 回收 ,再由 Finalizer 线程 通过 终结器 引用 找到 被引用对象 并 调用 它的 finalize方法 , 第二次 GC 时 才能 回收 被引用对象)

在这里插入图片描述


在这里插入图片描述
在这里插入图片描述


							2.垃圾回收算法

2.1 标记清除 (Mark Sweep)

第一阶段(标记) 把没有被引用的对象 标记起来
第二阶段 (清除)垃圾对象所占用的空间释放 , 对象所占用的空间的起始结束的地址 记录下来 放在 一个空闲的地址列表中 , 下次分配 其他对象的时候 得到 这个 空闲地址列表中 去找有没有一块 足够的 空间 去 容纳新对象,如果有 就 进行 内存分配 ,而不会对 占用的 空间 进行 清零的处理。

优点: 效率高,只需要 记录 垃圾 对象的 起始结束 地址 ,不需要其他额外处理

缺点: 容易产生 空间垃圾碎片 ,因为 不会 对 内存 进行整理 ,导致 内存空间 不连续。


2.2 标记整理 (Mark Compact)

第一阶段(标记) 把没有被引用的对象 标记起来
第二阶段 (整理) 会在 垃圾清除的 过程中, 把可用的 对象 向前 移动 到 一边 ,会使得 内存 更为 紧凑 。

优点: 不会产生 内存碎片。
缺点 : 由于涉及到了 对象的移动, 导致了 效率较低, 对象地址 这些 都可能 被改变了 。

在这里插入图片描述


2.3 复制 (COPY)

(第一阶段)会把 内存区 划分成 大小相等的 两块区域 , 一块 称为 FROM ,另一位 称为 TO 。
其中 TO 区域 始终 空闲着 ,没有存储对象 。

第二阶段(标记) 把被引用的对象(存活对象) 标记起来 , 然后 把 FROM 区域存活 的 对象 复制 到 TO 区域 中 ,复制的过程中 会 完成 对 空间碎片的整理 。

(第三阶段) 清除, 这时候 FROM 区域全是 垃圾对象 ,会 一起 清空。 并且 交换 2 个区域 ,
即 FROM 区域 变为 TO 区域, 这时候 FROM 区域 存储 的 就是 存活对象 ,TO 区域 此时又变为 空闲的 区域。

优点 : 不会有 空闲碎片

缺点 : 需要 占用 两倍 的 内存 空间

在这里插入图片描述


							   3 分代回收

在这里插入图片描述
3.1 分代垃圾回收 定义
大的内存区域 划分成 两大区域 ,一个是 新生代 区域 , 一个 是 老年代 区域。

而 新生代 又 划分 成 三大区域 , 一个是 伊甸园 , 幸存区 From , 幸存区 To 。

原因: 因为 java 对象 有些 需要 长期 引用 ,而长期 引用的 对象 存放在 老年代 区域 中 。

而 对应 用完 就可以 丢弃的 对象 就可以 存放在 新生代 去 区域 中 , 这样的 话 就可以 针对
对象 的 生命周期出 不同 特定 进行 不同的 垃圾 回收 策略 。

老年代的垃圾回收 很少 ,而 新生代 垃圾回收 很频繁。

3.2 分代 垃圾 回收 工作

新生代垃圾回收 Mirror GC 老年代垃圾回收 Full GC

  1. 当 创建 一个 新的 对象 的 时候 ,会默认 采用 伊甸园的 一块内存空间。
  2. 当 伊甸园 内存不足的 时候 ,就会 触发 一次 垃圾回收( Mirror GC)
  3. 此时 就会 利用 可达性分析算法 沿着 GC Root 引用链 寻找 ,找出 哪些对象属于 有用对象 还是 垃圾对象 ,然后 进行 一次 标记 工作 ,标记 存活对象 成功 后 , 就会采用 复制算法 。
  4. 把存活 对象 复制到 TO 区域中 , 还会 把 幸存对象 的 寿命 +1 ,初始寿命为 0 ,
  5. 此时 伊甸园的 对象 就会 被 垃圾回收 , 然后 交换 TO区域 和 FROM 区域 的 位置
  6. 然后 此时 伊甸园的 内存 空闲了 , 可以再次 分配对象 到 伊甸园了
  7. 当 伊甸园的 内存 又 不足的 时候 , 触发 第二次 垃圾回收 ( Mirror GC) , 然后 把 根据 可达性分析算法继续 区分对象 ,然后 把 再次 标记 存活对象 和 使用 复制 算法, 把 伊甸园的存活对象 和 FROM 区域的对象 复制到 TO 区域 ,然后 把 伊甸园 和 FROM 的 垃圾对象 清除。
  8. 如果 对象 在 第二次 垃圾回收的 时候 存在 于 FROM 区域 并且 属于 有用对象 则 寿命再次 加1 为2
  9. 如果是 伊甸园 区域 的话 还是 把 寿命 0 加为 1。
  10. 然后在把 TO 区域 和 FROM 区域的 位置 交换。
  11. 此时 FROM 区域 就 存在 了 寿命为 1 和 2 的 存活对象, 伊甸园 内存 位置 又空闲了
  12. 以此类推
  13. 如果 FROM 区域的 对象 年龄 超过 设置值(默认15), 就会 被 晋升到 老年代中
  14. 当 老年代 区域 内存不足的时候 ,会尝试 mirror gc , 如果 之后 空间内存还是不足,就会触发 Full GG。
  15. 老年代回收 就会 从 新生代 到 老年代 做一次 整个 清理。
  16. 每一次 垃圾回收 都会 触发 一次 stop the world ,暂停 其他用户的线程,等垃圾回收结束后才会恢复 用户线程 ,mirror gc 时间较短 ,而 full gc 时间更长。

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

小总结:

在这里插入图片描述
在新生代内存不够 ,但老年代内存足够的情况下,对于 大对象 直接 晋升 到 老年代

对于 大对象 的 内存 无法 存入 的情况下, 会 触发 一次 mirror gc 和 full gc ,如果还是内存不足 抛出 oom 异常
在这里插入图片描述
在这里插入图片描述

子进程内存溢出 抛出的异常 跟 其他所有异常 其实一样, 所有 不会让主线程停止 运行,
而 主线程 的 堆状态 跟 子线程 抛出异常 时 相同的 ,因为 他们 共享 堆内存,看程序
运行结束 的 堆状态 信息


3.3 (虚拟机)相关VM参数 (与GC相关)

在这里插入图片描述


								4. 垃圾回收器

分成 以下 三类的 垃圾回收器

  1. 串行
  • 单线程
  • 堆内存较小 , 适合个人电脑
  1. 吞吐量优先
  • 多线程
  • 堆内存 较大 , 多核 CPU , 适合 服务器端
  • 单位时间内 使得 STW (Stop the world) 的 时间 最短
    (0.2 0.2 =0.4)
  1. 响应时间优先
  • 多线程
  • 堆内存 较大 , 多核 CPU , 适合 服务器端
  • 尽可能 使得 单次 STW (Stop the world) 时间 最短
    (0.1 0.1 0.1 0.1 0.1 =0.5)

4.1 串行 回收器

Serial 回收器 发生在 新生代 中 , 使用 复制算法 – mirror gc
SerialOld 回收器 发生在 老年代 , 使用 标记整理 算法 – full gc

在这里插入图片描述
需要让 这些 线程 在一个 安全点 停下来, 原因 : 在垃圾回收过程中 可能对象的地址 会发生 改变
为了 保证 安全的 使用 这些 内存地址 , 需要 所有的 用户 正在 工作的 线程 都 达到 安全点 暂停下来
这时候 垃圾回收线程 不会 受到 其他线程 的 干扰, 不然会导致 其他 线程 找到 错误的 内存地址。


4.2 吞吐量优先 回收器

Parallel 并行

UserParallelGC 回收器 发生在 新生代 中 , 使用 复制算法 – mirror gc
UserParallelOldGC 回收器 发生在 老年代 , 使用 标记整理 算法 – full gc

用户线程 跑到 一个 安全点(同上) ,垃圾回收器 会开启 多个线程 进行 垃圾回收 ,默认情况下
垃圾回收线程 个数 与 CPU 个数 相关 。

在垃圾回收期间, 垃圾回收线程 多个同时进行,但 不允许 用户工作线程 继续运行,STW。

在这里插入图片描述
在这里插入图片描述


4.3 响应时间优先
CMS 收集器 (Concurrent Mark Sweep)
一种以获取最短回收停顿时间为目标的老年代收集器
特点:基于标记-清除算法实现。并发收集、低停顿,但是会产生内存碎片

用户工作线程和垃圾回收线程并发执行 ,都要去抢占CPU,减少STW (新生代)

应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如 web 程序、b/s 服务
CMS 收集器的运行过程分为下列4步:
初始标记:标记 GC Roots 能直接到的对象。速度很快但是仍存在 Stop The World 问题。
并发标记:进行 GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。
重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在 Stop The World 问题
并发清除:对标记的对象进行清除回收,清除的过程中,可能任然会有新的垃圾产生,这些垃圾就叫浮动垃圾,如果当用户需要存入一个很大的对象时,新生代放不下去,老年代由于浮动垃圾过多,就会退化为 serial Old 收集器,将老年代垃圾进行标记-整理,当然这也是很耗费时间的!

CMS 收集器的内存回收过程是与用户线程一起并发执行的,
可以搭配 ParNew 收集器(多线程,新生代,复制算法)
与 Serial Old 收集器(单线程,老年代,标记-整理算法)使用。

多个CPU 并发执行 , 线程都达到 安全点 暂停下来, CMS 垃圾回收器 开始工作,
进行初始标记的动作, 仍然需要 STW ; 其他用户线程 暂停下来 , 速度 很快 , 标记 根对象 , 用户线程 恢复 执行,
垃圾回收线程 还可以 并发 进行 标记 , 把剩余的 垃圾 找出来, 与 用户线程 并发执行;
不用暂停STW, 等到并发标记结束后, 重新标记 需要 STW, 因为 在 并发标记的 同时,用户线程也在工作,
用户线程 可能 产生 新对象 ,或者 改变 对象的引用 , 所以等到 并发标记 结束 以后, 在 做一遍
重新标记的 工作,等重新标记完, 用户线程 又可以 恢复 运行, 垃圾回收线程 并发 进行 清理。

用户线程 运行时, 可能 会修改对象,
比如 原本A 可达 B ,但经过 用户线程 操作后 A 又不可达 B, B就成为了 垃圾 , 所以 需要 重新 标记这些在 并发标记 阶段, 用户线程 产生 新的垃圾。


4.4 G1 垃圾回收器

在这里插入图片描述

低延迟: 响应时间, 单次耗时

4.4.1 定义
适用场景

  • 同时 注重 吞吐量 (throughput) 和 低延迟 (Low latency), 默认 的 暂停目标 是 200 ms
  • 超大堆内存, 会将 堆 划分成 多个 大小相等 的 Region (区域)
  • 整体上 是 标记 - 整理 算法, 两个区域 之间 是 复制算法
  • 高并发

相关 vm 参数

在这里插入图片描述


4.4.2 G1垃圾回收阶段

三大阶段 : 新生代 新生代+并发标记 混合收集 --> 循环过程

新生代伊甸园垃圾回收—–>
内存不足,新生代回收+并发标记—–>
混合收集,回收新生代伊甸园、幸存区、老年代内存——>
新生代伊甸园垃圾回收(重新开始)

在这里插入图片描述

4.4.2.1 G1垃圾回收阶段 (具体)

1.Young Collection:

存在Stop The World (时间短)

超大堆内存, 会将 堆 划分成 多个 大小相等 的 Region–>
| 向下
分区算法region:分代是按对象的生命周期划分,分区则是将堆空间划分连续几个不同小区间,每一个小区间独立回收,可以控制一次回收多少个小区间,方便控制 GC 产生的停顿时间

		下面是堆内存被划分成连续几个不同小区间 				E:伊甸园 S:幸存区 O:老年代     

在这里插入图片描述
新生代伊甸园垃圾回收 : 伊甸园 幸存的对象 使用 复制算法 放入 幸存区
又同以前一样, 增加年龄, 然后超过阙值(15)放入老年代


2.Young Collection + CM:

  • CM:并发标记
  • 在 Young GC 时会对 GC Root 进行初始标记
  • 在老年代占用堆内存的比例达到阈值时,对进行并发标记(不会STW),阈值可以根据用户来进行设定

在这里插入图片描述


3.Mixed Collection:

  • 会对E S O 进行全面的回收

  • 最终标记(Remark)会STW

  • 拷贝存活(Evacuation)会STW

  • -XX:MaxGCPauseMills:xxx :用于指定最长的停顿时间

  • 优先收集 垃圾最多的区域

  • 问:为什么有的老年代被拷贝了,有的没拷贝?
    因为指定了最大停顿时间,如果对所有老年代都进行回收,耗时可能过高。为了保证时间不超过设定的停顿时间,会回收最有价值的老年代(回收后,能够得到更多内存)

在这里插入图片描述


Full GC:

SerialGC

  • 新生代内存不足发生的垃圾收集 - minor gc
  • 老年代内存不足发生的垃圾收集 - full gc

ParallelGC

  • 新生代内存不足发生的垃圾收集 - minor gc
  • 老年代内存不足发生的垃圾收集 - full gc

CMS

  • 新生代内存不足发生的垃圾收集 - minor gc
  • 老年代内存不足

G1

  • 新生代内存不足发生的垃圾收集 - minor gc
  • 老年代内存不足(老年代所占内存超过阈值)
  • 如果垃圾产生速度慢于垃圾回收速度,不会触发Full GC,还是并发地进行清理
  • 如果垃圾产生速度快于垃圾回收速度,便会触发Full GC

Young Collection 跨代引用:

新生代回收的跨代引用(老年代引用新生代)问题

在这里插入图片描述

  • 卡表:老年代被划为一个个卡表
  • Remembered Set:Remembered Set 存在于E(新生代)中,用于保存新生代对象对应的脏卡
  • 脏卡:O被划分为多个区域(一个区域512K),如果该区域引用了新生代对象,则该区域被称为脏卡
  • 在引用变更时通过post-write barried + dirty card queue
    concurrent refinement threads 更新 Remembered Set
    在这里插入图片描述

Remark:

重新标记阶段

  • 黑色:已被处理,需要保留的

  • 灰色:正在处理中的

  • 白色:还未处理的

在这里插入图片描述

但是在并发标记过程中,有可能A被处理了以后未引用C,但该处理过程还未结束,在处理过程结束之前A引用了C,这时就会用到remark

  • 之前C未被引用,这时A引用了C,就会给C加一个写屏障,写屏障的指令会被执行,将C放入一个队列当中,并将C变为 处理中 状态

  • 在并发标记阶段结束以后,重新标记阶段会STW,然后将放在该队列中的对象重新处理,发现有强引用引用它,就会处理它

在这里插入图片描述


JDK 8u20 字符串去重:

  • 优点:节省大量内存

  • 缺点:略微多占用了 cpu 时间,新生代回收时间略微增加

String s1 = new String("hello"); // char[]{'h','e','l','l','o'}
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}

  • 将所有新分配的字符串(底层是char[])放入一个队列

  • 当新生代回收时,G1并发检查是否有重复的字符串

  • 如果字符串的值一样,就让他们引用同一个字符串对象

  • 注意,其与String.intern的区别

    • intern关注的是字符串对象
    • 字符串去重关注的是char[]
    • 在JVM内部,使用了不同的字符串表

JDK 8u40 并发标记类卸载:

所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类
-XX:+ClassUnloadingWithConcurrentMark 默认启用


JDK 8u60 回收巨型对象:

-JDK 8u60 回收巨型对象一个对象大于 region 的一半时,称之为巨型对象

-G1 不会对巨型对象进行拷贝

-回收时被优先考虑回收巨型对象

-G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为0的巨型对象就可以在新生代垃圾回收时处理掉
在这里插入图片描述


JDK 9 并发标记起始时间的调整:

  • 并发标记必须在堆空间占满前完成,否则退化为 FullGC

  • JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent

  • JDK 9 可以动态调整

    • -XX:InitiatingHeapOccupancyPercent 用来设置初始值
    • 进行数据采样并动态调整
    • 总会添加一个安全的空档空间

https://docs.oracle.com/en/java/javase/12/gctuning/ 官方文档


									5.垃圾回收调优

在这里插入图片描述


5.1 调优领域

  • 内存

  • 锁竞争

  • cpu占用

  • io


5-2 确定目标

-【低延迟】还是【高吞吐量】,选择合适的回收器

  • CMS,G1,ZGC (低延迟,响应时间优先)
  • ParallelGC
  • Zing

5-3 最快的 GC

最快的GC是不发生GC

查看Full GC前后的内存占用,考虑以下几个问题:

数据是不是太多?

  • resultSet = statement.executeQuery(“select * from 大表 limit n,m”)

  • 数据表示是否太臃肿?

    • 对象图
    • 对象大小
      • 最小都16字节
      • integer 24字节
      • int 4字节
  • 是否存在内存泄漏?

    • static Map map 频繁GC OOM
    • 长时间存活对象 软引用 弱引用 第三方缓存实现

5.4 新生代调优

  • 新生代特点

    • 所有 new 对象 操作 的 内存分配 非常廉价

    • 当new 一个 对象的时候 ,在 伊甸园 分配对象,分配速度快

      • TLAB (thread-local allaction buffer)
      • 当 new一个 对象的时候 会检查 TLAB 是否有可用 的 缓冲区内存 ,优先 会在这个 区域分配内存, 为了线程安全问题。(让每个线程 用自己 私有的 伊甸园内存 进行对象分配,可防止多个线程创建对象时的干扰)
    • 死亡对象的回收代价是零 (使用了复制算法, 在复制的过程 中 会 释放内存)

    • 新生代 大部分 对象 用过 即死 (垃圾回收的时候, 大部分 对象能够被 垃圾回收, 极少部分 会 幸存 )

    -Mirror GC 的 时间 远远 低于 Full GC
    (相差 一到两个 数量级)

    • 大越好吗?
      -Xmn
      Sets the initial and maximum size (in bytes) of the heap for the young generation (nursery). GC is
      performed in this region more often than in other regions. If the size for the young generation is
      too small, then a lot of minor garbage collections are performed. If the size is too large, then only
      full garbage collections are performed, which can take a long time to complete. Oracle
      recommends that you keep the size for the young generation greater than 25% and less than
      50% of the overall heap size.
      设置新生代堆的初始大小和最大大小(字节)。
      GC是在这一地区的演出频率高于其他地区。
      如果新生代的设置如果太小,则会执行许多 mirror gc。
      如果设置太大,则仅限执行full gc,这可能需要很长时间才能完成。
      建议您将新生代的大小保持在25%以上,小于25% , 总堆大小的50%。

    • 新生代能容纳所有【并发量 * (请求-响应)】的数据

    • 幸存区大到能保留【当前活跃对象+需要晋升对象】

    • 晋升阈值配置得当,让长时间存活对象尽快晋升

      • -XX:MaxTenuringThreshold=threshold 调整 最大 晋升预值
      • -XX:+PrintTenuringDistribution 在执行过程中 可以 显示 幸存区 中 对象的详情
        Desired survivor size 48286924 bytes, new threshold 10 (max 10)
      • age 1: 28992024 bytes, 28992024 total
      • age 2: 1366864 bytes, 30358888 total
      • age 3: 1425912 bytes, 31784800 total

5.5 老生代调优

以 CMS 为例

  • CMS的老年代 内存 越大越好
  • 先尝试 不 调优 ,如果 没有 Full GC 那么 说明 老年代 空间 充裕 ,否则 先 尝试 调试 新生代
  • 观察 发生 Full GC 时 老年代 内存 占用, 将 老年代 内存 预设 调大 1/4 -1/3
    • -XX : -XX:CMSInitiatingOccupancyFraction=percent 控制 老年代 占用 多少的时候 使用 CMS垃圾回收,percent 值

5.6 案例

  • 案例1 Full GC 和 Minor GC频繁

    • 意味着堆内存空间紧张,可能是新生代空间过小,导致不需要晋升到老年代的对象进入老年代,然后老年代空间存在大量这种对象,空间也紧张就是频繁gc;
  • 案例2 请求高峰期发生 Full GC,单次暂停时间特别长 (CMS)

    • 可以重新标记前开启垃圾回收,这样重新标记对象数没有那多,性能有一定提高;
  • 案例3 老年代充裕情况下,发生 Full GC (CMS jdk1.7)

    • 可能是JDK1.7永久代空间不足导致内存不足;
    • JDK1.8元空间使用系统内存不易内存溢出
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值