判断对象已死-垃圾回收算法-hotspot实现

判断对象已死

引用计数法

缺点:无法判断循环引用

可达性分析

定义:称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的

GC roots:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

关于引用

在jdk1.2 之前,对引用的概念太过于狭窄,我们希望描述一种类型:当内存足够时可以保留,当内存不够时可以抛弃,

jdk1.2之后通过扩展,有了:

  • 强引用

    正常使用的引用,只要引用还在就永远不会回收

  • 软引用

    软引用描述还有用但并非必须的对象,在即将抛出OOM之前进行回收,如果回收之后还无法获得足够的空间就OOM,提供了softReference来实现软引用

  • 弱引用

    被弱引用关联的对象只存活到下一次GC就会被回收,提供weakReference来实现

  • 虚引用

    最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知,提供PhantomReference来实现。

finalize

当一个对象被可达性分析判断可以GC,那么将会被标记,如果该对象覆盖了finalize方法,那么将会把对象放在F-queue队列,等待一个低优先级的Finalize线程来执行finalize方法,但注意如果对象的finalize方法中又使得对象和应用链中的对象重新建立了联系,
注意的点:

  1. 这是因为任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行
  2. finalize方法的用途是为了处理关闭外部资源,所以大多数情况并不需要主要这个方法

永久区(方法区)对象判断

方法区的回收主要是常量和类对象的回收,判断常量是否可回收,是检测是否还有对象引用常量,而判断类的可回收,条件如下

  1. 该类的所有对象都被回收。
  2. 加载该类的classLoardor被回收
  3. 该类的class对象没有被引用

    HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看类加载和卸载信息,其中-verbose:class和-XX:+TraceClassLoading可以在Product版的虚拟机中使用,-XX:+TraceClassUnLoading参数需要FastDebug版的虚拟机支持

对于大量使用classLoader ,如反射,cglib的框架,都需要设定这个参数来保证永久区不会溢出(这让我回想起曾经的热加载)

垃圾回收算法

标记清除

是最基础的回收算法,别的很多算法都基于这个思想,分为两个阶段,标记-清除,具体过程是标记后统一清除。
缺点:

  1. 效率不高
  2. 空间问题,如果没有整理,会产生大量的空间碎片,影响到分配大对象

复制算法

为了解决效率问题和碎片问题,将内存分为两个部分,每次new对象只放在一边,满了之后将还存活的对象复制到另一边,再清理掉旧的一边的内存,解决了碎片问题。现代垃圾回收采用这种算法来处理新生代区域内存。

原因是:新生代的内存朝生夕死,意味着它的存活很短,大部分对象都是需要被gc的,所以并不需要1:1的方式划分新生代,

而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”

标记整理算法

对于老年代,对象存活时间长,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法

分配担保指的是复制算法中如果一边内存的对象被复制去另一边的时候发现另一边空间不够,就需要有另一块内存来保证可以被使用,

所以提出标记整理算法,过程是先标记,然后让所有存活的对象都像另一端移动,然后清理掉端边界以外的内存,

分代回收

根据对象的存活周期将内存划分为不同区域(新生代,老年代),对不同的区域采用不同的回收算法

对于新生代,对象存活时间短,所以可以使用复制算法,而对于老年代,存活时间长,可以使用标记清除或者标记整理算法。

Hotspot的实现

枚举根节点

可达性分析的难点:

  • 目前方法区的范围太大,检查引用会消耗太大的时间
  • 因为并发,导致检查这个步骤必须stop the world

hotspot的实现上,使用准确式GC,即JVM知道内存当中某位置存放什么样的数据,使用oopMap(ordinary Object Pointer Map)普通对象指明Map,在类加载完成之后,JVM 就记录了这个对象实例的多少偏移量存放的是什么类型数据,这样GC的时候就可以直接使用oopMap 来得到引用信息。

安全点

维护oopMap 非常困难(因为并发,而且改变引用是很容易的事),所以hotspot 使用了安全点的概念,安全点可以抽象的看作当前系统运行的一个时间点,在这个时间点进行oopMap 更新和GC,安全点的选择是是否具有让程序长时间执行的特征为标准选择

因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safepoint

这就产生一个问题,在进行GC时会stop the world 来进行可达性分析,那么现在使用了安全点来标记gc的时间点,所以就需要所有线程在进行gc前跑到安全点上(但是否是”所有”线程),有两种方案:

  • 抢先式中断
  • 主动式中断

抢先式中断在GC发生时把所有线程中断,如果发现有线程中断的地方不在安全点,就激活线程,这种方式几乎没有虚拟机使用

主动式中断是当需要GC的时候,不主动中断线程,而是设置标志位,线程轮询标准位来判断是否挂起,而轮询的位置就是安全点位置,和分配对象内存的位置,

安全区域

安全点没办法解决当程序不分配CPU的时候,即程序sleep或blocked的时候,这时无法到达安全点和响应中断,就需要安全区域的概念

简单描述就是在一段代码中,引用关系不变的话,就属于safe region ,在这个区域中进行GC都是可以的,线程会标识自己进入safe region ,这样当进行GC的时候,就不用管这些线程了