黑马程序员JVM笔记02-垃圾回收

对象已死?

引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器的值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。这也就是需要回收的对象。
引用计数算法是对象记录自己被多少程序引用,引用计数为零的对象将被清除

内存泄漏问题:
在这里插入图片描述

可达性分析法

  • Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
  • 从GCRoots开始向下搜索,搜索走过的路径称为引用链,当一个对象到GCRoots没有任何引用链相连时,就说明这个对象是不可用的,那虚拟机就会判断它是可回收的对象
  • 哪些对象可以作为GCRoots?
    虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI引用的对象

四种引用

在这里插入图片描述

  • 强引用
    只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
  • 软引用(SoftReference)
    仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用对象;可以配合引用队列来释放软引用自身
    应用场景:如用来做一些网页缓存、图片缓存
/**
 * 演示软引用, 配合引用队列
 * -Xmx20m -XX:+PrintGCDetails -verbose:gc
 */
public class Demo2_4 {
    private static final int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) {
        List<SoftReference<byte[]>> list = new ArrayList<>();

        // 引用队列
        ReferenceQueue<byte[]> queue = new ReferenceQueue<>();

        for (int i = 0; i < 5; i++) {
            // 关联了引用队列, 当软引用所关联的 byte[]被回收时,软引用自己会加入到 queue 中去
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
            System.out.println(ref.get());
            list.add(ref);
            System.out.println(list.size());
        }

        // 从队列中获取无用的 软引用对象,并移除
        Reference<? extends byte[]> poll = queue.poll();
        while( poll != null) {
            list.remove(poll);
            poll = queue.poll();
        }

        System.out.println("===========================");
        for (SoftReference<byte[]> reference : list) {
            System.out.println(reference.get());
        }

    }
}
  • 弱引用(WeakReference)
    仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象,可以配合引用队列来释放弱引用自身
/**
 * 演示弱引用
 * -Xmx20m -XX:+PrintGCDetails -verbose:gc
 */
public class Demo2_5 {
    private static final int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) {
        //  list --> WeakReference --> byte[]
        List<WeakReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
            list.add(ref);
            for (WeakReference<byte[]> w : list) {
                System.out.print(w.get()+" ");
            }
            System.out.println();

        }
        System.out.println("循环结束:" + list.size());
    }
}

  • 虚引用(PhantomReference)
    必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存
    在这里插入图片描述

  • 终结器引用(FinalReference)
    无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize方法,第二次 GC 时才能回收被引用对象
    在这里插入图片描述

垃圾回收算法

标记-清除算法

标记的是可回收对象
在这里插入图片描述

  • 速度较快
  • 会产生内存碎片

标记-整理算法

在这里插入图片描述

  • 速度较慢
  • 没有内存碎片

标记-复制算法

标记可回收对象,将存活对象复制到To区域,再交换From和To
在这里插入图片描述

  • 没有内存碎片
  • 可用内存空间减半

分代垃圾回收

分代垃圾回收

在这里插入图片描述

  • 对象首先分配在伊甸园区域
  • 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用copy 复制到 to 中,存活的对象年龄加 1并且交换 from、to
  • minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
  • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(占对象头4bit)
  • 根据对象年龄有另外一个策略也会让对象进入老年代,不用等待15次GC之后进入老年代,他的大致规则就是,假如当前放对象的Survivor,一批对象的总大小大于这块Survivor内存的50%,那么大于这批对象年龄的对象,就可以直接进入老年代了
  • 大对象直接进入老年代:如果设置了-XX:PretenureSizeThreshold这个参数,那么如果你要创建的对象大于这个参数的值,比如分配一个超大的字节数组,此时就直接把这个大对象放入到老年代,不会经过新生代
  • 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长

相关VM参数

在这里插入图片描述

垃圾回收器

在这里插入图片描述

串行

  • 单线程
  • 适合堆内存较小、处理器数量少,如个人电脑
  • -XX:+UseSerialGC = Serial + SerialOld,Serial使用的是标记-复制算法(新生代),SerialOld使用的是标记-整理算法(老年代)
    在这里插入图片描述

