参考资料:
https://www.iteye.com/blog/rednaxelafx-1044951
《深入理解Java虚拟机(周志明)》
1. 预备知识:
1.1 虚拟机栈的内存模型
略
1.2 GCRoots
- GC Root是可达性分析的根节点对象
GC Root 有如下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象, 例如局部变量指向的对象等
- 方法区中类静态属性指向的对象
- 方法区中常量引用的对象
- JNI引用的对象
- 虚拟机内部引用的对象
- 被锁持有的对象
- 反映Java虚拟机内部情况的JMXBean, JVMTI中注册的回调,本地代码缓存等
2. 回收对象遇到的问题
在1.1中介绍了很多可达性分析的根节点对象,
除了1.虚拟机栈(栈帧中的本地变量表)中引用的对象,是经常变化的,其他一般比较稳定
虚拟机在以线程为GCRoot的时候,会遇到什么问题:
- JVM的GC在发生的时候,需要保证对象的各种引用非常稳定,这样才能分析出需要GC的对象
- 那么保证引用关系稳定,也就是让栈里面的变量不要乱动,最好的办法就是暂停所有的线程
问题1 : 那么如何在栈上如何获得稳定GCRoot呢?
3. 如何在栈上如何获得稳定GCRoot
答1: 暂停所有线程
- 问题又来了, 如何暂停所有线程
- 暂停线程主要分两种方法, 1. 抢先式中断; 2.主动式中断
3.1 抢先式中断
抢先式中断就是说,在触发GC的时候,不管代码执行到哪,直接把所有线程全部中断
详情: 略
3.2 主动式中断
主动式中断的思想是:
- 当GC触发的时候, 只是设置一个标志位
- 线程执行过程中会有检查点, 当线程执行到检查点的时候, 就会去检查这个标志
- 一旦发现中断标志, 则主动中断挂起
- 这个检查点就被叫做
Safepoint
4. Safepoint
Safepoint,解释器和JIT编译器都支持
- 既然选择了Safepoint作为轮询检查的点,那么肯定不能太密集,如果每执行一条字节码都检查,对JVM负担肯定太重了
- 但是也不能太稀疏,太稀疏会导致,迟迟不能进入Safepoint,会GC的时间变得很长
问2: 那么Safepoint一般会出现在什么位置呢?
答2:
- 循环的末尾
- 方法临返回前/调用方法的call指令后
- 可能抛异常的位置
4.1 如何在Safepoint位置找到GCRoot呢?
好,假设我们这时GC触发了
- 设置GC标志位
- 线程走到Safepoint
- 检查GC标志位,中断线程
- 开始做可达性分析
这里又出现一个问题了:
栈帧中的局部变量表(本地变量表)是以变量槽(Variable Slot, 32bit)为单位的一组内存空间
如果不结合字节码指令,这里的内存区域根本分辨不出来储存的是什么数据类型
这也导致了一个问题
GC收集器, 拿着这个本地变量表一看, 就是一个破byte数组, 根本不知道那一块表示的是对象的指针, 那也就无法确定GCRoot是哪个对象, 吐血
问3: GC收集器是如何找到栈帧中的本地变量表中引用的对象呢?
5. OopMap
答3: Hotspot使用OopMap来标记引用的位置
- 为GC生成的符号信息是OopMap,指出栈上和寄存器里哪里有GC管理的指针
- 数据结构: OopMap{零到多个“数据位置=内容类型”的记录 off=该OopMap关联的指令的位置}
; 例:
;《深入理解Java虚拟机(周志明)》 3.4.1 中的例子
0x026eb7a9: call 0x026e83e0 ; OopMap{ebx=Oop [16]=Oop off=142}
; EBX寄存器 和 栈中偏移量为16的内存区域 中各有一个普通对象指针的引用(Ordinary Object Pointer, OOP)
; 有效范围为: 从 call指令开始直到 0x026e83e0+142=0x026eb7be位置
; 0x026e83e0(指令流起始位置)
; 142(OopMap记录的偏移量)
; 0x026e83e0 ~ 0x026eb7be 这个指令流范围内OopMap生效的区域
6. 比较高效的Safepoint检查
问4: 如果在Safepoint检查GC标志位是个很频繁的操作,怎么才能优化它,让它的操作轻一点呢?
答4:
- 设置某个内存页为不可读
- Safepoint点的指令操作是读这个不可读的内存页
- 触发一个异常信号
- 异常处理器中挂起线程,实现等待操作
- 这样就起到只要一条指令(读指定内存页),即可起到在safepoint检查gc标志位的作用了
7. Safe Region
Safepoint是针对正常执行的线程提出的机制
但是如果线程处于sleep状态或者blocked状态,怎么进入safepoint呢
问5: 如果已经被挂起的线程无法走到safepoint怎么办
答5:
- 对于这种情况,虚拟机引入了安全区域(Safe Region)来解决
安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化, 因此,在这个区域中任意地方开始垃圾收集都是安全的. 我们也可以把安全区域看做被扩展拉伸了的安全点(Safepoint)
当用户线程执行到安全区域里面的代码时, 首先会标识自己已经进入了安全区域,那样当这段时间里虚拟机要发起垃圾收集时就不必去管这些已经声明自己在安全区域内的线程了
当线程要离开安全区域时, 它要检查虚拟机是否已经完成了根节点枚举(或者垃圾收集过程中其他需要暂停用户线程的阶段), 如果完成了, 那线程就当没事发生过, 继续执行;
否则它就必须一致等待, 知道收到可以离开安全区域的信号为止