JVM的垃圾回收机制原理

日常开发中,我们总需要创建大量的对象,如果我们没有及时把创建的对象回收,造成对象持续堆积,直至造成内存溢出问题。JVM提供了一种垃圾回收机制,在后台创建一个守护线程,在内存紧张的时候自动进行垃圾回收机制。

一、Java的内存结构模型

Java虚拟机在执行Java程序的时候会把它所管理的内存划分为若干个不同的数据区域,而有些区域是每个线程的私有区域,有些区域是所有线程所共享的共有区域。
在这里插入图片描述
从图中我们可以看出,虚拟机栈、本地方法栈、程序计数器属于线程私有区域。随着线程的创建和关闭进行创建和取消。其中,

  • 程序计数器可以看作是当前线程所执行的字节码的行号指示器,工作时就是通过改变计数器的值来选取下一条需要执行的字节码指令。而程序计数器只记录正在执行的字节码指令地址,如果执行本地Native方法,则计数器的值为空。
  • Java虚拟机栈描述的是Java方法执行的线程内存模型,每个方法被创建的时候,虚拟机都会同步创建一个栈帧用来存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法执行被调用到执行完毕的过程,就对应着一个栈帧从虚拟机栈入栈到出栈的过程。
  • 本地方法栈和虚拟机栈非常相似,为本地的Native方法提供方法服务。
  • 堆存放Java中所有的对象实例,所有的对象在创建时候都会在这里分配内存。
  • 方法区和Java堆一样,也是各个线程共享的内存区域,用于存储被虚拟机机加载的类型信息、常量、静态变量、编译后的代码缓存等。

二、如何判断Java对象是否存活

为了判断一个对象是否应该被回收,JVM给我们提供了两种算法:引用计数算法可达性分析法

2.1、引用计数算法

为每一个对象添加一个引用计数器,用来存储该对象被引用的个数。当该个数为0时,意味着没人引用这个对象,可以认为这个对象死亡。每当有一个地方去引用它时,引用计数器就+1。
但是,这种方法存在一个问题:当两个对象相互引用时,它俩的计数就永远不为0,就永远不会被回收。例如:当两个类互相是对方的成员变量,重写toString方法的时候,相互调用,就会造成循环引用。

2.2 可达性分析法

这种方法的思路是把所有对象之间的关系想象成一颗树,从树的根节点GC Roots出发,持续遍历出所有连接的树枝对象,这些对象被看作是存活对象。
可以作为GC Root节点的对象,主要有如下四种:

  • 虚拟机栈中引用的对象
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI引用的对象。

可达性分析法并发时可能产生的“对象”消失问题: 使用增量更新和原始快照两个方法解决。

增量更新: 在遍历过程中,如果添加了一条由已访问过节点指向未访问过节点的引用,我们就需要把这条引用记录下来,待并发扫描过后,再按照这些记录下来的引用关系的已访问节点重新扫描一次。

原始快照:在遍历过程中,如果删除了一条指向未访问节点的引用关系时,就要把这个要删除的引用记录下来,然后待并发扫描完成后,再将这些记录过的引用关系重新扫描一遍。

三、垃圾回收算法

通过上面的算法,我们可以明确标记出垃圾对象。接下来,JVM还同样为我们提供了一些算法,去回收这些垃圾对象。

3.1 标记 - 清除算法

这个算法分为两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记所有存活的对象,再统一回收掉所有未被标记的对象。标记的过程就是对象是否属于垃圾的判定过程。

优点:简单方便
缺点:容易产生大量的磁盘碎片,执行效率不稳定。

3.2 标记 - 复制算法

为了避免大量回收对象效率低的问题,这种算法把可用内存按容量划分成大小相等的两块,每次只使用其中的一块。这一块内存用完了,就将还存活者的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

优点:这种算法适用于回收新生代,并且分配内存时不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。
缺点:显而易见,这种算法的代价是内存缩小为原来的一半,空间浪费过大。

3.3 标记 - 整理算法

针对老年代的数据特征,标记整理算法中标记过程与标记-清除算法一样,但后续步骤不是直接将可回收对象进行清理,而是让所有存活的对象都向内存空间的另一端移动,然后直接清除掉边界以外的内存。

特点:特别适合老年代存活对象中,垃圾少的情况,并且每次整理后都有大块的空间来存储大对象。
缺点:整理过程过于复杂,算法复杂程度较高。

3.4 堆和方法区的内存回收

3.4.1 方法区的垃圾回收

方法区又称作永久代,垃圾回收主要包括两部分:废弃常量和无用的类。
首先是废弃常量垃圾回收的一般步骤:
第一步:判断一个常量是否是废弃常量:没有任何一个地方对这个常量进行引用。
第二步:垃圾回收

无用的类垃圾回收的一般步骤:
第一步:判断一个类是否是无用的类,看是否满足下面三个条件:

  • Java堆中不存在该类的任何实例,也就是该类的所有实例都被回收。
  • 加载该类的ClassLoader已经被回收了
  • 该类对应的Class对象在任何地方没有引用了,也不能通过反射访问该类的方法。

3.4.2 堆的垃圾回收—分代回收算法

Java堆分成三个部分,分别用来存储三种类型的数据:

  • 刚创建的对象 — 新生代

  • 存活了一段时间的对象 — 新生代

  • 永久存在的对象 — 老年代
    针对这几种对象,有如下方案:

  • 新生代- 标记-复制回收算法:对于新生区域,每次GC都有大量新对象死去,少量存活。因此采用复制回收算法,把少量存活的对象复制过去即可。

  • 老年代 - 标记整理 回收算法:老年代对象存活多,垃圾少,并且对象体积较大。根据这个特点,只少量的移动对象就能清除垃圾。而且不存在磁盘碎片会。所以应用标记整理算法。只需根据标记整理的具体步骤进行垃圾回收即可。

四、常见垃圾回收器

在JVM中,常见的垃圾回收器有Serial、ParNew、Parallel Scavenge、CMA、Serial Old(MSC)、Parallel Old 、 G1等。下面对这些垃圾回收器进行一个简要介绍

  • Serial(单线程): 这种回收器时最基本的新生代垃圾回收器,是单线程的垃圾回收期。采用的是 复制算法。垃圾清理时,Serial回收器不存在单线程的切换,所以单CPU环境下,垃圾清除效率较高。
  • Serial Old(单线程) Serial Old 是Serial 回收器的老年代版本,也是单线程回收器。使用标记-整理算法。
  • ParNew(多线程) 是在Serial回收器的基础上演化来的,属于Serial回收器的多线程版本,采用复制算法。运行在新生代区域。可以根据CPU核数来开启不同的线程数,从而达到最优的垃圾回收效果。
  • Parallel Scavenge(多线程) 也是运行在新生代区域,属于多线程的回收器,采用标记—复制算法。而与ParNew不同的是,Parallel Scavenge回收器更关心的是程序运行的吞吐量。即一段时间内用户代码运行时间占总时间的百分比。
  • Parallel Old(多线程) 是Parallel Scavenge回收器的老年代版本,属于多线程回收器,采用标记—整理算法,同样考虑吞吐量优先这一指标。
  • CMS(多线程) 是在最短回收停顿时间为前提的回收器,属于多线程回收器,采用标记—清除算法。适用于B/S结构的服务器。
  • G1回收器 G1是JDK1.7之中用来取代CMS的压缩回收期,物理上没有隔断新生代和老年代,但扔然属于分代垃圾回收器。
    在这里插入图片描述
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值