吞吐量优先

  • 多线程
  • 适合堆内存较大、多核CPU
  • 让单位时间内,STW时间最短,垃圾回收时间占比最低,就能提高吞吐量,吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾回收时间)
  • -XX:+UseParallelGC-XX:+UseParallelOldGC(开启其中一个,另一个会自动开启,JDK1.8下默认是开启的)
  • -XX:+UseAdaptiveSizePolicy:开启自适应调整伊甸园和survivor区比例以及晋升老年代的对象大小,会自动调整参数来达到最大吞吐量
  • 使用-XX:GCTimeRatio=ratio(关注吞吐量)和-XX:MaxGCPauseMillis=ms(关注最大停顿时间)给虚拟机设定一个优化目标
  • -XX:ParallelGCThreads=n:设定并行垃圾回收线程的数量
  • 这里的并行指多个垃圾回收线程同时运行
    在这里插入图片描述

响应时间优先(低延迟)

  • 多线程
  • 适合堆内存大、多核CPU
  • 低延迟:尽可能的缩短单次垃圾回收的时间(STW)
  • -XX:+UseParNewGC-XX:+UseConcMarkSweepGCSerialOld(CMS采用标记-清除算法,当因为碎片过多无法存新对象时,会出现并发失败,会退化到SerialOld串行回收器)
  • -XX:ParallelGCThreads=n设置并行线程数
    -XX:ConcGCThreads=threads设置并发线程数(一般为并行线程数的1/4)
  • -XX:CMSInitiatingOccupancyFraction=percent:设置触发CMS垃圾回收的时机,即内存占用达到percent百分比时就进行垃圾回收(预留空间给浮动垃圾)
  • -XX:+CMSScavengeBeforeRemark:再回收老年代时,先对新生代进行一次垃圾回收
  • 这里的并发指垃圾回收线程和用户线程可以同时运行

在这里插入图片描述

G1回收器

关于G1

  • 定义:G1即Garbage First

  • 发展:
    2004 论文发布
    2009 JDK 6u14 体验
    2012 JDK 7u4 官方支持
    2017 JDK 9 默认

  • 适用场景

    • 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms
    • 适合超大堆内存,会将堆划分为多个大小相等的Region
    • 整体上是标记+整理算法,两个区域之间是复制算法
  • 相关VM参数

    • -XX:+UseG1GC:开启G1回收器(JDK9默认)
    • -XX:G1HeapRegionSize=size
    • -XX:MaxGCPauseMillis=time

G1垃圾回收阶段

在这里插入图片描述

阶段一-Young Collection
  • 新生代垃圾回收会STW(较短)
    在这里插入图片描述
  • 幸存的对象同样使用复制算法拷贝到幸存区
    在这里插入图片描述
  • 幸存区存活一定次数就会进入老年代
    在这里插入图片描述
阶段二-Young Collection+并发标记
  • 在 Young GC 时会进行 GC Root 的初始标记
  • 老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW),由下面的 JVM 参数决定
    -XX:InitiatingHeapOccupancyPercent=percent (默认45%)

在这里插入图片描述

阶段三-Mixed Collection
  • 会对 E、S、O 进行全面垃圾回收
  • 最终标记(Remark)会 STW
  • 拷贝存活(Evacuation)会 STW
  • -XX:MaxGCPauseMillis=ms设置垃圾回收最大停顿时间,当时间不够时,G1对于老年代的垃圾回收就只会回收那些回收价值较高的对象
    在这里插入图片描述

总结Full GC

  • SerialGC
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • ParallelGC
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • CMS
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足-分情况:并发失败才算重GC
      G1
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足-分情况:当内存回收速度跟不上垃圾产生速度,才算重GC

Young Collection 跨代引用问题

  • 新生代回收的跨代引用(老年代引用新生代)问题
  • 老年代使用卡表,新生代使用Remembered Set记录有哪些脏卡
  • 在引用变更时通过 post-write barrier 更新,该异步指令存放在 dirty card queue
  • concurrent refinement threads 更新 Remembered Set
  • 卡表中引用了新生代的卡为脏卡
    在这里插入图片描述

