深入了解JVM(GC篇)

前言

这篇文章主要是我看了深入理解JAVA虚拟机这本书后做的总结笔记,也是对知识点的一个巩固和复习。

JAVA的垃圾回收机制

java垃圾回收机制可以用3个词来概括:where,when和how。
where即运行时内存分布情况。
when代表何时进行垃圾回收。
how表示如何回收对象。

JVM的内存模型

在这里插入图片描述
在jvm中,“几乎”所有的对象都被分配在堆上(在即时编译优化中,如果一个对象没有逃逸出方法,那么可以分配在栈上,随着方法的出栈一同消亡),这里也是GC管理的内存区域。

何时进行垃圾回收

在进行垃圾回收之前,首先第一步要判断的是一个对象是否还“活着”。常用的方法主要有两种:引用计数法和可达性分析法。

引用计数法

引用计数法的原理是在对象中添加一个引用计数器,当有一个地方对该对象进行引用时,就加1,引用失效时则减1。是一种比较简单有效的方法。但是JAVA中没有采用这种方法来进行判断,并且这种方法有缺陷,比如两个相互引用但是没有其他引用的对象就不会被判断为“死亡”对象。

可达性分析法

可达性分析法主要是从被称为“GC Roots”的根对象开始,根据引用关系进行搜索,搜索走过的路径被称为“引用链”。当一个对象在引用图中不可达时,说明这个对象已经死亡。
在JAVA中,固定可作为GC Roots的对象有以下几种:

  1. 在虚拟机栈中引用的对象,例如各个线程被调用的方法堆栈中使用的参数、局部变量、临时变量等。
  2. 在方法区中类静态属性引用的对象、
  3. 在方法去常量池中引用的对象,如字符串常量池的引用。
  4. 在本地方法栈中native方法引用的对象。
  5. Java虚拟机内部的引用,如基本数据类型对应的Class对象。
  6. 所有被同步锁持有的对象。
  7. 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

除了这些,还有一些其他“临时性”的对象,比如在分代垃圾收集器中,年轻代中对象可能有年老代的引用,所以可能需要把年老代的对象也加入GC Roots中。

四种引用

无论是引用计数法还是可达性分析法,都离不开“引用”,在Java中一共有4种引用方式:

  1. 强引用,普遍存在的赋值引用,只要有强引用关系,GC就不会回收。
  2. 软引用,用来描述一些还有用但非必要的对象,在抛出系统内存溢出异常前进行回收。
  3. 弱引用,用来描述一些非必要对象,在下一次GC时进行回收。
  4. 虚引用,不影响垃圾回收,主要用来跟踪垃圾回收的活动。

如果一个对象存在多种引用关系,需要使用“单弱多强”的规则进行判断,即单条引用链的强弱由最弱小的关系决定,多条引用链以最强引用链的关系来决定。

被回收之前

当一个对象被判断为不可达对象的时候,至少要经历两次标记过程:

  1. 如果是不可达对象则进行第一次标记。
  2. 如果该对象没有finalize()方法或者已经执行过该方法则进行第二次标记。

在两次标记之后才会“死亡”。
这里的执行指的是开始运行,而非运行结束,主要是防止有些对象的finalize方法执行缓慢或者死循环。
所以,如果对象需要拯救自己,则在finalize方法中进行关联即可(但是该方法只能使用一次,因为第二次会被判断为虚拟机已经执行过该方法)。

方法区的回收

一般方法区的回收性价比较低,并且判断条件苛刻。在方法区中主要对废弃的常量和不再使用的类型进行回收。
常量回收主要判断是否有地方进行引用。
类型回收需要同时满足以下3个条件:

  1. 该类所有的实例已经被回收。
  2. 加载该类的加载器已被回收。
  3. 该类对应的java.lang.class对象没有引用。

垃圾回收算法

垃圾回收算法主要分为两类:“引用计数式垃圾收集”和“追踪式垃圾收集”。
在Java中只涉及到后者,所以后面的算法都是“追踪式垃圾收集”。

分代收集理论

