如何在JVM中基于引用计数法实现GC

avatar
作者
猴君
阅读量:1

一. 为什么使用RC

       再Java虚拟机中,目前广泛使用的是引用计数法,具体详细请见:说说JVM的垃圾回收机制-CSDN博客 而为什么我使用引用计数来实现gc呢?其一是因为可达性分析法中我们需要先收集根集对象,GC roots主要包括:

  1. 再虚拟机栈中引用的对象,即栈帧本地方法表中引用的对象变量
  2. 方法区中静态属性引用的对象
  3. 方法区中常量引用的对象
  4. JNI中引用的对象,即本地方法引用的对象
  5. JVM内部引用,包括基本数据类型对象的Class对象,类加载器对象,异常对象等等。
  6. 所有被同步锁持有的对象。

所以根集的收集是十分麻烦的。同时由于可达性分析法实际上是一种标记算法,即会标注每个对象是否死亡。而后续的内存释放一系列工作则需要使用抽象的分区概念(青年代,老年代,其他书也有伊甸园代等等对分区的称谓)使用标记-清除,复制-清除,标记整理法来进行处理。所以整体实现比较麻烦,然后也会存在 stop the world 来进行暂停gc,此时需要涉及到gc线程,而我的虚拟机并未实现多线程,所以这是其二原由。然后引用计数法相较而言就比较简单了,它是在运行时进行gc的一种方法,即不存在 stop the world 虽然会带来一些内存开销,并且只需要控制一些指令对计数进行增减就可以实现判断该对象是否需要被gc,当计数为0直接释放该对象的内存。当然为什么目前主流JVM不使用引用计数是因为其存在一个较大的问题:循环引用!

二. 说说引用计数发的循环引用缺陷

循环引用是指一组对象互相引用,形成一个闭环,即使这组对象已经不再被外部引用,它们的引用计数也不会降至0,因此它们的内存不会被回收,从而导致内存泄漏(也就是我们所说的漂浮垃圾)。

public class Node {     public Node other; }  public class Main {     public static void main(String[] args) {         Node A = new Node();         Node B = new Node();         A.other = B;         B.other = A;          // 将外部引用设为null         A = null;         B = null;     } } 

循环引用有解决方案吗?当然有!

目前python的gc就是使用的引用计数法,python的解决方案是引入一个周期性运行的循环检测器来处理循环引用(详情见:python虚拟机的GC算法使用引用计数是如何处理循环引用的? - 知乎 (zhihu.com) )

当然本文不深入讨论循环检测器,接下来来详细说说引用计数法。

三. 引用计数的实现

给对象添加一个引用计数器,每当有一个地方引用它时,计数器就加一;当引用失效时,计数器就减一。当计数器为0时,说明对象没有任何引用,即为垃圾对象,可以被回收。代码实现如下:

// AddRef 增加对象的引用计数 func (self *Object) AddRef() { 	self.count++ 	//fmt.Printf("Object name: %v,\tcount: %v\n", self.class.name, self.count) }  // Release 减少对象的引用计数,并在计数为0时释放对象 func (self *Object) Release() { 	self.count-- 	if self.count == 0 { 		self.Free() 	} }  // Free 释放对象占用的资源 func (self *Object) Free() { 	if slots, ok := self.data.(Slots); ok { 		for _, slot := range slots { 			if slot.ref != nil { 				slot.ref.Release() 			} 		} 	} 	// Clear other references to allow GC to reclaim memory 	self.class = nil 	self.data = nil 	self.extra = nil }

然后我们初始化的时候有一个问题需要说明下,其实再对象创建的时候他的引用计数为0,例如:

Class A{ } new A();

此时该对象未赋给任何值,所以计数为0,只有当它被赋给某个变量时,才会计数++,(数组类对象除外,在java编译器对这里有严苛的语法要求,数组类对象创建后必须复制给某个变量)。我为了便于操作,且一般创建对象我们都会赋给某个变量,所以我就直接初始化为1了。接下来就是讨论计数加减的时候,具体参考jvm规范:

计数++:

  1. 当对象被创建并有变量引用它时:例如,Object obj = new Object();在这里,新创建的对象被obj变量引用,所以引用计数加一。

  2. 当对象被另一个对象引用时:例如,Object obj2 = obj;在这里,obj2变量也引用了obj所指向的对象,所以引用计数再次加一。

  3. 当对象被作为方法参数传递时:例如,在调用someMethod(obj);时,obj被作为参数传递给了方法,所以引用计数加一。

  4. 当对象被作为方法返回值返回时:例如,在return obj;中,obj被作为返回值返回,所以引用计数加一。

计数--:

  1. 当一个对象的引用被赋值为null时:例如,Object obj = new Object(); obj = null;。在这种情况下,obj不再引用先前创建的对象,因此该对象的引用计数减一。

  2. 当对象的引用超出其作用域时:例如,如果一个对象只在一个方法内部被引用,那么当该方法执行完毕后,该对象的引用计数应该减一。

  3. 当一个对象被另一个对象的字段引用,然后这个字段被赋值为另一个对象或null时:例如,obj1.field = obj2; obj1.field = null;。在这种情况下,obj2的引用计数减一。

  4. 当一个对象作为方法参数,方法执行结束后:例如,void someMethod(Object param) { ... },在方法执行结束后,param的引用计数减一。

落实到指令上,无非就是如下几个:

  1. newanewarraymultianewarray :会创建一个新的对象,并将引用计数设置为1
  2. astoreaastoredup : 会增加一个新的引用,所以引用计数需要增加。
  3. aconst_null、astoreaastore原来的引用被消除,所以原来引用的计数需要减少。
  4. invokevirtualinvokeinterfaceinvokespecialinvokestatic:调用一个方法时,会将对象引用传递给方法作为参数,这会增加一个新的引用。当方法返回时,如果返回值是一个对象引用,那么这个引用会被复制到调用者的操作数栈中,这也会增加一个新的引用。相反,如果方法返回后,原来的参数引用不再被使用,那么这些引用需要被消除,所以引用计数需要减少。
  5. 异常处理,未实现暂不考虑。

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!