Remark重标记

  • 重标记:进行进一步的检查,防止并发标记过程中对象引用改变导致错误
  • 写屏障:pre-write barrier,将改变引用的对象加入到 satb_mark_queue
  • 下图:黑色(已处理的存活对象)、灰色(正在处理的对象)、白色(垃圾或未处理的对象)
    在这里插入图片描述

G1的一些优化

JDK 8u20 字符串去重
  • 优点:节省大量内存
  • 缺点:略微多占用了 cpu 时间,新生代回收时间略微增加
  • -XX:+UseStringDeduplication开启字符串去重
//JDK8中字符串底层是char数组
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
  • JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent
  • JDK 9 可以动态调整
    • -XX:InitiatingHeapOccupancyPercent 改为只用来设置初始值
    • 进行数据采样并动态调整
    • 总会添加一个安全的空档空间

垃圾回收调优

Windows下查看已设置的GC参数:"C:\Program Files\Java\jdk1.8.0_91\bin\java" -XX:+PrintFlagsFinal -version | findstr "GC"
在这里插入图片描述

调优预备知识
掌握 GC 相关的 VM 参数,会基本的空间调整
掌握相关工具
明白一点:调优跟应用、环境有关,没有放之四海而皆准的法则

调优的方向

调优不仅仅局限于GC调优:

  • 内存
  • 锁竞争
  • cpu 占用
  • io

确定调优的目标

  • 低延迟(如互联网项目)还是高吞吐量(如科学运算),选择合适的回收器
  • 低延迟:CMS,G1,ZGC
  • 高吞吐量:ParallelGC
  • Zing

最快的GC是不发生GC

查看 FullGC 前后的内存占用,当频繁发生GC时,要考虑下面几个问题(是因为自身代码导致的问题?)

  • 数据是不是太多?

    例如:resultSet = statement.executeQuery(“select * from 大表”)
    就会加载太多数据到内存,可以在sql后加limit n限制

  • 数据表示是否太臃肿?

    • 对象图
    • 对象大小 (最小的对象都要占16字节) (Integer 24字节对比int 4字节)
  • 是否存在内存泄漏?
    例如使用Map存储了过多数据?
    避免内存泄漏:

    • 使用软引用
    • 使用弱引用
    • 使用第三方缓存实现,如Redis

新生代的GC调优

新生代特点

  • 所有的 new 操作的内存分配非常廉价
  • 每个线程都在伊甸园中有一块私有区域:TLAB,即thread-local allocation buffer,来避免多线程分配伊甸园内存时互相干扰
  • 死亡对象的回收代价是零
  • 大部分对象用过即死
  • Minor GC 的时间远远低于 Full GC

新生代内存越大越好?

在这里插入图片描述
合适的新生代内存:

  • 新生代能容纳所有【并发量 * (请求响应占用的内存)】的数据
  • 幸存区大到能保留【当前活跃对象+需要晋升对象】
    若幸存区设置太小,会导致“短命”的幸存对象提前进入老年代
  • 晋升阈值配置得当,让长时间存活对象尽快晋升
    -XX:MaxTenuringThreshold=threshold
    -XX:+PrintTenuringDistribution
    在这里插入图片描述

老年代的GC调优

以CMS垃圾回收器为例:

  • CMS 的老年代内存越大越好,预留更多空间,来避免因为浮动垃圾引起的并发回收失败(失败会退化到串行回收)
  • 先尝试不做老年代调优,如果系统没有出现 Full GC,就先尝试调优新生代
  • 观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3
    -XX:CMSInitiatingOccupancyFraction=percent:设置老年代内存达到多少就触发CMS垃圾回收

调优案例

  • 案例1 Full GC 和 Minor GC频繁
    是因为新生代内存太小?调整新生代内存大小
  • 案例2 请求高峰期发生 Full GC,单次暂停时间特别长 (CMS)
    通过打印日志查看是回收的哪个阶段耗时较长
  • 案例3 老年代充裕情况下,发生 Full GC (CMS jdk1.7)
    JDK1.7是使用永久代作为方法区的实现,故永久代的空间不足也会导致Full GC
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值