首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【原创】JVM 的垃圾回收与算法

【原创】JVM 的垃圾回收与算法

作者头像
良月柒
发布2020-04-14 14:43:21
3450
发布2020-04-14 14:43:21
举报

1.GC 思维导图

需要xmind文件可以从公众号加我微信。

2.如何确定垃圾

引用计数法

在 Java 中,引用和对象是有关联的,如果要操作对象则必须用引用进行。

每个对象有一个引用计数器,当对象被引用一次则计数器加 1,当对象引用失效一次则计数器减 1,对于计数器为 0 的对象意味着是垃圾对象,可以被 GC 回收。

画外音:循环引用的问题 先来看一段代码

如果采用引用技术算法,上述代码中 ojb1 和 obj2 指向的对象已经不可能再被访问,彼此互相引用对方导致引用技术都不为 0,最终无法被 GC 回收,可达性算法能解决这个问题。 看下这个过程中的 Java 内存模型:

再回到前面代码 GcDemo 的 main 方法共分为 6 个步骤:

  • Step1:GcObject 实例 1 的引用计数加 1,实例 1 的引用计数 = 1;
  • Step2:GcObject 实例 2 的引用计数加 1,实例 2 的引用计数 = 1;
  • Step3:GcObject 实例 2 的引用计数加 1,实例 2 的引用计数 = 2;
  • Step4:GcObject 实例 1 的引用计数加 1,实例 1 的引用计数 = 2;

执行完 Step 4,则 GcObject 实例 1 和 实例 2 的引用计数都等于 2。 继续往下执行:

  • Step5:栈帧中 obj1 不再指向 Java 堆,GcObject 实例 1 的引用计数减 1,结果为 1;
  • Step5:栈帧中 Obj2 不再指向 Java 堆,GcObject 实例 2 的引用计数减 1,结果为 1。

到此,发现 GcObject 实例 1 和 实例 2 的计数引用都不为 0,那么如果采用的引用计数算法的话,那么这两个实例锁占的内存将得不到释放,这边产生了内存泄漏。

可达性分析

为了解决引用计数法的循环引用问题,Java 使用了可达性分析的方法。这是目前主流的虚拟机都是采用 GC Roots Tracing 算法,比如 Sun 的 Hotspot 虚拟机便是采用该算法。

该算法那的核心是,通过一系列的“GC roots”对象作为起始点。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。这里涉及两个概念,一是 GC Roots,一是可达性。

画外音:不可达的对象也并非是‘非死不可’的,暂时是‘缓刑’阶段。 要真正判断一个对象死亡要经历两次标记过程:如果对象在进行根搜索后发现对象不可达,那它将会进行被第一次标记并且进行筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖 finalize() 方法或者 finalize() 方法已经被虚拟机掉用过,这两种情况都视为‘没有必要执行’。 如果对象被认为有必要执行 finalize() 方法,那么这个方法会被放置在一个名为 F-Queue 的队列之中,并在稍后由一条由虚拟机自动建立的、低优先级的 Finalizer 线程去执行。这里的‘执行’也只是指虚拟机会触发这个方法,但并不承诺一定会执行。 finalize() 方法是对象逃脱死亡命运的最后一次机会,稍后 GC 会对 F-Queue 中的对象进行第二次小规模的标记,如果对象在finalize() 中重新与引用链上的任何一个对象建立了关联,就会被移出‘即将回收’集合,如果没有移出,那就真的离死亡不远了。 finalize() 方法只会被系统自动调用一次。

可以作为 GC Roots 的对象

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中的静态变量和常量所引用的对象
  • 本地方法栈JNI中的引用的对象。

关于可达性的对象,便是能与 GC Roots 构成连通图的对象,如下图:

从上图,reference1、reference2、reference3 都是 GC Roots,可以看出:

  • reference1 -> 对象实例1;
  • reference2 -> 对象实例2;
  • reference3 -> 对象实例4;
  • reference3 -> 对象实例4 -> 对象实例 6;

对象实例 1、2、4、6 都是具有 GC Roots 可达性,也就是存货对象,不能被 GC 回收的对象。

而对于对象实例 3、5 直接虽然连通,但并没有任何一个 GC Roots 与之相连,这便是 GC Roots 不可达的对象,需要 GC 回收的垃圾对象。

画外音:再回头看看前面的代码实例(GcDemo),GcObject 实例 1 和 实例 2 虽然在引用计数上都不为 0,但从可达性算法来看,都是 GC Roots 不可达的对象。对于对象之间循环引用的情况,引用计数算法,GC 无法回收这两个对象,而可达性算法则可以正确回收。

3.标记清除算法(Mark - Sweep)

这是最基础的垃圾回收算法,分为两个阶段,标记和清除

标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。

从图中我们就可以发现,该算法最大的问题是内存碎片化严重,后续可能发生大对象不能找到可用空间的问题。

