GC的前置工作,聊聊GC是如何快速枚举根节点的
本文已收录至GitHub,推荐阅读 Java随想录
微信公众号:Java随想录
原创不易,注重版权。转载请注明原作者和原文链接
上篇文章中我们留下了个坑:「根节点枚举」,这篇文章就把坑填上。
在上篇文章中我们知道了HotSpot使用的是可达性分析算法,该算法需要进行根节点枚举。
但是查找根节点枚举的过程要做到高效并非一件容易的事情,现在Java应用越做越庞大,光是方法区的大小就常有数百上千兆,里面的类、常量等更是「恒河沙数」(一种修辞手法),若要逐个检查以这里为起源的引用肯定得消耗不少时间。
大家可以思考下,如果你是JVM的开发者,你会怎么去做?
前面的文章大伙可能有点忘了,那么首先我们对根节点枚举,先做个复习(我绝对不是在混字数)。

什么是根节点枚举
顾名思义,根节点枚举就是找出所有的GC Roots。
当然要成为GC Roots是有条件的,固定可作为GC Roots的对象包括以下几种(摘抄自《深入理解虚拟机 第3版》):
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
- 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
- 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
- 所有被同步锁(synchronized关键字)持有的对象。
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

上面说的这些,大伙肯定记不住,反正总结就一句话:固定可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中。
根节点枚举存在的问题
迄今为止,所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的。因此毫无疑问根节点枚举与之前提及的整理内存碎片一样会面临相似的「Stop The World」的困扰。
根节点枚举必须在一个能保障一致性的快照中才得以进行——这里「一致性」的意思是整个枚举期间执行子系统看起来就像被冻结在某个时间点上。
为什么要这么做?
试想一下,你妈给你打扫房间,你妈一边打扫,你一边丢垃圾,房间永远也打扫不干净。
所以本质上来说,根节点枚举遇到的问题,就是并发问题。
如果不「冻结」的话,根节点集合的对象引用关系在不断变化,那么分析结果准确性也就无法保证。
所以即使是号称停顿时间可控,或者(几乎)不会发生停顿的CMS、G1、ZGC等收集器,在枚举根节点这一步也是必须要停顿的。
弄明白问题之后,我们开动脑筋想想怎么解决。
如何解决根节点枚举的问题
目前主流Java虚拟机使用的都是「准确式垃圾收集」。
所谓准确式垃圾收集是指垃圾收集器能够精确地确定内存中哪些区域被对象引用,哪些区域已经不再使用,并且可以立即回收不再使用的内存。
在准确式垃圾收集中,垃圾收集器需要知道每一个引用类型变量(包括实例字段、静态字段、本地变量和输入参数等)在内存中的确切位置,以及这个位置是否正在被引用。
这样,当垃圾收集器需要进行回收时,它就可以精确地找到并回收那些不再有任何引用的对象所占用的内存。
相对应的,还有一种叫做「保守式垃圾收集」,它不能精确地识别所有的引用,只能保守地认为所有看起来像对象引用的值都可能是引用。这种方式可能会导致某些实际上可以被回收的内存得不到回收。
HotSpot采用的是准确式垃圾收集。
所以当用户线程停顿下来之后,其实并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得到哪些地方存放着对象引用的。
在HotSpot的解决方案里,是使用一组称为OopMap的数据结构来达到这个目的。OopMap可以理解为就是映射表,存储栈上的对象引用的信息,这是一种空间换时间的做法。
在 GC Roots 枚举时,只需要遍历每个栈桢的 OopMap,通过 OopMap 存储的信息,快捷地找到 GC Roots,这样就不需要进行全局扫描。
用大白话说,其实就是用类似映射表这种手段记录下来引用关系,时不时去更新下映射表,然后根节点枚举只需要扫描映射表就知道哪些地方存放引用了,而不用去进行全局扫描。
OK,弄明白之后,问题又来了,既然OopMap是一个映射表,这个表什么时候被更新?
你可能会觉得这有啥难的,引用更新的时候同步去更新映射表不就完事了吗,然而事情并没有想的那么简单。
要知道引用关系变化是十分频繁的,如果引用每变化一次就更新对应的OopMap,那将会需要大量的额外存储空间,这样垃圾收集伴随而来的空间成本就会变得无法忍受的高昂。
安全点
解决这个问题的办法就是「安全点」,事实上,只是在「特定的位置」记录了这些信息,这些位置被称为安全点(Safepoint)。
因此GC不是随时随地来的,得到达安全点时才可以开始GC。
所以流程我们就清楚了:先是到达安全点,然后更新OopMp,然后进行根节点枚举,找到GC Roots,开始GC。
安全点的选举,一般会在如下几个位置出现:
- 循环的末尾
- 方法临返回前
- 调用方法之后
- 抛异常的位置
到这里为止,貌似问题我们都解决了,but,还有一个问题我们需要考虑,我们前面说了系统要在某个时间点处于「冻结」状态,那么如何在垃圾收集发生时让所有线程都跑到最近的安全点,然后停顿下来?
有两种方案可供选择:抢先式中断(Preemptive Suspension)和主动式中断(Voluntary Suspension)。
- 「抢先式中断」:不需要线程的执行代码主动去配合,在垃圾收集发生时,系统首先把所有用户线程全部中断,如果发现有用户线程中断的地方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程响应GC事件。
- 「主动式中断」:当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。
安全点的设计似乎已经完美解决如何停顿用户线程,但是仍然有问题,安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入垃圾收集过程的安全点。但是,程序「不执行」的时候呢?
所谓的程序不执行就是没有分配处理器时间,典型的场景便是用户线程处于Sleep状态或者Blocked状态,这时候线程无法响应虚拟机的中断请求,不能再走到安全的地方去中断挂起自己。
对于这种情况,JVM引入安全区域(Safe Region)来解决。
安全区域
安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化。因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点。
当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域。
那样当这段时间里虚拟机要发起垃圾收集时就不必去管这些已声明自己在安全区域内的线程了。
当线程要离开安全区域时,它要检查虚拟机是否已经完成了根节点枚举(或者垃圾收集过程中其他需要暂停用户线程的阶段)。
如果完成了,那线程就当作没事发生过,继续执行;否则它就必须一直等待,直到收到可以离开安全区域的信号为止。
好了,本篇文章到这结束咯。结合上篇可达性分析,我们一步步揭开了GC的神秘面纱,然而路还很远,仍然还有很多东西是需要我们去学习了解的。
感谢阅读,如果本篇文章有任何错误和建议,欢迎给我留言指正。
老铁们,关注我的微信公众号「Java 随想录」,专注分享Java技术干货,文章持续更新,可以关注公众号第一时间阅读。
一起交流学习,期待与你共同进步!
GC的前置工作,聊聊GC是如何快速枚举根节点的的更多相关文章
- 直击面试,聊聊 GC 机制
前言 文章来源:https://studyidea.cn/ GC 中文直译垃圾回收,是一种回收内存空间避免内存泄漏的机制.当 JVM 内存紧张,通过执行 GC 有效回收内存,转而分配给新对象从而实现内 ...
- Java GC 专家系列3:GC调优实践
本篇是”GC专家系列“的第三篇.在第一篇理解Java垃圾回收中我们学习了几种不同的GC算法的处理过程,GC的工作方式,新生代与老年代的区别.所以,你应该已经了解了JDK 7中的5种GC类型,以及每种G ...
- Unity优化之GC——合理优化Unity的GC
转载请标明出处http://www.cnblogs.com/zblade/ 最近有点繁忙,白天干活晚上抽空写点翻译,还要运动,所以翻译工作进行的有点缓慢 =.= PS: 最近重新回来更新了一遍,文 ...
- 深入浅出 JVM GC(4)常用 GC 参数介绍
# 前言 从前面的3篇文章中,我们分析了5个垃圾收集器,还有一些 GC 的算法,那么,在 GC 调优中,我们肯定会先判断哪里出现的问题,然后再根据出现的问题进行调优,而调优的手段就是 JVM 提供给我 ...
- 6. GC 调优(工具篇) - GC參考手冊
进行GC性能调优时, 须要明白了解, 当前的GC行为对系统和用户有多大的影响. 有多种监控GC的工具和方法, 本章将逐一介绍经常使用的工具. 您应该已经阅读了前面的章节: 垃圾收集简单介绍 - GC參 ...
- Android内存优化4 了解java GC 垃圾回收机制2 GC执行finalize的过程
1. finalize的作用 finalize()是Object的protected方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法. finalize()与C++中的析构函数 ...
- GC类型以及不同类型GC的搭配
jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代) jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel ...
- GC 的认识(转) https://github.com/qcrao/Go-Questions/blob/master/GC/GC.md#1-什么是-gc有什么作用
1. 什么是 GC,有什么作用? GC,全称 Garbage Collection,即垃圾回收,是一种自动内存管理的机制. 当程序向操作系统申请的内存不再需要时,垃圾回收主动将其回收并供其他代码进行内 ...
- GC基本算法及C++GC机制
前言 垃圾收集器是一种动态存储分配器,它自动释放程序不再需要的已分配的块,这些块也称为垃圾.在程序员看来,垃圾就是不再被引用的对象.自动回收垃圾的过程则称为垃圾收集(garbage collectio ...
- Unity3D游戏GC优化总结---protobuf-net无GC版本优化实践
protobuf-net优化效果图 protobuf-net是Unity3D游戏开发中被广泛使用的Google Protocol Buffer库的c#版本,之所以c#版本被广泛使用,是因为c++版本的 ...
随机推荐
- 【工作随手记】并发之synchronized
synchronized对于java同学肯定都是耳熟能详的必修课了.但是不管对于新手还是老手都有一些容易搞错的点.这里权做一点记录. 锁的是代码还是对象? 同步块一般有两种写法. 1是直接加以方法体上 ...
- # 代码随想录算法训练营Day4|24.两两交换链表中的节点 19.删除链表的倒数第N个节点 面试题02.07.链表相交 142.环形链表Ⅱ
24.两两交换链表中的节点 题目链接:24.两两交换链表中的节点 总体思路: 两两交换链表中的节点使用虚拟头节点可以更方便地进行交换,这样头节点和普通节点可以以同一种方式进行. 虚拟头结点的建设代码: ...
- 荣登国家级榜单!ShowMeBug创始人李亚飞入选「科创中国·青年创业榜」
近日,中国科协召开2022"科创中国"年度会议,会上发布了2021"科创中国"系列榜单.其中,ShowMeBug 创始人&CEO李亚飞入选2021年科创 ...
- 新版idea快捷键总结学习----(用于java开发模式)
选择代码区 ctrl w 如果放到以if开头的语句,可以选择if判断条件所在的代码片段 游标在单个单词下时 选择单词 在选中多个单词时,选择整个字符串 三次点击时,如果不在字符串单词下,用于选择{}内 ...
- 原来kafka也有事务啊,再也不担心消息不一致了
前言 现在假定这么一个业务场景,从kafka中的topic获取消息数据,经过一定加工处理后,发送到另外一个topic中,要求整个过程消息不能丢失,也不能重复发送,即实现端到端的Exactly-Once ...
- MQ系列12:如何保证消息顺序性
MQ系列1:消息中间件执行原理 MQ系列2:消息中间件的技术选型 MQ系列3:RocketMQ 架构分析 MQ系列4:NameServer 原理解析 MQ系列5:RocketMQ消息的发送模式 MQ系 ...
- 解决NAT模式下SSH连接虚拟机
解决NAT模式下SSH连接虚拟机 简介: 用到的有软件:VirtualBox6.1,RetHat7.4 , SmartTTY 来由: 刚开始使用桥接模式(Bridged)网络连接,但是虚拟机没有网络. ...
- 韩顺平Spring体系化笔记(内含ioc,aop,动态代理等底层原理)
Spring Spring 核心学习内容 IOC.AOP. JdbcTemplate.声明式事务 1.Spring 几个重要概念 Spring 可以整合其他的框架(Spring 是管理框架的框架) S ...
- Mysql数据库体系化详细笔记(b站韩顺平)
Mysql数据库 一.数据库 使用命令行窗口连接MYSQL数据库 mysql服务启动,在cmd输入net start mysql 1.mysql -h主机名-Р端口-u用户名-p密码 2.登录前,保证 ...
- 使用python发送sip协议的OPTIONS
环境:Windows10_x64 Python版本 :3.9.2 sip协议提供了OPTIONS请求方法可用于探测对端状态,今天记录下Windows10环境下使用python3.9简单实现sip ...