JVM 手把手保姆级教程(2/3):垃圾回收

想拿高工资,想成为一名合格又优秀的java高级攻城狮,对于JVM的学习是必不可少的。
我本人找过很多课程,学过很多遍,却总是感觉学不太明白,感觉少点什么,我相信很多小伙伴会和我有一样的经历。
还好现在找到一个比较易于理解却不臃肿的视频教程,本笔记就是基于视频教程以及对视频中不易理解的部分进行多方咨询求证,力求写出一篇易于理解,帮助小伙伴们成长的教程,在这里非常感谢黑马的课程。视频:黑马程序员JVM完整教程

第一篇:JVM 手把手保姆级教程(1/3):内存结构
第二篇:JVM 手把手保姆级教程(2/3):垃圾回收
第三篇:JVM 手把手保姆级教程(3/3):类加载与字节码技术&内存模型

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

1.1 引用计数法

定义

给对象的引用进行计数,每当有一个地方引用该对象时,计数器就加1,当引用失效时,计数器就减1。当计数器为0的时候就代表该对象没有被引用,就会被垃圾回收。

弊端
循环引用,当A对象和B对象互相引用,而且AB对象都没有被其他对象引用时,A和B的计数器永远都是1,永远不会被回收。因此,Java虚拟机并未使用引用计数法。

1.2 可达性分析算法

  • Java虚拟机中的垃圾回收器通过可达性分析算法来探索所有存活的对象。
  • 扫描堆中的对象,看能否沿着GC Root对象为起点的引用链找到该对象,找不到,表示可以回收。
  • 哪些对象可以作为GC Root?
    • 虚拟机栈中引用的对象
    • 本地方法栈中 JNI(即一般说的Native方法)引用的对象
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 被同步锁synchronized持有的对象

举个栗子叭
比如我们提起盘子里的一串葡萄,我们手提的部位就是GC Root,掉在盘子里的葡萄就相当于不在引用链上的对象,也就是上述可以被回收的对象。

1.3 四种引用

1.3.1 强引用

强引用是使用最普遍的引用,也是最常用的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。如下:Object strongReference = new Object();
当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError异常,也不会回收具有强引用的对象。

1.3.2 软引用

在垃圾回收时,如果内存空间不足了,就会回收软引用对象,如果内存空间充足就不会回收。
一般会用于缓存,就算被回收也不会对系统造成影响。

1.3.3 弱引用

在垃圾回收时,无论内存是否充足,都会回收弱引用对象。
与软引用的区别在于:软引用对象只有内存不足才会被回收,而弱引用对象一旦被发现都会被回收。

1.3.4 虚引用

虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用并不会决定对象的生命周期。仅仅在被垃圾回收时收到一个系统通知,一般用于记录对象的垃圾回收。

1.3.5 总结

在这里插入图片描述
Java中4种引用的级别和强度由高到低依次为:强引用 -> 软引用 -> 弱引用 -> 虚引用

2. 垃圾回收算法

2.1 标记清除

标记清楚算法分为两个阶段:标记和清除。
标记:标记出GC Root引用链不可达对象。
清除:将标记的对象进行垃圾回收,释放内存。

原理如下图:蓝色代表强引用对象(即不可回收的对象),灰色代表不可达的对象(即可回收的垃圾对象),白色区域代表空闲内存。由下图可以看出灰色部分先被标记为垃圾对象,然后被清释放内存空间。
在这里插入图片描述
由上图可以看出,标记清除算法,能够快速的标记出垃圾对象并清除,但是没有对释放的内存空间进行整理,导致内存空间不连续,产生内存碎片(假如上图释放的三个内存空间分别为3M、2M、1M,这时又有一个新的对象需要5M的内存空间,那么这三个不连续的内存空间都放不下该对象,实际上被释放的总内存为6M是大于5M的,所以这三个内存空间就变成了不可用的内存碎片)。

优点:

  • 速递快

缺点:

  • 会产生内存碎片

2.2 标记整理

标记整理算法也分为两个阶段:标记和整理。与标记清除算法的区别就是,它将释放的内存空间进行里整理,所以被释放的内存空间都是连续的,不会产生内存碎片,当然整理内存也是有点耗时的,所以速度没有标记清除算法快。如下图所示:
在这里插入图片描述
优点:

  • 不会产生内存碎片

