admin管理员组

文章数量:1530517

垃圾回收

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

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

1.1 引用计数法

1.2 可达性分析算法

  • Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
  • 扫描堆中的对象,看是否能够沿着 GC Root对象 为起点的引用链找到该对象,找不到,表示可以回收
  • 哪些对象可以作为 GC Root ?

1.3 四种引用的定义

引用的定义
如果reference类型的数据中,存储的数值代表的是另一块内存的起始地址,就成这块内存代表着一个引用.

1.强引用

定义
强引用在程序代码中普遍存在,类似于
Object obj = new Object()
这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象.
回收时机
永不回收

2.软引用

定义
用来描述一些还有用但非必需的对象.对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收.如果这次回收还没有足够的内存,才会抛出内存溢出的异常.
回收时机
内存溢出异常发生之前

3.弱引用

定义
用来描述非必需对象的,它的强度软引用更弱一点,被弱引用的关联的对象只能生存到下一次垃圾收集发生之前.当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象.
回收时机
每一次垃圾收集器工作

4.虚引用

定义
虚引用也称为幽灵引用或者幻影引用.是最弱的一种引用关系。虚引用的存在不会对对象的生存时间构成任何影响,为一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知
回收时机
无影响,仅在对象被回收时收到一个系统通知。

​ 总结

关于四大引用的更详细阐述参考博客 https://blog.csdn/u014086926/article/details/52106589

1.4 四种引用回收的具体时机

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

1.5 软引用对象实例

/**
 * 演示软引用 可以应用于图片的加载等场景
 * -Xmx20m -XX:+PrintGCDetails -verbose:gc
 */
public class Demo2_3 {

    private static final int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
//        List<byte[]> list = new ArrayList<>();
//        for (int i = 0; i < 5; i++) {
//            list.add(new byte[_4MB]);
//        }
//
//        System.in.read();
        soft();

    }

    public static void soft() {
        // list --> SoftReference --> byte[]
        List<SoftReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
            //SoftReference保存了对一个Java对象的软引用后,在垃圾线程对 这个Java对象回收前,SoftReference类所提供的get()方法返回Java对象的强引用。
            //另外,一旦垃圾线程回收该Java对象之 后,get()方法将返回null。
            System.out.println(ref.get());
            list.add(ref);
            System.out.println(list.size());

        }
        System.out.println("循环结束:" + list.size());
        for (SoftReference<byte[]> ref : list) {
            System.out.println(ref.get());
        }
    }
}

