最近开始回顾整理一些Jvm的知识点,记录一下,如有描述不准确的地方还望大家评论指出,共同进步。

一、可达性分析算法

  在Jvm的HotSpot虚拟机中使用的是可达性分析算法来确定内存中的对象是否要被回收,那么首先来说一下可达性分析算法是怎么玩的呢?他的基本思路就是通过一系列成为GC Roots 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路被称为引用链。

如果某个对象到GC Roots间没有任何引用链相连,那么就证明这个对象是不可能再被使用的,就可以判定这样的对象是可以被回收的。

  在Java技术体系中,固定可以被作为GC Roots的对象包括以下几种:

  • 在虚拟机栈(栈的本地变量表)中引用的对象
  • 在方法区中静态属性引用的对象
  • 在方法区中常量引用的对象
  • 在本地方法栈中引用的对象
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(NPE),类加载器等等
  • 所有被同步锁持有的对象
  • 反应Java虚拟机内部情况的JMXBean,JVMTI中注册的回调,本地代码缓存等

上面列出的这些都是固定可以作为GC Roots的集合,除此之外根据用户选取的垃圾收集器以及当前内存回收的区域不同,还可以有其他对象被添加到 GC Roots的集合中,共同作为完成的GC Roots来进行可达性分析的起始点。

 

在可达性分析阶段为了方便理解,会将堆中的对象用三种颜色进行标注:

  • 白色:标记的初始阶段,所有都想都没有被扫描
  • 黑色:表示对象已经被扫描过,且所有引用都已经被扫描过
  • 灰色:对象中的一部分引用已经被扫描过

所以我们可以这么去理解:灰色阶段是一个中间态的阶段,最终所有的对象只能是黑色或者是白色,黑色的对象就是存活的对象,白色的就是需要被清除的。

但是我们知道在进行GC的过程中,除非是STW否则我们无法保证用户的线程不去改变已经被扫描和确认过的对象,因为GC的进程是与用户进程并发进行的。

那么这里就有可能出现两种情况:

  • 原本是白色的对象,被当作了黑色的对象
  • 这样的结果就是本来应该被清除的对象却被保留了下来,但是这个结果其实我们是可以接受的,这次没有被清除的,下次再被清除就可以了,这些对象就是我们经常说的浮动垃圾
  • 原本是黑色的对象,被当作了白色的对象
  • 这个就比较麻烦了,因为一旦被当作成白色的对象,那就是需要被清除的,这样就会导致程序出现错误。

 

Wilson在1994年通过理论证明了,当且仅当同时满足以下两个条件的时候,会将原本是黑色的对象误标记为白色的对象:

  • 赋值器插入了一条或多条从黑色对象到白色对象的新引用
  • 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用

这里应该这么理解:首先是需要同时满足这两个条件,其实这两个条件说的是一个过程,就是如果一个对象首先断了与灰色对象全部的直接和间接的联系,那么这个时候这个对象就肯定是白色的了,然后这个对象再与被一个黑色对象所引用。而我们知道如果在GC标记过程中,已经被标记为黑色的对象是不会被再次扫描的,那么这个对象即使已经被黑色对象所引用,但是因为这个黑色对象不会再次被扫描,所以这个对象仍然是白色的,所以在下次GC执行的时候就会被清除掉,所以就会产生程序级的错误了。

 

既然可能出现的问题我们理解清除了,就要知道怎么来处理这种问题。在Hotspot中提供了两种方式来解决这个问题:

  • 增量更新:当黑色对象添加了指向白色对象的引用关系时,会将这个引用记录下来,等并发扫描之后,再将这些黑色结果作为根节点重新扫描
  • 原始快照:将从灰色阶段删除的引用记录下来,扫并发扫描之后再将这些灰色节点作为根节点再扫描一次

 

二、哪些对象会被临时性的添加到GC Roots中?

  上文提到了,除了固定GC Roots的节点外 ,还会有一些其他节点会被临时的添加到GC Roots中,那么到底有哪些节点会被临时的添加进去呢?

这里首先要提到一个分代收集的理论,比如我们这次要回收新生代,或者是G1中我们要回收一个指定的Region的时候,我们肯定是不希望把所有的GC Roots都进行可达性分析的,因为这样比较浪费资源。

比如我们这里想要回收新生代,那么我们会首先找到存在于新生代中的GC Root节点,通过引用链的方式去寻找可达对象,这样分析完之后我们可以确定哪些对象是可达的,哪些是不可达的。但是这里存在一个问题,