缺点:

  • 速度比较慢

2.3 复制

复制算法是先将内存空间分为两部分,这里就用A(系统使用的内存空间)和B(空闲的内存空间)表示两个内存空间。首先标记出A空间的垃圾对象,将标未标记的使用中的对象放到B空间,然后清除A空间的垃圾对象,最后把A空间和B空间调换一下位置(A空间变成空闲的内存空间,B空间变成系统使用的内存空间)。如下图:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
优点:

  • 不会产生内存碎片

缺点:

  • 占用双倍内存空间

3. 分代垃圾回收

3.1 图解

先看一张清晰明了的图,了解一下分代垃圾回收是怎么回事。
请添加图片描述
根据上图来个小结:

  • 对象首先分配在伊甸园区。
  • 伊甸园空间不足时,触发Minor GC,伊甸园和From存活的对象会复制到To中,存货的对象年龄+1并且交互From和To。
  • Minor GC 会引发 stop the world,暂停其他线程,等垃圾回收结束后,恢复用户线程运行。
  • 当幸存区对象的寿命超过阈值时,会晋升到老年代,最大的寿命是15次,如果内存紧张,不达到阈值也会晋升到老年代。
  • 当老年代空间不足时,会先触发Minor GC,如果空间仍然不足,那么就触发 Full GC ,Full GC也会引发stop the world,只不过停止的时间更长,如果空间还是不足就会报OOM异常。

3.2 相关JVM参数

含义 参数
对初始大小 -Xms
对最大大小 -Xmx 或 -XX:MaxHeapSize=size
新生代大小 -Xmn 或 -XX:NewSize=size + -XX:MaxNewSize=size
幸存区比例(动态) -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
幸存区比例 -XX:SurvivorRatio=ratio (默认ratio是8)
晋升阈值(晋升到老年代的阈值) -XX:MaxTenuringThreshold=threshold
晋升详情 -XX:+PrintTenuringDistribution
GC详情 -XX:+PrintGCDetails -verbose:gc
FullGC 前 MinorGC (默认是打开的) -XX:+ScavengeBeforeFullGC

3.3 代码演示

public class Code_10_GCTest {
    
    
    private static final int _512KB = 512 * 1024;
    private static final int _1MB = 1024 * 1024;
    private static final int _6MB = 6 * 1024 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    private static final int _8MB = 8 * 1024 * 1024;

    // -Xms20m -Xmx20m -Xmn10m -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
    public static void main(String[] args) {
    
    
        List<byte[]> list = new ArrayList<>();
        list.add(new byte[_6MB]);
        list.add(new byte[_512KB]);
        list.add(new byte[_6MB]);
        list.add(new byte[_512KB]);
        list.add(new byte[_6MB]);
    }

}

首先配置JVM 参数, 然后main函数里先不加代码运行看结果,再然后一行行添加代码分析打印的结果。

4. 垃圾回收器

垃圾回收器可以大致分为三大类串行的垃圾回收器、吞吐量优先的垃圾回收器和响应时间优先的垃圾回收器。

相关概念:

  • 并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
  • 并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个 CPU 上
  • 吞吐量:即 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )),也就是。例如:虚拟机共运行 100 分钟,垃圾回收器花掉 1 分钟,那么吞吐量就是 99% 。

4.1 串行

特点

  • 单线程
  • 适合堆内存少,CPU核数少的机器

涉及的垃圾回收器:

  • Serial :新生代垃圾回收器,采用复制算法。
  • SerialOld:老年代垃圾回收器,采用标记整理算法。

图解
在这里插入图片描述
相关JVM参数

#指定新生代和老年代都用串行回收器
-XX:+UseSerialGC

4.2 吞吐量优先

特点

  • 多线程
  • 适合堆内存大,多核CPU
  • 让单位时间内STW的时间最短

涉及的垃圾回收器(JDK8默认):

  • Parallel Scavenge :简称Parallel回收器,新生代垃圾回收器,采用复制算法。
  • Parallel Old:老年代垃圾回收器,采用标记整理算法。

