Java一个优点就是GC(Garbage Collection),虽然它能帮我们管理内存,但是它工作的时候会STW(Stop the World)。也就是停止所有的工作线程,"你们先别干活,我先来清理清理垃圾!"。
那就出问题了啊,你想想比如你在玩游戏的时候,电脑来个STW停个几秒钟时间,清理下垃圾。你可能就会****,你队友可能也会***。
但是现在常用的一些垃圾收集器都得STW,虽然各种垃圾收集器已经各种绞尽脑汁减少STW的时间,但是做不到避免STW。具体的各种垃圾收集器的对比下次单独写一篇文章。
今天先来讲述一下GC是如何快速枚举根节点。在HotSpot虚拟机中,是通过可达性分析来判断此对象是否需要回收的。那可达性分析就需要找到“源头”,也就是根节点。
通过枚举一个一个根节点(GC Roots),然后顺藤摸瓜一路摸下来,然后没摸到的那些对象就把它咔嚓回收了。那这个顺藤摸瓜的过程就必须让世界停止,也就是那些工作线程都得停了,你想想如果不STW那对象引用关系变来变去的,垃圾收集器得怎么咔嚓对象啊,容易咔嚓错了,那咱们使用者不就急眼了啊。所以枚举根节点时STW不可避免,所以只能让STW尽量的短。
根节点主要在全局性的引用(常量、类静态属性)和执行上下文(栈帧中的本地变量表)中。那我们如果要一个一个的找过去就很慢。并且我们的HotSpot又是准确性GC,也就是它需要知道某个位置上的某个数据的类型,类型是准确的。这样它就能准确的知道这块数据类型是不是它关心的指针也就是引用啦!
在HotSpot中是用了一种叫OopMap的结构来存放一个对象内什么偏移量上是什么类型的数据。在类加载过程中就会进行记录。可以把OopMap理解为一个附加信息,或者说一件衣服的吊牌,咱们看吊牌就知道这衣服啥做的。所以GC在扫描的时候就可以直接看这些“吊牌”来知道信息了。
在JIT编译的时候也会在一些特定的位置记录下OopMap,记录了执行到该方法的某条指令的时候,栈上和寄存器里哪些位置是引用,每个方法可能会有好多个OopMap,这是根据特定位置来决定的,这个特定位置会把这个方法分会好几块,每一块都有一个OopMap。
这些特定的位置主要在:
1、方法临返回前/调用方法的call指令后
2、循环的末尾
3、可能抛出异常的地方
这些特定的位置也叫安全点(Safepoint)。
之所以要在特定的位置才记录OopMap,是因为如果对每条指令都记录一下的话,那就会需要大量的空间,提高了GC的空间成本,所以用一些比较关键的点来记录就能有效的缩小记录所需的空间。
因此GC不是随时随地来的,得到达安全点时才可以开始GC。
平时OopMap是压缩在内存中,只要当要GC的时候才会解压出来,然后开始遍历来扫描对应的偏移量。
对于JNI(Java Native Interface)方法,因为本地方法和解释器、JIT编译器没啥关系,所以就没有OopMap。它的引用是加了个中间层一样的,就是句柄,也就是引用不是直接指向堆中的对象,而是引用指向句柄,句柄指向堆中对象。所以GC直接扫描句柄就行了,不需要扫描栈帧。