就是这里我们只是选取了存在于新生代中的GC Root节点作为根节点开始的分析,那么剩下的没有被可达的对象,是有可能存在从老年代中的引用的,所以如果这个时候我们直接把这些没有能够从新生代GC Root节点可达的对象

标记为可删除,那么就有可能误杀一些对象。

  所以基于上面的分析,在进行新生代回收的时候,我们首先选择存在于新生代中的GC Root添加到集合中,然后再看有没有存在于老年代的跨代引用,如果存在那么就需要把老年代中的部分对象作为GC Root添加到集合中,用这些节点一起共同进行可达性分析

通过这种方式进行筛选进行可达性分析之后依然不可达的对象,我们就可以放心的标记为不可达,在后面的回收过程中会把这样的对象回收掉。

三、怎么知道新生代中的对象是否被老年代引用了呢?

   在Jvm中是通过一种叫做Remembered Set的数据结构来记录跨代引用的,用以避免在进行新生代GC的时候把整个老年代都添加GCRoots的扫描中。当然这里不仅仅指新生代与老年代中的引用,在后面出现的一些不分代的垃圾收集器中也用到这个数据结构,比如G1中的Region概念等等。

Remembered Set是一种用于记录从非收集区指向收集区与中指针的集合的一种抽象数据结构,关于怎么记录这些指向关系,在Jvm中定义了几种不同的精度:

  • 字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数),该字包含跨代指针
  • 对象精度:每个记录精确到一个对象,该对象里有字段汉有跨代指针
  • 卡精度:每个记录精确到一块内存区域,该区域有对象含有跨代指针

其中的第三种精度是使用一种叫做卡表的方式实现的Rembered Set,卡表的底层实现就是一个数字,每个数组元素对应的是内存中一块特定大小的内存块,这个内存块被称为卡页,,一般来说卡页的大小都是2的N次幂的字节数,在HotSpot中一个卡页的大小是2^9,所以对应的在数组中就会把内存区域按照2^9进行划分成多个内存块。

这样我们就可以知道在一个卡页中会存在很多个对象,只要卡页中有一个对象的字段存在着跨代引用,那么就想卡表对应的数组元素设置为1,这个页就叫做脏页。然后在接下来进行垃圾回收的时候,只要首先找到脏页对应的卡页,然后再找到里面存在跨代引用的对象,把他们添加到GC Roots中一起进行可达性分析,这样就可以保证待分析的区域不会出现对象误杀的情况了。

四、怎么快速找到固定的GC Root呢?

  上面聊了一些关系对象可达性的内容,但是我们知道在有与用户交互的场景中我们更多的是关心STW的时间,但是现在的Jvm的内存是越来越大了,那么怎么快速的找到GC Root对象呢?很显然扫描整个方法区或者是虚拟机栈是不现实的想法。

在选取GC Roots的时候最不好确定的就是在栈中的对象,快速的判断和找出在栈中指向堆中的对象,是影响GC STW的关键,就是在这个环节Jvm必须要暂停所有的用户线程,在一个相对静止的快照进行分析,所以这个环节在寻找GC Root的用时多少就决定了STW的时间。

这里HotSpot采用了一种OopMap的数据结构来记录哪个地方存的是引用,这样在进行GC Root标记的时候,直接扫描OopMap,就可以快速的确定GC Root,从而减少STW的时间了。

  首先我们要知道在OopMap中要记录两种情况,一种是对象,一种是方法。如果是对象,那么在这个类被加载完成之后,那么类对象内在什么位置保存的是引用的对象就可以提前知道了,这样在线程栈中就可以通过OopMap来记录对应的引用对象的地址信息。第二种就是方法,我们知道在栈中

每个方法对应一个栈帧,在方法的内部会存在引用关系的变化,这时会根据安全点的原则进行OopMap的记录,这样在每个线程栈上都有一个对应的OopMap , 通过扫描这些OopMap 其实我们就可以快速的定位到在堆中的GCRoot对象,为后面进行可达性标记分析提供支持。 

五、写屏障

  前文我们知道通过Remembered Set可以解决跨代引用的问题,但是怎么实现跨代引用出现的时候同时更新Remembered Set的呢?这里HotSopt中采用了写屏障的技术,首先我们思考一下应该在什么时候更新Remembered Set呢?应该是在对象赋值的那一刻去更新才对,

所以写屏障就是在引用类型赋值的时候,添加一个AOP切面,这样在赋值动作发生的时候会产生一个Around通知,而更新Remembered Set的工作就是在这个里面来实现的,所以如果开始了写屏障是会增加一些时间消耗的。