快速了解GC有这篇文章就够了!

一、什么是GC

GC(Garbage Collection)垃圾收集,回收垃圾,释放内存(那Java中的垃圾是什么呢?垃圾一般是没有被任何对象引用的对象),Java 提供的 GC 功能可以自动监测对象是否过期(超过生命周期)从而达到自动清除过期对象回收内存的目的。

二、为什么要了解GC

在实际项目中,可能会遇到内存泄漏的问题,我们需要对项目业务进行合理的内存分配,需要对其进行监控和调节,在这之前,我们需要了解GC。

三、对象被判定为垃圾的标准

前边已经说过了,没有被任何对象引用的对象被判定为垃圾

四、判断对象是否为垃圾的算法

  • 引用计数算法
  • 可达性分析算法
4.1、引用计数算法
  • 通过判断对象的引用数量来决定对象是否可以被回收
  • 每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1
  • 任何引用计数为0 的对象实例可以被当做垃圾收集
  • 优点:执行效率高,程序执行受影响小
  • 缺点:无法检测出循环引用的情况,导致内存泄漏
    比如下边这个例子:
package com.mtli.jvm.gc;

/**
 * @Description:
 * @Author: Mt.Li
 * @Create: 2020-04-28 13:38
 */
public class MyObject {
    public MyObject childNode;
}

package com.mtli.jvm.gc;

/**
 * @Description:
 * @Author: Mt.Li
 * @Create: 2020-04-28 13:39
 */
public class ReferenceCountProblem {
    public static void main(String[] args) {
        MyObject object1 = new MyObject();
        MyObject object2 = new MyObject();

        // 循环引用
        object1.childNode = object2;
        object2.childNode = object1;
    }
}

这种情况下,引用是无法为0的,无法检测,这也是一个致命短板

4.2、可达性分析算法
  • 主流的Java垃圾收集器采用的是可达性分析算法。它通过判断对象的引用链是否可达来决定对象是否可以被回收。
  • 程序把所有的引用关系看做一张有向图,将名称为gc root 的对象作为起始点,从这些节点开始向下搜索,其所走过的路径被称为引用链(reference chain),路径上所有遇到的对象都标记为存活,当gc root到某个对象没有任何引用链时,就是说gc root到对象是不可达的,那么该对象也就被标记为垃圾了。
    在这里插入图片描述
    红色部分是GC Root可达的对象,标记存活,绿色的是不可达的,即垃圾对象。
    那么问题来了什么对象可以作为GC Root的对象?
  • 虚拟机栈中引用的对象(栈帧中的本地变量表)
  • 方法区中的常量引用的对象,即常量保存的是该对象的地址
  • 方法区中的类静态属性引用的对象(类似于常量情况)
  • 本地方法栈中INI(Native方法)的引用对象
  • 活跃线程的引用对象也可以作为GC Root

五、垃圾回收算法

说完如何判定垃圾的算法后,当然是要进行垃圾回收了,这又有哪些算法呢?

  • 标记-清除算法(Mark and Sweep)
  • 复制算法(Copying)
  • 标记-整理算法(Compacting)
5.1、标记-清除算法
  • 标记:从根集合进行扫描,对存活的对象进行标记
  • 清除:对堆内存从头到尾进行线性遍历,回收不可达对象内存
    在这里插入图片描述
    缺点:
  • 容易造成大量碎片
    • 上图中,我们回收掉不可达对象之后,剩下的存活对象处于不连续的内存空间中,造成碎片化,当我们想要插入一个新的对象,并且需要的连续内存较大(比如需要3个单位)图中最多只有两个单位,那么就要进行新一轮的垃圾回收,这样也会导致新的碎片出现,也会浪费时间,降低效率。