当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”的理论,这个理论建立在两个分代假说之上:

  1. 弱分代假说:绝大多数对象朝生夕灭。
  2. 强分代假说:熬过越多次垃圾收集的对象越难以消亡。

以及一个推论:存在相互引用关系的对象应该倾向于同时消亡。

所以大多数GC都把Java堆划分成不同区域,每次只回收其中一个或者某些区域。根据回收的区域不同可以分为:

  • Minor GC:指目标为新生代的GC。
  • Major GC:指目标为年老代的GC,目前只有CMS才有。
  • Mixed GC:指整个新生代和部分年老代的GC,目前只有G1。
  • Full GC:整个Java堆和方法区的GC,一般GC中只有在内存溢出的时候才进行。

4种垃圾回收算法

  • 标记 - 清除法:标记存活对象,然后进行垃圾收集,存在两个问题,执行效率不稳定,空间碎片化。
  • 标记 - 整理法:标记存活对象,然后把对象都移动到一端,清除边界以外的空间。
  • 标记 - 复制法:每次使用一半的区域,然后把存活的对象复制到另一块上,缺点是内存利用率不高。
  • 分代收集算法:根据新生代和年老代的不同采用不同的算法。在新生代采用标记复制的方法,在年老代使用标记清除或者标记整理的方式。

HotSpot算法细节

根节点枚举

在HotSpot中采用OopMap的数据结构来保存引用信息,可以快速完成根节点枚举。

安全点

HotSpot只在安全点的时候生成OopMap,所有线程发现中断位为真时,在最近的一个安全点上中断,然后开始进行垃圾收集。

安全区域

有些线程处于Sleep之类不执行状态的时候,无法响应中断请求。安全区域指的是某一代码片段不会发生引用改变,当线程进入安全区域之后进行声明,虚拟机不需要去管理在安全区域之内的线程。当线程需要离开安全区域时,需要判断虚拟机是否完成了根节点枚举。

记忆集和卡表

为了解决跨代引用的问题,新生代或者区域中会有记忆集的数据结构用来存放引用信息,进行根节点枚举的时候只要把记忆集中的对象加入。HotSpot中记忆集记录精度为卡精度,此时记忆集被称为卡表。每个卡页中存放了多个对象,只要有一个对象存在跨代引用指针,就把整个卡页加入根节点。

写屏障

由于引用变化的时候卡表需要更新,所有引入了写屏障的概念,在引用对象赋值之前或者之后进行额外的动作,分别称为“写前屏障”和“写后屏障”,在G1之前的处理器都采用写后屏障。

并发的可达性分析

在较新的垃圾收集器中,只在根节点枚举的之后进行一个短暂的停顿,之后则并发地进行遍历。
遍历的时候采用3色遍历的方法:

  • 白色:表示该对象还未访问过,如果分析结束之后,对象仍是白色,则说明该对象已经死亡。
  • 黑色:该对象已经访问过,并且该对象所有引用都已经进行了扫描。
  • 灰色:该对象已经访问过,但是引用还未扫描完毕。

并发遍历的时候存在两个问题:浮动垃圾和对象消失。
浮动垃圾的产生是由于引用关系改变后,一个消亡对象被误判为黑色。
对象消失则是一个被引用对象被误判为白色。
浮动垃圾的问题只需在下次垃圾回收的时候再回收就行,但是对象消亡的问题会导致程序错误。
当且仅当以下两个条件同时满足的时候,对象消亡才会发生:

  1. 赋值器插入了一条或者多条从黑色对象到白色对象的新引用。
  2. 赋值器删除了全部从灰色对象到该白色对象的引用。

为了解决这个问题,只需要破坏其中一个条件即可,所有有了两种解决方案:增量更新和原始快照。
增量更新指的是,每次黑色对象增加白色对象引用的时候记录下来,然后等并发扫描结束之后再对这些新增的引用进行一次扫描,即黑色对象重新变为灰色对象。
原始快照指的是,每次灰色对象删除白色对象引用的时候把这些引用记录下来,并发结束之后再以这些旧引用重新进行扫描,也就是先按照开始扫描的对象图快照来进行搜索。

