你了解JVM垃圾回收机制及其实现原理吗?一文带你深入探讨

前言

对于 JVM 来说,我们都不陌生,其实 Java Virtual Machine(Java 虚拟机)的缩写,它也是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM 有自己完善的硬件架构,如处理器、堆栈等,还具有相应的指令系统,其本质上就是一个程序,当它在命令行上启动的时候,就开始执行保存在某字节码文件中的指令。

Java 语言的可移植性就是建立在 JVM 的基础之上的,任何平台只要装有针对于该平台的 Java 虚拟机,字节码文件(.class)就可以在该平台上运行,这就是“一此编译,多次运行”。除此之外,作为 Java 语言最重要的特性之一的自动垃圾回收机制,也是基于 JVM 实现的。那么,自动垃圾回收机制到底是如何实现的呢?在本文中,就让我们一探究竟。

什么是垃圾?

在 JVM 进行垃圾回收之前,首先就是判断哪些对象是垃圾,也就是说,要判断哪些对象是可以被销毁的,其占有的空间是可以被回收的。根据 JVM 的架构划分,我们知道, 在 Java 世界中,几乎所有的对象实例都在堆中存放,所以垃圾回收也主要是针对堆来进行的。

在 JVM 的眼中,垃圾就是指那些在堆中存在的,已经“死亡”的对象。而对于“死亡”的定义,我们可以简单的将其理解为“不可能再被任何途径使用的对象”。那怎样才能确定一个对象是存活还是死亡呢?这就涉及到了垃圾判断算法,其主要包括引用计数法和可达性分析法。

垃圾判断算法

引用计数法

在这种算法中,假设堆中每个对象(不是引用)都有一个引用计数器。当一个对象被创建并且初始化赋值后,该对象的计数器的值就设置为 1,每当有一个地方引用它时,计数器的值就加 1,例如将对象 b 赋值给对象 a,那么 b 被引用,则将 b 引用对象的计数器累加 1。

反之,当引用失效时,例如一个对象的某个引用超过了生命周期(出作用域后)或者被设置为一个新值时,则之前被引用的对象的计数器的值就减 1。而那些引用计数为 0 的对象,就可以称之为垃圾,可以被收集。

特别地,当一个对象被当做垃圾收集时,它引用的任何对象的计数器的值都减 1。

  • 优点:引用计数法实现起来比较简单,对程序不被长时间打断的实时环境比较有利。
  • 缺点:需要额外的空间来存储计数器,难以检测出对象之间的循环引用。

可达性分析法

可达性分析法也被称之为根搜索法,可达性是指,如果一个对象会被至少一个在程序中的变量通过直接或间接的方式被其他可达的对象引用,则称该对象就是可达的。更准确的说,一个对象只有满足下述两个条件之一,就会被判断为可达的:

  • 对象是属于根集中的对象
  • 对象被一个可达的对象引用

在这里,我们引出了一个专有名词,即根集,其是指正在执行的 Java 程序可以访问的引用变量(注意,不是对象)的集合,程序可以使用引用变量访问对象的属性和调用对象的方法。在 JVM 中,会将以下对象标记为根集中的对象,具体包括:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中的常量引用的对象
  • 方法区中的类静态属性引用的对象
  • 本地方法栈中 JNI(Native 方法)的引用对象
  • 活跃线程(已启动且未停止的 Java 线程)

根集中的对象称之为GC Roots,也就是根对象。可达性分析法的基本思路是:将一系列的根对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,如果一个对象到根对象没有任何引用链相连,那么这个对象就不是可达的,也称之为不可达对象。

当标记阶段完成后,GC 开始进入下一阶段,删除不可达对象。当然,可达性分析法有优点也有缺点,

  • 优点:可以解决循环引用的问题,不需要占用额外的空间
  • 缺点:多线程场景下,其他线程可能会更新已经访问过的对象的引用

引用类型

  1. 强引用:发生 gc 的时候不会被回收
  2. 软引用:有用但不是必须的对象,在发生内存溢出之前会被回收
  3. 弱引用:有用但不是必须的对象,在下一次 GC 时会被回收
  4. 虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知

垃圾辨别方法

  1. 引用计数器为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1当计数器为 0 时就可以被回收。缺点是不能解决循环引用的问题
  2. 可达性分析从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的

GC Roots,GC 的根集合, 是一组必须活跃的引用

可作为 GC Roots 的对象有:

虚拟机(栈帧中的本地变量表)中引用的对象方法区中类静态属性引用的对象方法区中常量引用的对象本地方法栈中 JNI(即一般说的 native 方法)中引用的对象

垃圾收集算法

  1. 引用计数(Reference Counting)原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数垃圾回收时,只用收集计数为 0 的对象缺点:无法处理循环引用问题
  2. 标记-清除(Mark-Sweep)第一阶段从引用根节点开始标记所有被引用的对象第二阶段遍历整个堆,把未标记的对象清除缺点:此算法需要暂停整个应用,同时,会产生内存碎片
  3. 复制(Copying)把内存空间划为两个相等的区域,每次只使用其中一个区域垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。此算法每次只处理正在使用中的对象因为复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题缺点:需要两倍内存空间
  4. 标记-整理(Mark-Compact)第一阶段从引用根节点开始标记所有被引用对象第二阶段遍历整个堆,将所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题
  5. 分代(Generational Collecting)基于对对象生命周期分析后得出的垃圾回收算法把堆中对象分为年青代、年老化、持久代(JDK8 不存在持久代)对不同生命周期的对象使用不同的算法进行回收现在的垃圾回收器一般使用此算法

