学习笔记系列(5)——GC相关知识

前言

这篇主要关于一些自己整理记忆的GC相关知识,也欢迎大家告诉我有什么更需要补充的点
现在难免文笔稚嫩粗糙,而且笔记也都是以自己能够理解的语言文字方式表达出来用于自己总结梳理。如果有幸被大佬萌看到,非常希望各位大佬可以指出我的不足缺漏,并且可以指导我该往什么方向更深入的学习思考。
掘金——帮助开发者成长的社区,也希望自己能够从小菜鸡一点点进步成长。

GC知识点整理

GC回收堆区、方法区的对象

  • 为什么会有GC

安全性考虑、减少内存泄漏、减少程序员工作量

  • 什么对象会被GC回收
  1. 对象没有引用
  2. 作用域发生未捕获异常
  3. 程序在作用域正常执行完毕
  4. 程序执行system.exit()
  5. 程序发生意外终止

方法区

关于方法区中需要回收的是一些废弃的常量和无用的类。

  1. 废弃的常量的回收。这里看引用计数就可以了。没有对象引用该常量就可以放心的回收了。
  2. 无用的类的回收。

什么是无用的类:
A. 该类所有的实例都已经被回收。也就是 Java 堆中不存在该类的任何实例;
B. 加载该类的 ClassLoader 已经被回收;
C. 该类对应的 java.lang.Class 对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

对于堆中的对象,主要用可达性分析判断一个对象是否还存在引用,如果该对象没有任何引用就应该被回收。
而根据我们实际对引用的不同需求,又分成了4种引用,每种引用的回收机制也是不同的。

JVM GC什么时候执行

年轻代

Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。
minorGC 很多新创建的对象会被分配在这里,他们很快就会变得不可达,会消失 存活率在10%以内
eden区不够存放新对象时,会发生minorGC

简述过程
  1. 绝大多数刚刚被创建的对象会存放在Eden区
  2. Eden区执行第一次GC(Minor GC)之后,存活的对象被移动到其中一个幸存者空间(From Survivor)
  3. 之后,每次Eden区执行GC后,存活的对象被堆积在同一个幸存者空间
  4. 当一个幸存者空间饱和,还在存活的对象会被移动到另一个幸存者空间,然后清空已经饱和的幸存者空间
  5. 当上述步骤重复n次后,存活的对象达到maxTenuringThreshold年龄阈值后(默认15),就会移动到到老年代

老年代

主要存放应用程序中生命周期长的内存对象。
FULL GC 对象从老年代消失的过程。老年代的区域比年轻代要大得多,所以FullGC的次数也少
升到老年代的对象大于老年代的剩余空间时,就会发生FullGC,或小于的时候被HandlePromotionFailure参数强制执行FullGC

  • 怎么会频繁的发生fullgc

滥用static关键字就会导致full gc频繁
多线程情况下负载过大,线程池队列阻塞严重也会导致
或者搞了个无限制的可无穷大的东西常驻内存里也会导致
比如缓存不设置过期且长时间累积

永久代

这个区域发生GC的条件特别苛刻
所有实例被回收
加载该类的classLoader被回收
class对象无法通过任何途径访问

GC算法

引用计数算法(废弃)

给每个对象设置一个计数器,引用到的时候就+1,引用失效的时候就-1,当计数器为0时就被认为是垃圾。
实现简单高效,但是不能解决循环引用问题,计数器增加和减少都带来了不必要的开销。jdk1.1以后就废除了。

根搜索算法

以GC ROOT为起点,顺着引用节点寻找,找到后,继续找他的引用节点,直到所有的引用节点被找完。剩余的节点即没有引用到的节点,被称为无用节点,可以被回收。

作为GCROOT的对象有:

  1. 虚拟机栈中引用的对象
  2. 本地方法栈中引用的对象
  3. 方法区中静态属性引用的对象
  4. 方法区中常量引用的对象

标记-清除算法

从根节点开始扫描,对于存活的对象进行标记,结束之后再对整个空间扫描,没有被标记的对象就会被回收。

标记-清除算法不涉及对对象的移动,仅仅处理存活的对象。
问题:

  1. 如果回收未存活的对象,没有对存活的对象进行整理,就会导致内存碎片。
  2. 标记和清除的效率也不高,清除后会产生大量的不连续空间,如果要分配大空间对象的话,可能会找不到足够的连续空间。

会使用三色抽象、位图标记等技术来提高算法的效率,存活对象较多时较高效。

复制算法

从根集合开始扫描,将存活的对象复制到空闲区间,且严格按照内存地址依次排列,与此同时,GC线程将更新存活对象的内存引用地址指向新的内存地址。扫描结束后对活动区间的对象全部回收。此时空闲区间就会变成活动区间。下一次GC就会重复这个操作。复制算法可以通过碰撞指针的方式进行快速地分配内存

递归算法、迭代算法、解决了前两者递归栈、缓存行等问题的近似优先搜索算法

对于存活对象比较少的时候好用。必须要克服50%内存的浪费。
复制算法弥补了标记/清除算法中,内存布局混乱的缺点
主要用于新生代的回收,因为新生代的对象基本都是朝生夕死,所以两个空间比例大概是8:1

标记整理算法

用于老年代回收
从根集合开始扫描,将存活的对象进行标记,没被标记的对象清除,清除出来的slot位置,由最右侧的存活对象填充。
他解决了内存碎片的问题,但他的效率比标记-清除算法低,是因为之后要整理存活对象的新指向的引用地址吧

