垃圾回收算法优化细节
想必大家都听过初始标记,并发标记,重新标记这些垃圾不同回收阶段的名词, 这些阶段究竟做了哪些事,到底消耗了多少时间,不知道吧?
好的,我主要从以下三点来分析:
一、如何发起垃圾回收
1、根节点枚举
什么叫根节点呢?根据垃圾回收的可达性算法,垃圾回收会从 GC Roots 这个集合的引用链去寻找回收对象, 根节点就是 这里的 GC Root 对象,那可以被当成 GC Root 对象的又有哪些呢?
所谓“GC roots”,就是一组必须活跃的引用,注意是引用,而不是对象(R大) 包括: 1、所有Java线程当前活跃的栈帧里指向GC堆里的对象的引用;换句话说,当前所有正在被调用的方法的引用类型的参数/局部变量/临时值。 Person p = new Person(); 这个 p 变量是 GC Root
2、比如常量类、静态变量,比如 static 修饰的 hashmap Map<String,String> map = new HashMap(); 这个静态变量 map是 GC Root
3、常量 比如String常量池(StringTable)里的引用
4、所有当前被加载的Java类 及其成员变量 正常情况下 spring 管理的bean 里面的某个成员变量 也是 GC Root 节点
注意是引用,而不是对象
迄今为止,所有的垃圾收集器在搜索 GC Root 这一步骤时都必须暂停用户线程,也就是 stw. stop the world
之所以要 stw 的原因是:为了让系统在某个瞬间,所有的对象引用必须是一致的,所以要让系统静止下来保存一份内存快照似的(并不是真的生成一份快照),让系统整个看起来是一致的,不能有 GC Root 节点还有变化的引用关系存在,这样哪些对象是可以垃圾回收的分析的结果就不准确了。
哪怕是号称停顿时间可控的 CMS G1 ZGC 这些热门的 垃圾回收期,搜索 GC Root 节点时也是必须要停顿的
那如何找根节点呢?是不是直接从元空间找到那些 静态属性的引用就可以了? 是,但不完全是。
JVM 采用了一个叫 ==OopMap== 的数据结构来 保存执行上下文以及对象之间的引用关系。 比如: Person p = new Person(); 这个 new Person() 对象 作为一种强引用,也是 GC Root 节点, 我们经常在 业务代码里面创建这样的对象, 在创建 Person 这个类,在类被加载完成以后,JVM 就会把这个类里的属性类型,偏移量都计算出来,在特定的位置记录下栈空间和寄存器里哪些位置有引用关系, 举个例子:OopMp 记录 Person p = new Person() 关系如下: 0x1453eab7f :call 0xwef2656e0 ; OopMap{ebx= Oop [16] = Oop, off = 142} EBX 寄存器 和栈偏移量为 16 的内存 区域有个对象指针 在 内存地址 0x1453eab7f 到 0xwef2656e0 处记录了 变量 p 的引用关系, 然后 垃圾回收器知道这个信息之后就会在堆空间把 new Person() 这个对象当做 GC Root 节点
这样垃圾收集器在扫描的时候就可以快速的直接找到这些 GC Root 节点。
二、如何保证垃圾回收安全
2、安全点
刚刚我们已经知道了,垃圾回收的时候用户线程是要停下来的,那停在哪里呢(哪行代码)? 安全点是怕 如果随便停在一个地方会有并发不安全发生吗?是,但不完全是~
在 OopMap 的协助下, JVM 可以快速且准确的找到所有的 GC Root 节点,但有一个很现实的问题也随着而来, 对象之间的引用关系是变化的,如果说发生一次引用关系就要导致 OopMap 的内容变化,那就要记录非常多的变化指令,那也会需要大量的额外储存空间,那垃圾收集的代价就太高了,这毫无疑问的会影响垃圾回收的性能。
在特定的代码位置记录 OopMap信息,这样的位置被称为安全点。 为什么要这样呢? 因为引用关系是会变化,就决定了用户程序执行时并非在任意一行代码指令处都停下来进行垃圾收集,而是要到了安全点了才能够停下来。
那哪些地方可以成为安全点呢?如果选择安全点?
方法调用前后,循环跳转,异常。