JVM GC

本文最后更新于:2022年7月28日 下午

概览:JVM GC

预警!仅用于本人快速自学,不过欢迎指正。

内存区域与垃圾回收

Java运行时内存区域:程序计数器、虚拟机栈、本地方法栈、堆和方法区。

  • 程序计数器、虚拟机栈、本地方法栈这三者线程私有,与线程的生命周期相同。栈中的栈帧随着方法的进入和退出而入栈出栈,每一个栈中分配多少内存基本上是在类结构确定下来时就已知了,这几个区域分配回收具有确定性
  • 但是堆和方法区,具有不确定性:一个接口的多个实现类需要的内存可能不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有运行期间,才能知道。这一部分是需要垃圾回收所重点关注的。

对象是否存活

1.引用计数法,缺陷:循环引用问题

可达性分析算法

实现:找到一系列的GC Roots节点,作为起始节点,沿着引用关系搜索,搜索所走过的路径称为引用路径,如何一个对象到GC Roots之间没有引用路径,那就说明对象不再被使用。

JVM GC Roots对象

  • 虚拟机栈中的引用对象,eg:方法参数、局部变量、临时变量
  • 方法区中的静态属性引用,例如Java中的引用类型静态变量
  • 方法区常量引用的对象,例如字符串常量池的引用
  • java虚拟机内部的引用,例如系统类加载器
  • 被同步锁持有的对象
  • 本地方法栈中JNI引用的对象

四大引用

强、软、弱、虚

  • 强:传统引用
  • 软:SoftReference,系统将要发生内存溢出前进行标记回收
  • 弱:WeakReference,关联对象存活到下次垃圾回收
  • 虚:PhantomReference,目的:在关联对象被回收时获得通知

直接内存的回收,Cleaner 继承了 PhantomReference虚引用。

方法区的回收

虚拟机规范中不强制。

方法区回收内容:废弃的常量不再使用的类型

类型不再使用的条件:

  1. 该类的所有实例都已经被回收。堆中不存在该类和派生子类。
  2. 类的加载器被回收。——最好是类加载器可替换的场景,否则难以达到。
  3. Class对象没有任何对象引用。

在大量使用反射、动态代理、CGLib等字节码框架这类频繁自定义类加载器的场景,通常都需要Java虚拟机具备类型卸载的能力。

垃圾收集算法

分代收集

经验法则一:绝大多数对象朝生夕灭。

经验法则二:活过多次垃圾回收的对象更容易存活。

由经验法则引发出的设计原则:堆应该划分不同的区域,回收对象按照年龄分配到不同区域存储。

由此有了新生代和老年代,新生代中每次回收都有大批对象死去,而存活的对象会逐步晋升到老年代之中。

划分区域之后,垃圾收集器可以每次只收集某一部分的垃圾,但是有一个问题:例如,只想回收新生代的垃圾,但是有可能老年代引用了新生代,这样的话回收也必须扫描老年代进行可达性分析。

经验法则三:跨代引用相对于同代引用来说占比极少。

根据这条法则,在新生代创建一个全局的数据结构——记忆集,标识出老年代的哪一块内存存在跨代引用,当发生新生代的MinorGC时,只扫描有跨代引用的区域。

标记-清除

两阶段:

  1. 标记出所有需要回收的对象(或者标记存活对象)
  2. 统一回收所有被标记的对象

缺点:

  1. 执行效率不稳定,标记和清除随着对象数量增长而效率降低
  2. 内存空间碎片化问题,会产生大量内存碎片

标记-复制

将可用内存分为容量相等的两块,每次只使用其中的一块,当一块用完时,将还存活的对象移动到另一块,再把已经使用过的清理掉。

优点:实现简单,不用考虑空间碎片。

缺点:空间浪费,在对象存活率较高的时候会进行太多的内存复制,老年代最好不要使用。

目前,主流虚拟机都使用它来回收新生代。

HotSpot虚拟机将新生代分为Eden区、Survivor From区、Survivor To区,8 :1:1。

当Survivor 区不足够容纳一次MinorGC时,使用老年代来分配担保。

标记-整理

