JVM--垃圾回收

JVM–垃圾回收(GarbageCollection)

背景

  • JVM–垃圾回收(GarbageCollection)

  • 博主以黑马JVM进行学习

概述

  • 如何判断对象可以回收
  • 垃圾回收算法
  • 分代垃圾回收
  • 垃圾回收器
  • 垃圾回收调优

如何判断对象可以回收

  • 引用计数法

    • 被引用一次就+1

    • 可能出现循环引用问题

  • 可达性分析算法

    • 先确定根对象,即肯定不能被回收的对象
    • 看对象有没有被根对象直接或间接使用,如果有不能回收,没有可以回收
    • Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
    • 扫描堆中的对象,看是否能沿着GC Root对象为起点的引用链找到该对象,找不到,表示可以回收
    • 哪些对象可以作为GC Root呢?
      • 通过Memory Analyzer(MAT)工具和jmap工具
      • jmap -dump:format=b,live,file=1.bin 21384
        • format=b:生成二进制格式的堆转储文件
        • live:只导出存活的对象(会触发一次 Full GC)
        • file=1.bin:导出到 1.bin 文件
        • 21384:目标 Java 进程的 PID
      • 根对象有
        • System Class: 系统加载的类(如 rt.jar 中的 java.lang.String),不会被回收
        • Native Stack: JNI 方法引用的对象(本地方法栈中的对象)
        • Thread: 活跃线程(如正在运行的用户线程、GC 线程)
        • Busy Monitor: 持有同步锁(synchronized)的对象
  • 四种引用

    • 强引用

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

      • -Xmx20m -XX:+PrintGCDetails -verbose:gc

      • 当没有强引用它时,仅有软引用引用该对象时,在垃圾回收后,当内存不充足时,会再次触发垃圾回收

      • 关联了引用队列,但软引用锁关联的对象被回收时,软引用对象会进入引用队列,方便进一步释放(即可以配合引用队列来释放软引用自身)

      • 引用队列ReferenceQueue<>()

    • 弱引用(WeakReference)

      • -Xmx20m -XX:+PrintGCDetails -verbose:gc

      • 当没有强引用它时,仅有软引用引用该对象时,不管内存充不充足,都会回收弱引用对象

      • 弱引用对象会进入引用队列,方便进一步释放(即可以配合引用队列来释放弱引用自身)

    • 虚引用

      • 必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由Reference Handler 线程调用虚引用相关方法释放直接内存

      • Cleaner虚引用对象

      • 当ByteBuffer被垃圾回收时,ByteBuffer的直接内存部分不能被java管理,所以虚引用对象进入引用队列,调用Unsafe.freeMemory方法释放直接内存

    • 终结器引用

      • 所有对象都会继承Object对象,都会有finallize()方法
      • 当对象被垃圾回收时,终结器引用对象就会进入引用队列
      • finallize()方法工作效率很低,第一次并不能释放
      • 无需手动编码。但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次GC时才能回收被引用对象
    • 对比

    引用类型 回收时机 典型应用
    强引用 永不回收 普通对象引用(Object o = new Object()
    软引用 内存不足时 缓存(如图片缓存,内存不足自动释放)
    弱引用 GC 时必回收 ThreadLocal、WeakHashMap(缓存临时数据)
    虚引用 仅作为回收通知 DirectByteBuffer 直接内存释放
    终结器引用 两次 GC 后回收 Object.finalize ()(已被标记为过时)

垃圾回收算法

  • 标记清除
    • 根据GCRoot是否引用进行标记
    • 再清除,记住位置坐标
    • 优点:速度快
    • 缺点:容易产生内存碎片,内存空间不连续
  • 标记整理
    • 根据GCRoot是否引用进行标记
    • 整理:避免出现内存碎片,让内存变得更紧凑
    • 优点:不易产生内存碎片
    • 缺点:效率较低
  • 复制
    • 把内存区划分成大小相等的俩个区,一个称为FROM,另一个分为TO(空着)
    • 把FROM区存活的对象复制到TO区,清空FROM,再进行俩个区交换·
    • 优点:不会产生内存碎片
    • 缺点:会占用双倍内存空间
  • 对比
算法 优点 缺点 适用区域
标记清除 速度快、不移动对象 内存碎片、空间不连续 老年代(CMS)
标记整理 无内存碎片 效率低(需要移动对象) 老年代(SerialOld)
复制 无碎片、效率高 占用双倍内存 新生代(Eden/Survivor)

分代垃圾回收

  • 分为老年代(垃圾回收频率较低)、新生代(经常释放)
  • 新生代三个区:伊甸园、幸存区From、幸存区To
  • 对象创建时,先存伊甸园中,当伊甸园满时(新生代空间不足时),就会触发一次垃圾回收(Minor GC),如果存活就复制到幸存区To并寿命+1同时幸存区To变成From,而From变成To
  • minor gc 会引发 stop the world ,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
  • 当在幸存区的对象超过了一个阈值(最大寿命是15(4bit)),则晋升到老年代
  • 当新生代和老年代都放不下对象时,会先尝试触发一次minor gc ,之后仍然不足,就会触发一次Full GC
  • Full GC 也会引发 stop the world ,不过时间更长
  • GC分析
    • 相关的VM参数
参数名称 参数含义
堆初始大小 -Xms
堆最大大小 -Xmx 或 -XX:MaxHeapSize=size
新生代大小 -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size)
幸存区比例(动态) -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
幸存区比例 -XX:SurvivorRatio=ratio
晋升阈值 -XX:MaxTenuringThreshold=threshold
打印详情 -XX:+PrintTenuringDistribution
GC 详情 -XX:+PrintGCDetails -verbose:gc
FullGC 前 MinorGC -XX:+ScavengeBeforeFullGC
  • CG大对象oom,在新生代空间不够的情况下,会直接晋升老年代