4.复制算法(copying)

为了解决 Mark - Sweep 算法内存碎片化的缺陷而被提出的算法。

按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清理掉,如图:

这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一半。

且存活对象增多的话,Copying 算法的效率会大大降低。

5.标记整理算法(Mark-Compact)

结合了以上两个算法,为了避免缺陷而提出。

标记阶段和 Mark-Sweep 算法相同,标记后不是清理对象,而是将存活对象移向内存的一端,然后清除端边界外的对象。

6.分代收集算法

分代收集法是目前大部分 JVM 所采用的方法, 其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将 GC 堆划分为老年代(Tenured/Old Generation)和新生代(Young Generation)。

老年代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。

新生代与复制算法

目前大部分 JVM 的 GC 对于新生代都采用 Copying 算法,因为新生代中每次来及回收都要回收大部分对象,即要复制的操作比较少,但通常并不按照 1:1 来划分来划分新生代。

画外音:新生代 GC(Minor GC) 指发生在新生代的垃圾收集动作,所有的 Minor GC 都会触发全世界的暂停(stop-the-world),停止应用程序的线程,不过这个过程非常短暂。因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。 当年轻代满时就会触发Minor GC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC。

一般将新生代划分为一块较大的 Eden 空间和两个较少的 Survivor 空间(From Space,To Space),每次使用 Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor 空间中。

画外音:Eden 占新生代 8/10 空间、两个 Survivor 分别占 1/10 空间。

在执行机制上 JVM 提供了串行 GC(SerialGC)、并行回收 GC(ParallelScavenge)和并行 GC(ParNew)。

老年代与标记整理算法

老年代因为每次只回收少量对象,因而采用 Mark-Compact 算法。

画外音:老年代 GC(Major GC / Full GC) 指发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,ParallelScavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程) 。MajorGC 的速度一般会比 Minor GC 慢 10倍以上。

在执行机制上 JVM 提供了串行 GC(Serial MSC)、并行 GC(Parallel MSC) 和并发 GC(CMS)。

7.垃圾回收的过程

1. Java 虚拟机提到过处于方法区的永久代(Permanet Generation),它用来存储 Class 类,常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。

画外音:1.8 开始已经将 永久代 移除,改为了元空间。

2. 对象的内存分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(Survivor 目前存放对象的那一块),少数情况会直接分配到老年代。

3. 当新生代的 Eden Space 和 From Space 空间不足时就会发生一次 Minor GC,进行 GC 后,Eden Space 和 From Space 区的存活对象就会被挪到 To Space,然后 Eden Space 和 From Space 进行清理。

4. 如果 To Space 无法足够存储某个对象,则将这个对象存储到老年代。

5. 在进行 GC 后,使用的便是 Eden Space 和 To Space 了,如此反复循环。

6. 当对象在 Survivor 区躲过一次 GC 后,其年龄就会 +1。默认情况下年龄到达 15 的对象会被移到老年代中。

8.垃圾收集器(简略版)

新生代:

串行GC(SerialGC)

在整个扫描和复制过程采用单线程的方式来进行,适用于单CPU、新生代空间较小及对暂停时间要求不是非常高的应用上,是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定。

并行回收GC(ParallelScavenge)

在整个扫描和复制过程采用多线程的方式来进行,适用于多CPU、对暂停时间要求较短的应用上,是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。

并行GC(ParNew)

与老年代的并发GC配合使用。

老年代:

串行GC(Serial MSC)

client模式下的默认GC方式,可通过-XX:+UseSerialGC强制指定。每次进行全部回收,进行Compact,非常耗费时间。

并行GC(Parallel MSC)(吞吐量大,但是GC的时候响应很慢)

server模式下的默认GC方式,也可用-XX:+UseParallelGC=强制指定。可以在选项后加等号来制定并行的线程数。

并发GC(CMS)(响应比并行gc快很多,但是牺牲了一定的吞吐量)

使用CMS是为了减少GC执行时的停顿时间,垃圾回收线程和应用线程同时执行,可以使用-XX:+UseConcMarkSweepGC=指定使用,后边接等号指定并发线程数。

CMS每次回收只停顿很短的时间,分别在开始的时候(Initial Marking),和中间(Final Marking)的时候,第二次时间略长。

CMS一个比较大的问题是碎片和浮动垃圾问题(Floating Gabage)。碎片是由于CMS默认不对内存进行Compact所致,可以通过-XX:+UseCMSCompactAtFullCollection。

不管做什么,只要坚持下去就会不一样!

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-04-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员的成长之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.GC 思维导图
  • 2.如何确定垃圾
  • 3.标记清除算法(Mark - Sweep)
  • 4.复制算法(copying)
  • 5.标记整理算法(Mark-Compact)
  • 6.分代收集算法
  • 7.垃圾回收的过程
  • 8.垃圾收集器(简略版)
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档