GC Roots
可达性分析, 可达到的地方是GC Roots, 只要与GC Roots之间存在引用链, 则说明这个对象有可达性的, 这个对象不应该被回收, 固定可以作为GC Roots的对象有以下几种:
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等。
- 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
- 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
- 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
- 所有被同步锁(synchronized关键字)持有的对象。
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
但是由于G1收集器这种会局部回收的收集器的存在, 所以不同区块之间的对象也会有引用关系, 那么就会将非回收区域的并且对回收区域有引用的对象也加入GC Roots集合中
OopMap
OopMap 记录了栈上本地变量到堆上对象的引用关系。其作用是:垃圾收集时,收集线程会对栈上的内存进行扫描,看看哪些位置存储了 Reference 类型。如果发现某个位置确实存的是 Reference 类型,就意味着它所引用的对象这一次不能被回收。所以在HotSpot中就采用了这个OopMap的数据结构, gc 发生时,程序首先运行到最近的一个安全点停下来,然后更新自己的 OopMap ,记下栈上哪些位置代表着引用.
OopMap的作用:
- 加快了枚举根节点的速度, 这样枚举根节点的时候只需要递归遍历OopMap中的引用对象的地址, 就可以将这些对象加入到GC Roots集合中
- OopMap也可以帮助HotSpot进行准确式GC
是什么东西“准确”呢?关键就是“类型”,也就是说给定某个位置上的某块数据,要能知道它的准确类型是什么,这样才可以合理地解读数据的含义;GC所关心的含义就是“这块数据是不是指针”。
要实现这样的GC,JVM就要能够判断出所有位置上的数据是不是指向GC堆里的引用,包括活动记录(栈+寄存器)里的数据。
在这里给出一篇HotSpot虚拟机团队开发人员的文章找出栈上的指针/引用
G1收集器
这里就用G1收集器的收集方式, 来对可达性分析做一个阐述
初始标记
这是回收过程的第一个动作, 这一步中会标记一下GC Roots能直接关联到的对象, 并且修改TAMS指针的值, 让并发标记阶段的用户线程可以在可用的region中分配新的对象
TAMS指针是为了并发标记而生的, 在G1中的每个Region中都会存在这么两个指针, 划分出一块区域用于并发标记时给新对象分配的空间
这一步是会触发停顿的
并发标记
在并发标记这一阶段, 会扫描整个GC Roots的引用链, 并且会跟用户线程并发进行, 所以会产生新的引用生成或者旧的引用消失的情况, 所以这里就要讲一下G1收集器或者其他的收集器在并发标记这一阶段是如何处理这些情况的
并发的可达性分析
在并发标记的时候, 如果已经死亡的对象我们标记为存活, 这虽然糟糕但是并不会导致很严重的后果, 但是如果把存活的对象标记为死亡, 那就会导致很严重的后果, 这里用一个三色标记图来展示一下情况
·白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是 白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。
·黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代 表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对 象不可能直接(不经过灰色对象)指向某个白色对象。
·灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。
可以说白色的是要被回收的, 黑色是不会被回收的, 灰色是白色到黑色的一个中间态, 最后会变黑的
这里引用一张深入理解java虚拟机中的图
如果单纯的线性扫描下去, 在并发过程中, 就可能会产生最后两图中的情况, 这会导致本该存活的对象被可达性分析判定为死亡
Wilson于1994年在理论上证明了,当且仅当以下两个条件同时满足时,会产生“对象消失”的问 题,即原本应该是黑色的对象被误标为白色:
·赋值器插入了一条或多条从黑色对象到白色对象的新引用;
·赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。
所以我们只需要破坏上述两个条件中的其中一条就可以保证对象不会消失
增量更新
当一个黑色的对象获得一个新的引用关系的时候, 就将这个引用关系记录下来, 在后面的最终标记阶段将这些产生了引用关系的黑色对象变为灰色, 并且重新开始一个扫描过程, 如此一来保证了新的引用也会被标记为黑色, 按我的理解增量更新是不会产生浮动垃圾的, 也就是不会产生上述的对象应该死亡却存活的情况
原始快照
当一个灰色对象删除了对一个白色对象的引用后, 就将这个删除的引用记录下来, 并发标记结束之后, 会将这些原本的灰色对象按照删除之前的引用重新扫描一次, 这就保证了整个扫描是按照扫描刚开始的图来进行扫描的, 也会导致本该死亡的对象却存活的情况
最终标记
在最终标记这个阶段中, 用户线程是无法进行工作的, G1会用多线程并行使用原始快照重新扫描
筛选回收
这是垃圾回收操作的回收阶段, 根据各个Region的回收价值以及回收的成本进行排序, 从而达到用户所期望的停顿时间来制定回收的计划, 所以说G1的回收动作在全局上看是采用复制算法的, 在这个阶段会选择需要回收的Region构成回收集, 然后把这些Region里面的存活对象复制到空的Region中, 之后直接清理掉整个回收集的空间. 由于这里的回收动作是涉及到对象的移动的, 所以必须要暂停掉用户线程, 采用多线程的形式来回收Region
所以对于G1收集器的收集策略来说, 并不追求将整个堆内存进行清理, 而是通过可控的停顿时间达成更好的用户体验, 只要我扫垃圾的速度比你丢垃圾的速度更快, 那这个房间就永远不可能堆满垃圾
记忆集与卡表
在进行回收的时候, 为了避免将整个其他分区中的对象加入GC Roots扫描, 就会在分区中建立一个记忆集的数据结构, 这个数据结构主要是记录从非收集区域指向收集区域的指针(跨代指针)集合的抽象数据结构, 而卡表就是这个抽象数据结构的具体实现
卡表很像操作系统中所使用的页表, 卡表中每一个元素都对应着其标识的内存区域中的一块特定大小的内存块, 这个内存块被称为卡页, 卡页的大小是2^N字节, 而卡页中存放着一个或者多个对象, 而这些对象的字段如果存在着跨代的指针的话, 那么与之对应的卡表元素就会变"脏", 卡表元素初始是0, 变"脏"则是1.
所以想要找跨代指针, 只需要找卡表中为1的元素, 找到这些卡表对应的卡页, 然后把卡页中的元素加入GC Roots中去扫描就可以避免将整块分区加入GC Roots了