//控制台打印的堆栈信息
[B@6d6f6e28
1
[B@135fbaa4
2
[B@45ee12a7
3
[GC (Allocation Failure) [PSYoungGen: 2140K->488K(6144K)] 14428K->13039K(19968K), 0.0034611 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[B@330bedb4
4
[GC (Allocation Failure) --[PSYoungGen: 4809K->4809K(6144K)] 17360K->17368K(19968K), 0.0019947 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 4809K->4540K(6144K)] [ParOldGen: 12559K->12516K(13824K)] 17368K->17056K(19968K), [Metaspace: 3361K->3361K(1056768K)], 0.0067460 secs] [Times: user=0.05 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) --[PSYoungGen: 4540K->4540K(6144K)] 17056K->17072K(19968K), 0.0008754 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 4540K->0K(6144K)] [ParOldGen: 12532K->655K(8704K)] 17072K->655K(14848K), [Metaspace: 3361K->3361K(1056768K)], 0.0071275 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[B@2503dbd3
5
循环结束:5
null
null
null
null
[B@2503dbd3
Heap
 PSYoungGen      total 6144K, used 4432K [0x00000000ff980000, 0x0000000100000000, 0x0000000100000000)
  eden space 5632K, 78% used [0x00000000ff980000,0x00000000ffdd43c8,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 8704K, used 655K [0x00000000fec00000, 0x00000000ff480000, 0x00000000ff980000)
  object space 8704K, 7% used [0x00000000fec00000,0x00000000feca3d60,0x00000000ff480000)
 Metaspace       used 3393K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 372K, capacity 388K, committed 512K, reserved 1048576K

软应用对象本身也会占用内存,当软引用所引用的对象被垃圾回收后,软引用对象本身也就失去了价值,这时可以配合引用队列将软引用对象本身进行删除。

/**
 * 演示软引用, 配合引用队列
 */
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());
        }

    }
}

1.6弱引用对象实例

/**
 * 演示弱引用
 * -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());
    }
}

2. 垃圾回收算法

2.1 标记清除

定义: Mark Sweep

  • 速度较快
  • 会造成内存碎片

2.2 标记整理

定义:Mark Compact

  • 速度慢
  • 没有内存碎片

2.3 复制

定义:Copy

  • 不会有内存碎片
  • 需要占用双倍内存空间

3. 分代垃圾回收

  • 对象首先分配在伊甸园区域
  • 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to
  • minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
  • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
  • 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长

3.1 相关 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

4. 垃圾回收器

  1. 串行
    • 单线程
    • 堆内存较小,适合个人电脑
  2. 吞吐量优先
    • 多线程
    • 堆内存较大,多核 cpu
    • 让单位时间内,STW 的时间最短 0.2 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高
  3. 响应时间优先
    • 多线程
    • 堆内存较大,多核 cpu
    • 尽可能让单次 STW 的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5

4.1 串行

垃圾回收线程工作的同时用户线程不能工作

-XX:+UseSerialGC = Serial + SerialOld

4.2 吞吐量优先

垃圾回收线程工作的同时用户线程不能工作

-XX:+UseParallelGC ~ -XX:+UseParallelOldGC

-XX:+UseAdaptiveSizePolicy //每次 GC 后会重新计算 Eden、From 和 To 区的大小,计算依据是 GC 过程中统计的 GC 时间、吞吐量、内存占用量。

-XX:GCTimeRatio=ratio(调整垃圾回收的时间和总时间的占比) 1/1+ratio,例如ratio=99,在100分钟的时间里有1分钟用于垃圾回收,如果达不到这个目标,parallel就会调整堆的大小

-XX:MaxGCPauseMillis=ms //最大暂停毫秒数 默认值是200

-XX:ParallelGCThreads=n

4.3 响应时间优先

垃圾回收线程工作的同时用户线程也能工作

-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld

-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads (并发的线程数一般建议设置为parallel 的1/4)

-XX:CMSInitiatingOccupancyFraction=percent //执行cms垃圾回收的老年代内存占比,比如值为80,就是老年代的内存占比为80%时执行垃圾回收,这是为了留给一些空间给浮动垃圾

浮动垃圾:cms回收器在回收过程其他的并发线程有可能此时也会产生垃圾,这时的垃圾叫做浮动垃圾。

-XX:+CMSScavengeBeforeRemark //在CMS GC前启动一次ygc,目的在于减少old gen对ygc gen的引用,降低remark时的开销-----一般CMS的GC耗时 80%都在remark阶段

cms采用标记加清除算法,在内存碎片比较多的情况下,mionor gc不足,老年代碎片过多造成并发失败,cms垃圾回收器就不能正常工作,就会退化为SerialOld回收器,回收时间就会变长

4.4 G1

定义:Garbage First

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

适用场景

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

相关 JVM 参数

-XX:+UseG1GC

-XX:G1HeapRegionSize=size

-XX:MaxGCPauseMillis=time

1) G1 垃圾回收阶段

2) Young Collection
  • 会 STW

//以复制的方式将幸存对象复制到幸存区

//幸存区的一部分对象会到老年代中去(红色箭头),幸存区的一部分对象还是会留在幸存区,伊甸园中的幸存对象会进入到幸存区

3) Young Collection + CM
  • 在 Young GC 时会进行 GC Root 的初始标记
  • 老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW),由下面的 JVM 参数决定

-XX:InitiatingHeapOccupancyPercent=percent (默认45%)

4) Mixed Collection

会对 E、S、O 进行全面垃圾回收

  • 最终标记(Remark)会 STW (在上一个并发阶段标记过程中,用户线程有可能产生新的垃圾,所及需要一次最终标记)
  • 拷贝存活(Evacuation)会 STW (对老年代进行拷贝时会回收哪些垃圾较多的区域)

-XX:MaxGCPauseMillis=ms //为了达到MaxGCPauseMillis这个目标,GC1会选择一部分价值较大的老年代对象进行回收(还是复制算法)

5) Full GC
  • SerialGC
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • ParallelGC
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • CMS
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足
  • G1
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足

​ G1什么时候可以称的上full gc,当垃圾回收的速度赶不上垃圾产生的速度,并发收集失败,就会退化成一个并行收集(和CMS类似,cms退化成串行收集)

6) Young Collection 跨代引用
  • 新生代回收的跨代引用(老年代引用新生代)问题

新生代的回收过程:首先找到GC root对象,root对象进行可达性分析,找到存活的对象,将存活的对象复制到幸存区

  • 卡表与 Remembered Set
  • 在引用变更时通过 post-write barrier + dirty card queue
  • concurrent refinement threads 更新 Remembered Set

7) Remark
  • pre-write barrier + satb_mark_queue

这张图表示的是在并发标记阶段时对象的处理状态,黑色表示已经处理完成,并且有引用在引用他们,在结束时会被保留下来,灰色的表示正在处理当中的对象,白色的表示尚未处理的对象。当处理完毕时,没有箭头指向的白色正方形因为没有对象引用他所以他会被当做垃圾回收

当对象的引用发生改变时,jvm就会给该对象加入一个写的屏障,此时出发写屏障指令,同时将给对象加入到一个队列中,并且把该对象表示成灰色,表示它还没有处理完,等到并发标记结束,进入到重新标记阶段,remark线程会从队列中取出该对象,并对他进行重新判断,判断它是否被回收

8) 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 内部,使用了不同的字符串表
9) JDK 8u40 并发标记类卸载

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

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

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

  • 回收时被优先考虑

  • G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为0(某个巨型对象在老年代的引用为0) 的巨型对象就可以在新生代垃圾回收时处理掉

11) JDK 9 并发标记起始时间的调整
  • 并发标记必须在堆空间占满前完成,否则退化为 FullGC
  • JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent
  • JDK 9 可以动态调整
    • -XX:InitiatingHeapOccupancyPercent 用来设置初始值
    • 进行数据采样并动态调整
    • 总会添加一个安全的空档空间
12) JDK 9 更高效的回收
  • 250+增强
  • 180+bug修复
  • https://docs.oracle/en/java/javase/12/gctuning

4.5常用垃圾回收器总结

Serial(串行收集器)
  • 特性:单线程,stop the world,采用复制算法
  • 应用场景:jvm在Client模式下默认的新生代收集器
  • 优点:简单高效
ParNew
  • 特点:是Serial的多线程版本,采用复制算法
  • 应用场景:在Server模式下常用的新生代收集器,可与CMS配合工作
Parallel Scavenge
  • 特点:并行的多线程收集器,采用复制算法,吞吐量优先,有自适应调节策略
  • 应用场景:需要吞吐量大的时候
SerialOld
  • 特点:Serial的老年代版本,单线程,使用标记-整理算法
Parallel Old
  • Parallel Scavenge的老年代版本,多线程,标记-整理算法
CMS
  • 特点:以最短回收停顿时间为目标,使用标记-清除算法
  • 过程:
    • 初始标记:stop the world 标记GC Roots能直接关联到的对象
    • 并发标记:进行GC Roots Tracing
    • 重新标记:stop the world;修正并发标记期间因用户程序继续运作而导致标记产生变动的 那一部分对象的标记记录
    • 并发清除:清除对象
  • 优点:并发收集,低停顿
  • 缺点:
    • 对CPU资源敏感
    • 无法处理浮动垃圾(并发清除 时,用户线程仍在运行,此时产生的垃圾为浮动垃圾)
    • 产生大量的空间碎片
G1
  • 特点:
    • 面向服务端应用,将整个堆划分为大小相同的region。
    • 并行与并发
    • 分代收集
  • 空间整合:从整体看是基于“标记-整理”的,从局部(两个region之间)看是基于“复制”的。
  • 可预测的停顿:使用者可明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
  • 执行过程:
    • 初始标记:stop the world 标记GC Roots能直接关联到的对象
    • 并发标记:可达性分析
    • 最终标记:修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录
    • 筛选回收:筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划

5. 垃圾回收调优

预备知识

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

5.1 调优领域

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

5.2 确定目标

  • 【低延迟】还是【高吞吐量】,选择合适的回收器
  • CMS,G1,ZGC
  • ParallelGC
  • Zing

5.3 最快的 GC

答案是不发生 GC

  • 查看 FullGC 前后的内存占用,考虑下面几个问题
    • 数据是不是太多?
      • resultSet = statement.executeQuery(“select * from 大表 limit n”)
      • 查数据库时禁用*,用多少数据查多少数据
    • 数据表示是否太臃肿?
      • 对象图
      • 对象大小 16 Integer 24 int 4
    • 是否存在内存泄漏?
      • static Map map =
      • 第三方缓存实现

5.4 新生代调优

  • 新生代的特点

    • 所有的 new 操作的内存分配非常廉价
      • TLAB thread-local allocation buffer
    • 死亡对象的回收代价是零
    • 大部分对象用过即死
    • Minor 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.

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

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

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

    -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:CMSInitiatingOccupancyFraction=percent //空间占用达到老年代总内存多少百分比时开始垃圾full gc回收(75-80之间)

5.6 案例

  • 案例1 Full GC 和 Minor GC频繁

​ 解决方案:可以适当调大新生代的内存大小,同时将幸存区的大小调大,适当设置阈值

  • 案例2 请求高峰期发生 Full GC,单次暂停时间特别长 (CMS)

查看gc日志,有可能是出现在Remark阶段,打开-XX:+CMSScavengeBeforeRemark

  • 案例3 老年代充裕情况下,发生 Full GC (CMS jdk1.7)

参考博客:
https://blog.csdn/weixin_43508555/article/details/105350303

https://blog.csdn/LiBoom/article/details/81077897

本文标签: 垃圾系列JVM