5.2、复制算法
  • 分为对象面和空闲面

  • 对象在对象面上创建

  • 当被定义的对象面用完的时候,将还存活的对象复制到空闲面,然后将该对象面所有对象内存清除

  • 顺序分配内存,简单高效

  • 解决碎片化问题

  • 适用于对象存活率低的场景,如年轻代
    在这里插入图片描述
    在这里插入图片描述
    优点很明显,解决了碎片化的问题,复制算法不使用链表,分块直接用连续的内存空间,放入新对象只需要找到满足该对象内存大小的内存块放入即可,省去了大量时间。
    缺点:

  • 这种算法,需要留出一半的空间给空闲面,造成堆使用率低下

  • 不适合高存活率的场景,那样的话每次复制要复制很多,性能上不允许,故不适合老年代

5.3、标记-整理算法
  • 标记:从根集合进行扫描,对存活的对象进行标记
  • 清除:移动所有存活的对象,且按照内存地址次序一次排列,然后将末端内存地址以后的内存全部回收
  • 在标记-清除算法上增加了对象的移动,增加了成本,却解决了内存碎片的问题
  • 适用于存活率极高的场景,如老年代
    在这里插入图片描述
5.4、主流回收算法——分代收集算法(Generation Collector)
  • 按照对象生命周期的不同划分区域以采用不同的垃圾回收算法
  • 目的:提高JVM的回收效率
  • 年轻代采用复制算法
  • 老年代采用标记-清除算法、**标记-整理算法

JDK1.8中的分代
在这里插入图片描述
JDK1.6中的分代
在这里插入图片描述

六、年轻代的垃圾收集

年轻代占堆空间的1/3,是为了尽可能快速地收集掉那些生命周期短的对象,它有如下分区

  • Eden区(新创建的对象一般放在这里),占新生代的8/10
  • 两个Survivor区,一个为From区一个是To区(不是固定的,根据GC执行而定的),是为了配合复制算法

下面我们用图来解释:

我们假设Eden能存4个对象,Survivor分别能存3个对象
在这里插入图片描述
1.现在Eden(临时急救中心)中有4个对象,其中三个挂了,需要回收尸体,空出位置,另外一个需要转移到医院,现在有S0和S1两个医院,规定是必须有个医院作为待命的to随时准备接收病人,另一个则作为接诊区from如下图,S0转移进一个则S0此时是from,S1是to,且病人编号+1
在这里插入图片描述
2.现在Eden又来了四个急救病人,有俩没挺住,另外两个存活的要送医院,为了方便接收和明示病人批次病人编号都+1,我们要将S0医院的转移到to中,刚进来的两个也要转到to中,两家医院的标识对换,病人编号都+1(以此类推)。复制完后S0是to,S1是from
在这里插入图片描述
3.没想到坏事并行啊,这下又有四个人进到Eden临时急救中心,可惜的是只有一个活了下来,祸不单行,医院S1的有一个没救过来,挂了。刚好都给他们转到S0去,S1留着待命。此时S0变成from,S1变成to
在这里插入图片描述
最后情况(Eden可以是有对象的):
在这里插入图片描述
(回收Eden和Survivor区的过期对象的过程被称为Minor GC

当编号达到某个值时(由 -XX:MaxTenuringThreshold定义,可以是15或者自定义),需要转移到老年代去。当然情况不是唯一,有的对象较大,Eden装不下或者S区装不下,经过上面的程序,容纳不下,就会进入到老年代。

对象如何晋升到老年代
  • 经历一定Minor次数依然存活的对象
  • Survivor区或者Eden区装不下的对象
  • 新生成的大对象( -XX: +PretenuerSizeThreshold)
常用的调优参数
  • -XX:SurvivorRatio : Eden和Survivor的比值,默认8:1
  • -XX:NewRatio : 老年代和年轻代内存大小的比例,比如2:1
  • -XX:MaxTenuring Threshold : 对象从年轻代晋升到老年代经过GC次数的最大阈值

七、老年代的垃圾收集

老年代(2/3堆空间):

  • 存放生命周期较长的对象
  • 标记-清理算法
  • 标记-整理算法
  • 垃圾回收:Full GC和Major GC。major gc 通常与Full GC等价,只是由于HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,所以说到Major的时候,要询问清楚是full gc还是old gc

Full GC(全局范围的GC)

Full GC比Minor GC慢很多,它是全局范围的GC,清理年轻代、老年代、元空间
触发条件:

  • 老年代空间不足
  • 永久代空间不足(jdk1.7之前)
  • CMS GC时出现promotion failed ,concurrent mode failure
  • Minor GC晋升到老年代的平均大小大于老年代的剩余空间
  • 调用System.gc(),对老年代和新生代进行回收,具体还是由虚拟机执行

可以这么理解,当触发老年代GC时,通常也会触发年轻代GC回收,也就是对整个堆进行垃圾回收,即full GC。

Stop-the-World

  • JVM由于要执行GC而停止应用程序的执行
  • 任何一种GC算法中都会发生
  • 多数GC优化用减少Stop-the-world发生的时间来提高程序性能

SafePoint

  • 分析过程中对象引用关系不会发生变化的点

  • 产生Safepoint的地方:方法调用;循环跳转;异常跳转等等

  • 一旦GC发生,所有的线程跑到安全点再停顿下来,如果某个线程不在安全点,就等其到达安全点,再GC

  • 安全点数量要适中,太少会让GC等待很久,太多会增加程序运行的负荷

八、常见的垃圾收集器

JVM运行模式

  • Server,启动较慢,长期稳定运行后,程序运行速度要比Client快。因为采用重量级虚拟机,其中对程序有更多优化
  • Client,启动较快

可以在cmd中查询java -version
在这里插入图片描述
默认是以Server启动

垃圾收集器之间的联系

(借图)
在这里插入图片描述

8.1、年轻代常见的垃圾收集器
Serial收集器(-XX:+UseSerialGC,复制算法)
  • 1.3版本之前年轻代的唯一选择

  • 单线程收集,进行垃圾收集时,必须暂停所有工作线程

  • 简单高效,Client模式下默认的年轻代收集器

ParNew收集器(-XX:+UseParNewGC,复制算法)
  • 多线程收集,其余的行为、特点和Serial收集器一样
  • 单核CPU情况下执行效率不如Serial,在多核下执行才有优势
  • 默认开启线程数和CPU核数相同,可以加限制
Parallel Scavenge收集器(-XX:+UseParallelGC,复制算法)

ParallelScavenge收集器的目标则是达到一个可控制的吞吐量

  • 吞吐量=运行用户代码时间/(运行用户代码+垃圾收集时间)
  • 比起其他的收集器关注用户线程停顿时间,它更关注系统的吞吐量
  • 在多核下执行才有优势,Server模式下默认的年轻代收集器
  • 调优参数(-XX:+UseAdaptiveSizePolicy),如果开启 AdaptiveSizePolicy,则每次 GC 后会重新计算 Eden、From 和 To 区的大小,计算依据是 GC 过程中统计的 GC 时间、吞吐量、内存占用量。被称为 GC自适应的调节策略
  • -XX:GCTimeRatio, 垃圾收集执行时间占应用程序执行时间的比例,计算方法:1 / (1 + n),n就是参数设置的值,默认为99,默认占比1%,如果设置成19,则默认占比5%
  • -XX:MaxGCPauseMillis,控制最大垃圾收集停顿时间,大于0的毫秒数;MaxGCPauseMillis设置得稍小,停顿时间可能会缩短,但也可能会使得吞吐量下降;因为可能导致垃圾收集发生得更频繁
8.2、老年代常见收集器
Serial Old收集器( -XX:USeSerialOldGC,标记-整理算法)
  • 单线程收集,进行垃圾收集时,必须暂停所有工作线程
  • 简单高效,Client模式下默认的老年代收集器
Parallel Old

和Parallel Scavenge配套使用(1.6~1.8是Parallel Old,1.5及之前是Serial Old配合 PS),使用的是标记-整理算法。

CMS收集器(-XX:+UseConcMarkSweepGC,标记-清除算法)(老年代收集器的半壁江山)
  • 初始标记:stop-the-world(std),虚拟机会停顿正在执行的任务,从垃圾回收的根对象开始,只扫描到能够和根对象关联的对象,并做标记,并不会耗时太多,很快就能完成。
  • 并发标记:并发追溯标记,程序不会停顿,该阶段紧随初始标记,会继续向下追溯标记,不会停止应用线程,两者并发执行
  • 并发预处理:查找执行并发标记阶段从年轻代晋升到老年代的对象,减少下一个阶段——重新标记的工作量(重新标记会暂停虚拟机)
  • 重新标记:暂停虚拟机,扫描CMS堆中的剩余对象。从跟对象开始一直向下追溯所有对象,比较耗时
  • 并发清理:清理垃圾对象,程序不会停顿
  • 并发重置:重置CMS收集器的数据结构

缺点:

  • 如果在并发标记后产生的垃圾,只能等待本次清理完毕
  • 最大的问题是本收集器采用的是标记-清除算法,会产生比较多的不连续的碎片空间,如果碎片空间过多,但是又需要一块较大的连续空间,这时候只能进行一次GC了
G1收集器(-XX:+UseG1GC,复制+标记-整理算法)
  • 并行和并发
  • 分代收集
  • 空间整合(用标记-整理算法)
  • 可预测的停顿
  • 将整个Java内存划分为多个大小相等的Region
  • 年轻代和老年代不再物理隔离。堆内存被划分为多个大小相等的 heap 区,每个heap区都是逻辑上连续的一段内存(virtual memory). 其中一部分区域被当成老一代收集器相同的角色(eden, survivor, old), 但每个角色的区域个数都不是固定的。这在内存使用上提供了更多的灵活性。
  • 相比CMS,G1消除了内存碎片的问题

详解及其执行过程可参考G1垃圾收集器入门

九、关于Java中的强引用、软引用、弱引用、虚引用

强引用(Strong Reference)
  • 最普遍的引用:Object obj = new Object()
  • GC哪怕是抛出OOM终止程序也不会回收具有强引用的对象
  • 通过将对象设置为null来弱化引用,使其被回收(等待其超过生命周期)
软引用(Soft Reference)
  • 对象处在有用但非必须的状态

  • 只有当内存空间不足时,GC会回收该引用的对象的内存

  • 可以用来实现高速缓存

    • 使用方法:

    • String str = new String("abc"); // 强引用
      SoftReference<String> softRef = new SoftReference<String>(str); // 软引用
      
弱引用(Weak Reference)
  • 非必须的对象,比软引用更弱一些

  • GC时会被回收(无论内存资源是否紧缺)

  • 被回收的概率也不大,因为GC线程优先级比较低

  • 适用于引用偶尔被使用且不影响垃圾收集的对象

  • String str = new String("abc"); // 强引用
    WeakReference<String> weakRef = new WeakReference<String>(str); // 弱引用
    

虚引用(PhantomReference)

  • 不会决定对象的生命周期

  • 任何时候都可能被垃圾收集器回收

  • 跟踪对象被垃圾收集器回收的活动,起哨兵作用,GC在回收对象时,会先判断该对象是否有虚引用,如果有,则加入到引用队列中,程序可以判断,引用队列是否已经加入虚引用来了解被引用的对象是否被回收

  • 必须和引用队列ReferenceQueue联合使用

  • String str = new String("abc");
            ReferenceQueue queue = new ReferenceQueue();
            PhantomReference ref = new PhantomReference(str, queue);
    

综上,Java中的引用强弱级别为:

强引用>软引用>弱引用>虚引用

引用类型被垃圾回收时间用途生存时间
强引用从来不会对象的一般状态JVM停止运行时终止
软引用在内存不足时对象缓存内存不足时终止
弱引用在垃圾回收时对象缓存gc运行后终止
虚引用Unknown标记、哨兵Unknown

以上均为个人理解,如有不足,请及时在评论区提出,谢谢。

  • 14
    点赞
  • 102
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值