图解
在这里插入图片描述
相关JVM参数

#手动指定新生代使用Parallel GC,开启后也会自动开启老年代使用ParallelOld GC,两者相互激活
-XX:+UseParallelGC

#手动指定老年代使用ParallelOld GC,开启后也会自动开启新生代使用Parallel GC,两者相互激活
-XX:+UseParallelOldGC

#自适应大小调整策略(调整新生代大小,伊甸园和两个幸存者区)
-XX:+UseAdaptiveSizePolicy

#用于调整吞吐量,垃圾回收占用的时间不能超过1/(1+radio),超过了就会调整堆的大小(一般会增大),取值范围(0,100),默认值99
-XX:GCTimeRatio=ratio 

#最大暂停毫秒数,默认200ms。为了尽可能把时间调整到该参数的值以内,JVM可能会调整堆的大小或者其他的一些参数,所以这个参数要谨慎使用。
-XX:MaxGCPauseMillis=ms

#并行垃圾回收器的线程数,最好与CPU个数相等,避免设置线程过多争抢CPU资源。默认:1->cpu小于等于8个,n=cpu个数;2->cpu大于8个,n=3+(5*CPU_COUNT/8)
-XX:ParallelGCThreads=n

4.3 响应时间优先

特点

  • 多线程
  • 适合堆内存大,多核CPU
  • 尽可能让单次的STW(stop the word)时间最短

涉及的垃圾回收器(JDK8默认):

  • ParNew:新生代垃圾回收器,并行垃圾回收器,采用复制算法,相当于Serial收集器的并行版本。
  • CMS(Concurrent Mark Sweep):老年代垃圾回收器,关注点在于低延迟,使用标记清除算法。
  • SerialOld:老年代垃圾回收器,采用标记整理算法。备用垃圾回收器,当CMS出现问题的时候会使用该收集器(详见下面的解释:为什么要备用一个垃圾回收器)。

图解
在这里插入图片描述

  • 初始标记:会出现STW,但是时间特别短暂可以忽略不计,仅仅标记出GC Roots能直接关联到的对象,一旦标记完成就会恢复之前暂停的所有线程,由于GC Roots直接关联的对象比较少,所以速度非常快。
  • 并发标记从GC Roots直接关联的对象开始,遍历整个引用链的过程,这个过程耗时比较长,但是不需要暂停用户线程,用户线程可以与垃圾回收器一起并发运行。
  • 重新标记:会出现STW,由于在并发标记阶段中,用户线程和垃圾回收器线程可以一起或交叉运行,因此并发标记阶段可能有些无法确定是不是垃圾的对象,所以需要重新标记来确定这部分对象。但是STW的时间要远比并发标记的时间短。如果在并发标记阶段出现了新的垃圾(浮动垃圾),那么这些浮动垃圾是无法被标记的,只能等下一次垃圾回收了,这也是CMS垃圾回收器的一个弊端。
  • 并发清除:清除垃圾对象,释放内存空间。由于不需要移动存活对象,所以可以与用户线程并发执行。可能会产生内存碎片(标记清除算法的缺点)。也正因为是并发操作,用户线程还在运行,所以不能使用标记整理算法,因为标记整理算法会移动对象。

为什么要备用一个垃圾回收器?
由于CMS是并发垃圾回收器,某些阶段用户线程和垃圾回收器线程可以并发执行,那么用户线程运行中可能会出现内存不足的情况,这时候就需要启用备用的SerialOld垃圾回收器来暂停一切线程(STW)进行垃圾回收。

相关JVM参数

#手动指定使用CMS垃圾回收器,开启后会自动打开-XX:-UseParNewGC 
-XX:+UseConcMarkSweepGC

#设置堆内存使用率的阈值,一旦达到该阈值便进行垃圾回收
-XX:CMSInitiatingOccupancyFraction=percent

#设置CMS的线程数量,CMS默认启动的线程数是(n+3)/4
-XX:ParallelGCThreads=n

#重新标记前,先对新生代进行垃圾回收
-XX:+CMSScavengeBeforeRemark

4.4 G1

4.4.1 概述