垃圾回收器

  • 串行

    • 单线程
    • 堆内存较小,适合个人电脑
    • 开启语句:-XX:+UseSerialGC = Serial(工作在新生代:复制算法) + SerialOld(老年代:标记整理算法)
    • 因为是单线程工作,所以当垃圾回收线程工作时,其它线程会发生阻塞
  • 吞吐量优先

    • 多线程
    • 堆内存较大,多核cpu
    • 让单位时间内,STW的时间最短
    • 开启语句:
      • -XX:+UseParallelGC ~ -XX:+UseParallelOldGC
      • -XX:+UseAdaptiveSizePolicy //采用自适应调整大小
      • -XX:GCTimeRatio=ratio //调整堆的大小 公示 1/1+ratio
      • -XX:MaxGCPauseMillis=ms //最大暂停时间毫秒数,默认200ms
      • -XX:ParallelGCThreads=n //控制线程数
    • 垃圾回收线程数与cpu多少核有关
  • 响应时间优先

    • 多线程
    • 堆内存较大,多核cpu
    • 尽可能让单次STW的时间最短
    • 其它用户线程仍然可以运行
    • 开启语句:
      • -XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
      • -XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads //并行的垃圾回收线程/并发的垃圾回收线程
      • -XX:CMSInitiatingOccupancyFraction=percent //何时执行垃圾回收
        • -XX:+CMSScavengeBeforeRemark //在重新标记之前,做一次垃圾回收
    • 对整个应用的吞吐量是有影响的
  • G1:Garbage First

    • 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是200ms

    • 超大堆内存,会将堆划分为多个大小相等的Region

    • 整体上是标记+整理算法,两个区域之间是复制算法

    • 相关JVM参数

      • -XX:+UseG1GC //jdk9以后默认启动
      • -XX:G1HeapRegionSize=size
      • -XX:MaxGCPauseMillis= time
    • G1垃圾回收阶段

      • 循环: —>young Collection—>Young Collection + Concurrent Mark —>Mixed Collection—>
    • Young Collection

      • 会STW
    • Young Collection + CM

      • 在Young GC 时会进行GC Root的初始标记
      • 老年代占用堆空间比例达到阈值,进行并发标记(不会STW),由下面的JVM参数决定
        • -XX:InitiatingHeapOccupancyPercent=percent(默认45%s)
    • Mixed Collection

      • 会对E(伊甸园区)、S(幸存区)、O(老年区)进行全面的垃圾回收

        • 最终标记(Remark)会STW

        • 拷贝存活(Evacuation)会STW

          -XX:MaxGCPauseMillis=ms //最大暂停时间

    • Full GC

      • SerialGC

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

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

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

        • 新生代内存不足发生的垃圾收集 - minor gc
        • 老年代内存不足,分俩中情况
          • 第一种,清理垃圾速度足够清理干净
          • 第二种,清理垃圾速度比产生垃圾速度慢,就会退为并行垃圾回收
    • Young Collection 跨代引用

      • 新生代回收的跨代引用(老年代引用新生代)问题
        • 使用查表技术,将老年代分成card,有脏卡区
        • 卡表与Remembered Set
        • 在引用变更时通过post -write barrier + dirty card queue
        • concurrent refinement threads 更新 Remember Set
    • Remark(重标记)

      • pre-write barrier + satb_mark_queue
    • JDK 8u20字符串去重

      • 优点:节省大量内存

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

      • -XX:+UseStringDeduplication

        • String s1 = new String("hello"); // char[]{'h','e','l','l','o'}
          String s2 = new String("hello"); // char[]{'h','e','l','l','o'}
          
      • 将所有新分配的字符串放到一个队列

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

      • 如果它们的值一样,让它们引用同一个char[]

      • 注意,与String.intern()不一样

        • String.intern()关注的是字符串对象
        • 而字符串去重关注的是char[]
        • 在JVM内部,使用了不同的字符串表
    • JDK 8u40并发标记类卸载

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

      • 一个对象大于region的一半时,称之为巨型对象
      • G1不会对巨型对象进行拷贝
      • 回收时被优先考虑
      • G1会跟踪老年代所有incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉(希望越早回收越好)
    • JDK 9并发标记起始时间的调整

      • 并发标记必须在堆空间占满前完成,否则退化为FullGC
      • JDK9之前需要使用-XX:InitiatingHeapOccupancyPercent(默认45%)
      • JDK9可以动态调整
        • -XX:InitiatingHeapOccupanyPercent用来设置初始值
        • 进行数据采样并动态调整
        • 总会添加一个安全的空挡空间
    • JDK 9更高效的回收