分代回收算法

起源:研究发现,大部分 java 对象只存活一小段时间,而存活下来的小部分 java 对象则会存活很长一段时间

简单来说,将堆分成两部分,年轻代用来存放新对象,当对象存活时间够长时,移动到年老代

堆的分代

  • 年轻代 Young Generation
  1. 默认占总空间的 1/3(通过 -XX:NewRatio 指定年轻代和老年代比例)
  2. 分为 Eden、To Survivor、From Survivor 三个区,默认占比 8:1:1(通过 -XX:SurvivorRatio 指定)
  • 年老代 Tenured Generation
  1. 默认占总空间的 2/3
  • 持久代 Perm Generation(JDK8后不存在)
  1. 即方法区,用于存放静态文件,如今Java类、方法等
  2. 持久代对垃圾回收没有显著影响在
  3. JDK8 中,废弃了持久代,改用元空间(metaspace)实现方法区,属于本地内存

分代收集

  • 年轻代回收器
  1. 假设大部分对象都存活很短时间,需要频繁采用耗时较短的垃圾回收算法
  2. 新生代垃圾收集器一般采用复制算法,优点是效率高,缺点是内存利用率低
  3. 垃圾收集器有:Serial、ParNew、Parallel Scavenge
  • 年老代回收器
  1. 假设老年代中的对象大概率继续存活,真正触发老年代 gc 时,代表假设出错或堆空间已耗尽,一般需要全堆扫描,全局垃圾回收
  2. 老年代收集器一般采用的是标记-整理的算法进行垃圾回收
  3. 垃圾收集器有:Serial Old、Parallel Old、CMS
  • 整堆回收器

G1:兼顾吞吐量和停顿时间的 GC 实现,JDK 9 以后的默认 GC 选项

回收过程

新对象存放在年轻代的 Eden 分区,Eden 空间耗尽时,触发 gc,一般使用复制算法

年老代空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法

  1. 把 Eden 和 From Survivor 存活的对象放入 To Survivor 区
  2. 清空 Eden 和 From Survivor 分区
  3. From 和 To 交换指针,保证下次 gc 前To Survivor 为空
  4. Survivor 分区的对象,经过一次复制年龄就 +1,年龄到达 15时(默认 15),Survivor 分区升级为老生代。对象也会直接进入年老代

gc 类型

  • Minor GC
  1. 一般情况下,当新对象生成,并且在 Eden 申请空间失败时,就会触发Minor GC
  2. 在年轻代 Eden 区域进行GC,清除不存活对象,并且把尚且存活的对象移动到 Survivor 区。然后整理 Survivor 的两个区
  3. 很频繁的 gc,不影响老年代
  • Full GC

对整个堆进行整理,包括Young、Tenured和Perm。Full GC比Scavenge GC要慢,因此应该尽可能减少Full GC。有如下原因可能导致Full GC:

  • Tenured 被写满
  • Perm 域被写满(JDK8 之前)
  • System.gc( ) 被显示调用
  • 上一次 GC 之后对的各域分配策略动态变化

垃圾收集器

收集器分类

  • 串行收集器
  1. 使用单线程处理所有垃圾回收工作,因为无需多线程交互,所以效率比较高
  2. 无法使用多处理器的优势,所以适合单处理器机器,也可以用在小数据量情况下的多处理器机器
  3. 可以使用 -XX:+UseSerialGC 打开
  • 并发收集器
  1. 对年轻代进行并行垃圾回收,可以减少垃圾回收时间。一般在多线程多处理器机器上使用使用 -XX:+UseParallelGC打开
  2. 打开并行收集器 jdk5 引入,在 jdk6 中进行了增强,可对堆年老代进行并行收集使用 -XX:+UseParallelOldGC 打开
  3. 如果年老代不使用并发收集,而使用单线程进行垃圾回收,会制约扩展能力
  • 并发收集器
  1. 可以保证大部分工作都并发进行(应用不停止),垃圾回收只暂停很少的时间
  2. 此收集器适合对响应时间要求比较高的中、大规模应用
  3. 使用 -XX:+UseConcMarkSweepGC 打开

常见收集器

  1. Serial:最早的单线程串行垃圾回收器
  2. Serial Old:Serial 垃圾回收器的老年版本,同样也是单线程的,可以作为 CMS 垃圾回收器的备选预案
  3. ParNew:是 Serial 的多线程版本
  4. Parallel :和 ParNew 收集器类似,是多线程的收集器Parallel 是吞吐量优先的收集器,可以牺牲等待时间换取系统的吞吐量
  5. Parallel Old:是 Parallel 老生代版本Parallel 使用复制算法,Parallel Old 使用标记-整理算法
  6. CMS:一种以获得最短停顿时间为目标的收集器,非常适用 B/S 系统
  7. G1:一种兼顾吞吐量和停顿时间的 GC 实现,是 JDK 9 以后的默认 GC 选项

CMS 收集器

  • CMS:Concurrent Mark-Sweep
  1. 牺牲吞吐量来获得最短回收停顿时间
  2. 非常适合用在要求服务器响应速度的应用上使用 -XX:+UseConcMarkSweepGC 来指定
  3. 使用 CMS 垃圾回收器
  • CMS 使用标记-清除的算法
  1. 在 gc 时候会产生大量的内存碎片
  2. 当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure
  3. 临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低

喜欢请多多点赞评论转发,关注小编,你们的支持就是小编最大的动力!!!

猜你喜欢

转载自blog.csdn.net/python6_quanzhan/article/details/106670911