主要实现有双指针(Two-Finger)回收算法、滑动回收(Lisp2)算法和引线整理(Threaded Compaction)算法等

image.png

虽然 compaction 与 copying 都涉及移动对象,但取决于具体算法,compaction 可能要先计算一次对象的目标地址,然后修正指针,最后再移动对象。copying 则可以把这几件事情合为一体来做,所以可以快一些。另外,还需要留意 GC 带来的开销不能只看 Collector 的耗时,还得看 Allocator 。如果能保证内存没碎片,分配就可以用 pointer bumping 方式,只需要挪一个指针就完成了分配,非常快。而如果内存有碎片就得用 freelist 之类的方式管理,分配速度通常会慢一些。

垃圾回收器

Serial(-XX:+UseSerialGC)

复制算法 新生代
串行收集器。
Serial收集器是Java虚拟机中最基本、历史最悠久的收集器。在JDK1.3之前是Java虚拟机新生代收集器的唯一选择。目前也是ClientVM下ServerVM 4核4GB以下机器默认垃圾回收器。Serial收集器并不是只能使用一个CPU进行收集,而是当JVM需要进行垃圾回收的时候,需暂停所有的用户线程,直到回收结束。
JVM中文名称为Java虚拟机,因此它像一台虚拟的电脑在工作,而其中的每一个线程都被认为是JVM的一个处理器,因此图中的CPU0、CPU1实际上为用户的线程,而不是真正的机器CPU。
Serial收集器虽然是最老的,但是它对于限定单个CPU的环境来说,由于没有线程交互的开销,专心做垃圾收集,所以它在这种情况下是相对于其他收集器中最高效的。

SerialOld(-XX:+UseSerialGC)

标记-整理算法 老年代
主要client模式。Server模式下,1、与其他垃圾收集器搭配使用。2、作为使用 CMS 收集器的后备垃圾收集方案。

ParNew

复制算法 新生代
ParNew其实就是Serial收集器的多线程版本。除了Serial收集器外,只有它能与CMS收集器配合工作。
ParNew是许多运行在Server模式下的JVM首选的新生代收集器。但是在单CPU的情况下,它的效率远远低于Serial收集器,所以一定要注意使用场景。

ParallelScavenge(-XX:+UseParallelGC)

复制算法 新生代
吞吐量优先收集器 自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别。
ParallelScavenge收集器的目标是达到一个可控件的吞吐量,所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。如果虚拟机总共运行了100分钟,其中垃圾收集花了1分钟,那么吞吐量就是99% 。

ParallelOld

标记-整理 老年代
这个收集器在JDK1.6之后才开始提供的,在此之前,ParallelScavenge只能选择SerialOld来作为其老年代的收集器,这严重拖累了ParallelScavenge整体的速度。而ParallelOld的出现后,“吞吐量优先”收集器才名副其实!
在注重吞吐量与CPU数量大于1的情况下,都可以优先考虑ParallelScavenge + ParalleloOld收集器。

CMS 标记清除 老年代

它是对于响应时间的重要性需求大于吞吐量要求的收集器
主要目标是获取最短垃圾回收停顿时间

CMS的执行过程如下:

初始标记(STW initial mark)

在这个阶段,需要虚拟机停顿正在执行的应用线程,官方的叫法STW(Stop Tow World)。这个过程从根对象扫描直接关联的对象,并作标记。这个过程会很快的完成。

并发标记(Concurrent marking)

这个阶段紧随初始标记阶段,在“初始标记”的基础上继续向下追溯标记。注意这里是并发标记,表示用户线程可以和GC线程一起并发执行,这个阶段不会暂停用户的线程哦。

并发预清理(Concurrent precleaning)

这个阶段任然是并发的,JVM查找正在执行“并发标记”阶段时候进入老年代的对象(可能这时会有对象从新生代晋升到老年代,或被分配到老年代)。通过重新扫描,减少在一个阶段“重新标记”的工作,因为下一阶段会STW。

重新标记(STW remark)

这个阶段会再次暂停正在执行的应用线程,重新重根对象开始查找并标记并发阶段遗漏的对象(在并发标记阶段结束后对象状态的更新导致),并处理对象关联。这一次耗时会比“初始标记”更长,并且这个阶段可以并行标记。

并发清理(Concurrent sweeping)

这个阶段是并发的,应用线程和GC清除线程可以一起并发执行。

并发重置(Concurrent reset)

这个阶段任然是并发的,重置CMS收集器的数据结构,等待下一次垃圾回收。

CMS的缺点:

  1. 内存碎片。由于使用了 标记-清理 算法,导致内存空间中会产生内存碎片。不过CMS收集器做了一些小的优化,就是把未分配的空间汇总成一个列表,当有JVM需要分配内存空间的时候,会搜索这个列表找到符合条件的空间来存储这个对象。但是内存碎片的问题依然存在,如果一个对象需要3块连续的空间来存储,因为内存碎片的原因,寻找不到这样的空间,就会导致Full GC。

  2. 需要更多的CPU资源。由于使用了并发处理,很多情况下都是GC线程和应用线程并发执行的,这样就需要占用更多的CPU资源,也是牺牲了一定吞吐量的原因。

  3. 需要更大的堆空间。因为CMS标记阶段应用程序的线程还是执行的,那么就会有堆空间继续分配的问题,为了保障CMS在回收堆空间之前还有空间分配给新加入的对象,必须预留一部分空间。CMS默认在老年代空间使用68%时候启动垃圾回收。可以通过-XX:CMSinitiatingOccupancyFraction=n来设置这个阀值。

G1 标记-整理

无内存碎片。可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享