针对老年代的垃圾回收,提出的算法。

  1. 标记存活对象
  2. 将所有存活的对象向内存的一端移动,然后清理掉边界意外的内存。

问题:移动存活对象 —— 需要更新引用,必须全程暂停用户程序才能够使用, —— 称为 Stop The World。

垃圾收集器

垃圾收集不可知、 不可控

1. serial 收集器 与 serial-old

单线程收集器, —— 进行垃圾收集的时候,必须暂停其他工作线程,直到收集结束。

  • 新生代采取复制算法,暂停所有线程
  • 老年代采取标记-整理算法,暂停所有线程

适用场景:客户端模式下,单核处理器或者处理器核心比较少。

2. ParNew

serial收集器的多线程版本,GC时多线程进行处理。

3. Parallel Scavenge收集器 与 Parallel -old

新生代收集器,基于标记-复制算法,并行的多线程收集器。

  • 可控制吞吐量:控制用户代码时间 与 处理器总时间比值。

老年代收集器,支持多线程并发收集,基于标记-整理

4.CMS收集器

CMS —- Concurrent Mark Sweep

目的:获取最短回收停顿时间。

采用:标记-清除算法

运行过程

  1. 初始标记,STW,快速标记GC Roots关联的直接对象
  2. 并发标记,从GC Roots关联的直接对象查找对象图中的垃圾,耗时时间长,但是GC线程与用户线程并发执行。
  3. 重新标记,STW,修正并发标记期间,标记变动的那一部分,耗时短。
  4. 并发清除,清理删除标记阶段已经死亡的对象,GC线程与用户线程并发执行

缺点

  1. 在并发阶段会占用处理器的一部分资源,导致应用程序变慢,降低总吞吐量。

  2. 无法处理浮动垃圾,可能会引起Full GC 和 STW。

    浮动垃圾:在并发标记与并发清除期间产生的新垃圾这一次GC中不能回收,称之为浮动垃圾。为了确保有足够的空间,CMS不能在快满的时候进行收集,而是达到了一定比例就需要收集。如果预留的空间无法满足程序分配,就会出现并发失败现象,这是只能使用STW,然后Serial-old收集器来进行老年代垃圾回收,这样的停顿时间会很长。

  3. 基于标记-清除垃圾算法,会产生大量内存碎片。

5. G1收集器

G1 —- Garbage First收集器

里程碑式的垃圾收集器 —— 局部收集、基于Region的内存布局。

前面的收集器:分代思想来做处理。

G1: 面向堆内存的任何部分来组成回收集,虽保留新生代、老年代的概念,但区域不再固定,而是集合。

将Java堆分成大小相等的独立区域Region,此外还有区域专门存储大对象 —— 超过Region一半的区域。

其他:新生代-老年代比例划分

默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小老年代 ( Old ) = 2/3 的堆空间大小。其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。
默认的,Eden : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。

JVM老年代和新生代的比例 - 一中晴哥威武 - 博客园 (cnblogs.com)

其他:G1收集器的适用场景?

  1. 面向服务端应用,针对具有大内存、多处理器的机器。

  2. 最主要的应用是需要低 GC 延迟,并具有大堆的应用程序提供解决方案。

如:在堆大小约 6GB 或更大时,可预测的暂停时间可以低于 0.5秒;(G1 通过每次只清理一部分而不是全部的 Region 的增量式清理来保证每次 GC 停顿时间不会过长)。

  1. 用来替换掉 JDK1.5 中的 CMS 收集器,在下面的情况时,使用 G1 可能比 CMS 好。

    1. 超过 50% 的 Java 堆被活动数据占用
    2. 对象分配频率或年代提升频率变化很大
    3. GC停顿时间过长(长于0.5至1秒)
  2. HotSpot 垃圾收集器里,除了 G1 以外,其他的垃圾收集器使用内置的 JVM 线程执行 GC 的多线程操作,而 G1 GC 可以采用应用线程承担后台运行的 GC 工作,即当 JVM 的 GC 线程处理速度慢时,系统会调用应用程序线程帮助加速垃圾回收过程。

原文链接:https://blog.csdn.net/chengqiuming/article/details/119271564


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!