经典垃圾收集器

CMS收集器

针对年老代,采用标记清除的方式,分为四个步骤:

  1. 初始标记:短暂暂停,标记一下GC Roots直接关联的对象。
  2. 并发标记:遍历整个对象图。
  3. 重新标记:短暂暂停,处理增量更新中的对象。
  4. 并发清除:并发地清除对象。

G1收集器

目标是整个内存区域,G1把内存区域划分为Region(区域),垃圾回收的衡量标准不再是新生代或者年老代,而是哪一块区域存在的垃圾比较多。
Region中还存在Humongous区域,专门存放大对象。
G1在后台维护一个回收时间和回收空间的列表,根据用户给出停顿时间不同进行回收。
G1一共分为4个步骤:

  1. 初始标记:和CMS一样,对和GC Roots直接相关的对象进行标记,有短暂停顿。但是G1存在两个名为TAMS的指针,在并发阶段,所有用户新分配的对象都在这两个指针之上,这些对象是默认隐式存活的。
  2. 并发标记:从GC Root开始递归扫描,采用原始快照的方式来处理并发问题。
  3. 最终标记:做一个短暂的停顿,处理并发标记阶段结束之后留下来的一部分SATB记录。
  4. 筛选回收:根据回收价值和成本进行排序,根据用户希望的停顿时间进行回收操作,把需要回收的区域加入回收集,然后把其中存活的对象直接复制到空的区域中,这一步需要暂停用户线程。

低延迟垃圾回收器

Shenandoah收集器

与G1类似,但是有一些不同:支持并发整理的算法,默认不使用分代收集,使用矩阵而非记忆集来维护引用关系。
具体实现步骤如下:

  1. 初始标记:同G1。
  2. 并发标记:同G1。
  3. 最终标记:同G1。
  4. 并发清理:清理一个存活对象都不存在的区域。
  5. 并发回收:采用读屏障和跳转指针来进行并发回收。
  6. 初始引用更新:确保并发回收线程的结束。
  7. 并发引用更新:开始更新存活对象的引用。
  8. 最终更新:修改GC Roots中的引用。
  9. 并发清理:清理旧的对象所在的区域。

ZGC收集器

采用Region布局,ZGC的Region是可以动态销毁和创建的,分为3种Region:

  • 小Region:容量固定为2M,存放小于256KB的对象。
  • 中Region:固定位32M,用于存放大于256KB但小于4MB的对象。
  • 大Region:容量不固定,但是为2MB的整数倍,用于存放4MB以上的对象。

ZGC的核心是他的染色体指针技术,即把少量信息存放在指针上,ZGC一共采用了4个标志位:三色标记状态(占2位)、是否进入重分配集(即被移动过)、是否只能通过finalize方法才能访问。
但是指针变动会导致地址发生变化,所以在Linux和x86-64平台上,ZGC采用了多重映射的方法将多个虚拟地址映射到同一个物理地址上。
ZGC收集器相比G1收集器,多出来的步骤主要如下:

  1. 并发标记:在这个阶段中除了标记之外,ZGC中会修改指针中的三色状态标记。
  2. 并发预备重分配:将需要回收的区域加入回收集。
  3. 并发重分配:将存活对象进行复制,并且维护一个转发表,如果用户对某个对象进行访问,会被内存屏障所截获,然后根据转发表中的信息进行访问,并且修改指针上的信息,直接指向新对象。
  4. 并发重映射:将旧引用修改成新引用。

后记

以上内容大致概括了书中第三章的内容,但是很多地方仅仅只是做了一个归纳,没有展开分析。
关于G1垃圾收集器,有一篇博客写的非常好和详细:面试官问我关于G1怎么知道你是什么时候的垃圾
接下来几篇文章应该都是有关于JVM的,都是一些面经上热点问题的相关知识点和拓展,在完成JVM部分之后会继续写有关于JAVA并发和一些基础知识点的文章,最后完成有关于操作系统数据库和计算机网络部分的文章。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值