Garbage First:Jdk9默认垃圾回收器,新生代和老年代都可以回收。

使用场景

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

4.4.2 G1 垃圾回收阶段

在这里插入图片描述
主要分为三个阶段(循环执行):

  • Young Collection:新生代垃圾回收
  • Young Collection + Concurrent Mark:新生代垃圾回收同时还有老年代的并发标记
  • Mixed Collection:混合收集

4.4.3 Young Collection

  • 会产生STW

刚开始对象会产生在E(伊甸园)区。
在这里插入图片描述

伊甸园空间不足,会进行一次Minor GC,通过复制算法将存货的对象放到S(幸存者)区。
在这里插入图片描述

当幸存区的空间不足时,也会进行垃圾回收,复合条件的晋升到O(老年)区,存货下来的会复制到另一个S区,E区的一些幸存对象也会复制到另一个S区(和前面讲的新生代垃圾回收机制一样)
在这里插入图片描述

4.4.4 Young Collection + Concurrent Mark

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

4.4.5 Mixed Collection

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

  • 最终标记(Remark)会STW,并发标记后可能会产生新的垃圾,需要STW然后进行最终标记。
  • 拷贝存货对象(Evacuation)会STW,标记完成后会对存活的对象进行Copy。

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

下图红色的O代表老年代垃圾回收,之所以没有回收所有的老年代就是为了尽可能的保证在指定的停顿时间内完成垃圾回收,指定时间内不能回收所有老年代的话,就只回收最有价值的。
在这里插入图片描述

4.4.6 Young Collection跨代引用

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

5. 垃圾回收调优

5.1 调优领域

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

5.2 确定目标

  • 低延迟还是高吞吐量,选择合适的垃圾回收器
    • 确定自己的项目是什么类型的,选择合适的回收器
    • 如果是科学运算类的计算比较多的,选择高吞吐量的垃圾回收器比较合适
    • 如果是互联网电商之类的,选择低延迟优先的比较合适
  • 低延迟的回收器:CMS、G1、ZGC
    • 目前市场上用CMS的比较多,但是到了JDK9默认的就是G1了
  • 高吞吐量的回收器:ParallelGC

5.3 新生代调优

新生代的内存并不是越大越好,太小了也不合适。

  • 新生代内存太大:老年代内存占比有所降低,会更频繁地触发 Full GC。而且触发 Minor GC 时,清理新生代所花费的时间会更长
  • 新生代内存太小:频繁触发 Minor GC ,会 STW ,会使得吞吐量下降
  • 官方推荐新生代内存大小为总堆内存的25%-50%之间

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

##最大幸存阈值
-XX:MaxTenuringThreshold=threshold
##打印晋升详情
-XX:+PrintTenuringDistrubution

5.4 老年代调优

以 CMS 为例:

  • CMS 的老年代内存越大越好
  • 如果系统运行一段时间没有发送Full GC,那么可以不用调优了,否则先尝试调优新生代。
  • 观察发现 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3
#当老年代内存使用达到percent的时候就进行CMS垃圾回收。
-XX:CMSInitiatingOccupancyFraction=percent

5.5 案例分析

案例1:Full GC 和 Minor GC 频繁
GC频繁说明内存空间紧张,大概可以推断出是新生代内存太小了。新生代内存太小会导致一些生存周期短的对象(没达到晋升条件)提前晋升到老年代,从而导致老年代对象过多内存不足发生Full GC。
解决方法:试着增加新生代内存空间,让生成周期较短的对象尽可能的留在新生代。

案例2:请求高峰期发生 Full GC,单次暂停时间特别长(CMS)
查看GC日志,看哪个阶段暂停时间比较长(一般CMS重新标记的时候耗时比较长),如果是重新比较比较耗时的话,可以通过 -XX:+CMSScavengeBeforeRemark 参数,然JVM在重新标记之前先对新生代对象进行垃圾回收。

案例3:老年代充裕情况下,发生 Full GC(jdk1.7)
jdk1.7方法区是在永久代内的,jdk1.8永久代废除方法区在元空间内。
永久代内存不足也会导致Full GC。

猜你喜欢

转载自blog.csdn.net/hpp3501/article/details/120881283