回收器 核心目标 适用场景 JVM 参数
Serial + SerialOld 简单、低内存 单机程序、小堆内存(<1G) -XX:+UseSerialGC
ParallelGC 高吞吐量 后台计算、批处理程序 -XX:+UseParallelGC
CMS 低延迟 电商、Web 应用(JDK8) -XX:+UseConcMarkSweepGC
G1 平衡吞吐 & 延迟 大堆内存(>4G)、分布式应用 -XX:+UseG1GC
ZGC/Shenandoah 极低延迟(<10ms) 高并发、低延迟场景(JDK11+) -XX:+UseZGC

垃圾回收调优

  • 预备知识

    • 掌握GC相关的VM参数,会基本的空间调整
    • 掌握相关工具
    • 调优跟应用、环境有关
    • https://docs.oracle.com/en/java/javase 官网学习
    • 查看虚拟机运行的参数:-XX:+PrintFlagsFinal -version | findstr “GC”
  • 调优领域

    • 内存
    • 锁竞争
    • cpu占用
    • io
  • 确定目标

    • 【低延迟】还是【高吞吐量】,选择合适的回收器
    • CMS,G1,ZGC
    • ParallelGC(高吞吐量)
    • Zing
  • 最快的GC是不发生GC

    • 查看FullGC前后的内存占用,考虑下面几个问题
      • 数据是不是太多?
        • resultSet = statement.executeQuery(“select * from 大表 limit n”)
      • 数据表示是否太臃肿?
        • 对象图(用到哪个查哪个)
        • 对象大小 (能用基本类型int 4字节,就不用Integer 24字节)
      • 是否存在内存泄漏?
        • static Map map =
        • 软引用
        • 弱引用
        • 第三方缓存实现,如redis等
  • 新生代调优

    • 内存调优都建议从新生代开始

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

      • TLAB thread-local allocation buffer
    • 死亡对象的回收代价是零

    • 大部分对象用过即死

    • Minor GC的时间远远低于Full GC

    • 新生代内存越大越好?

      • 一开始,空间大小越大,吞吐量越高,到一定时间后,垃圾回收时间变长,吞吐量开始下降
        • 建议大小:新生代能容纳所有【并发量* (请求-响应)】的数据
      • 幸存区大到能保留【当前活跃对象+需要晋升对象】
      • 晋升阈值配置得当,让长时间存活的对象尽快晋升
        • -XX:MaxTenuringThreshold=threshold 调整最大晋升阈值
        • -XX:+PrintTenuringDistribution
  • 老年代调优

    • 以CMS为例
      • CMS的老年代内存越大越好
      • 先尝试不做调优,如果没有Full GC那么已经。。。,否则先尝试调优新生代
      • 观察发生Full GC时老年代内存占用,将老年代内存预设调大1/4~1/3
        • -XX:CMSInitiationOccupancyFraction=percent //值越低,触发时间越早
  • 案例

    • 案例1 Full GC 和Minor GC频繁

      • 先增大新生代内存
    • 案例2 请求高峰期发生Full GC,单次暂停时间特别长(CMS)

      • 使用工具查看是哪个阶段暂停时间长
    • 案例3 老年代充裕情况下,发生Full GC(CMS JDK1.7)

      • 1.7永久代,空间设置小了,就会触发GC
    • 问题现象 解决方案
      Minor GC 频繁 增大新生代内存(-Xmn)
      Full GC 频繁 增大老年代内存、检查内存泄漏、调整 CMS 触发阈值
      STW 时间过长 切换到 G1/ZGC、降低 MaxGCPauseMillis
      JDK7 永久代 OOM 增大永久代(-XX:MaxPermSize=256m)