【垃圾回收器】基于Go实现引用计数法(ReferenceCount)

news2025/1/11 12:59:12

不想传火的,可以点击下面的链接!

github:GCByGO

给我点赞嘛,球球了!
请添加图片描述

What This?

现象

引用计数法是一种垃圾回收算法,用于跟踪对象被引用的次数。在该算法中,每个对象都会维护一个计数器,记录有多少个指针指向它。

当一个新指针引用一个对象时,该对象的计数器就会加1,当指针被释放时,对象的计数器就会减1。

当对象的计数器变为0时,说明该对象没有任何指针指向它,也就意味着该对象可以被回收。

引用计数法会定期扫描内存中的对象,并回收计数器为0的对象。

优缺点

  • 优点(快):引用计数法的优点是回收垃圾的时机可以很快,因为对象的计数器是在实时更新的。
  • 缺点(循环引用):是如果存在循环引用,即两个或多个对象互相引用,但是没有其他对象引用它们,那么这些对象的计数器永远不会变为0,也就无法回收它们,会导致内存泄漏。

因此,引用计数法通常需要与其他垃圾回收算法一起使用,以解决循环引用的问题。

举例

肯定有人没耐心看文字!不过谁叫我人心善呢,见不得你们这么痛苦。

在这里插入图片描述

计数器的构成其实很简单,就是只有计数器和域,

  • 域:专门存放自身节点与其他节点的引用关系,即自己的指针->其他节点
  • 计数器:统计被引用的次数
    在这里插入图片描述

下面的两幅图就很形象了。

一开始我们创建了ABCD这四个节点,其中C被A引用
请添加图片描述
然后修改A->C变成A->B之后的结果如下;
请添加图片描述
你学废了吗?

实现

伪代码

update_ptr() 函数:用于更新指针 ptr,使其指向对象 obj,同时进行计数器值的增减。
注意:

这里的ptr有两重含义,意思就是如上图的修改A->C变成A->B涉及到的两个不同的指针。

另外要保证先增加后减少,这样就可以避免ptrobj是同一个对象的情况所导致的被误删。
在这里插入图片描述
inc_ref_cnt() 函数:计数器增量操作。
在这里插入图片描述
dec_ref_cnt()函数:计数器减量操作。
在这里插入图片描述

GO实现

目录结构

ReferenceCount
	|		|
	|	main.go
	|
	----->PKG
		   |
		   ------>allocate.go
		   ------>createData.go
		   ------>freeLinkedList.go
		   ------>object.go

链表freeLinkedList.go

定义了一个 FreeLinkedList 的数据结构,它是一个基于链表的数据结构,其中的节点被定义为 Node 结构体。链表的头节点是一个指向第一个节点的指针。

Node 结构体包含了一个 data 属性,用于存储节点的数据值,以及一个 next 属性,指向下一个节点。String() 方法实现了将节点的数据值格式化输出的功能。

type Node struct {
	data interface{}
	next *Node
}

type FreeLinkedList struct {
	head *Node
}

这个 FreeLinkedList 数据结构提供了以下几个方法:

  • insertAtEnd:在链表的末尾插入一个新的节点,新节点的数据值是传入的参数。
func (list *FreeLinkedList) insertAtEnd(data interface{}) {
	newNode := &Node{data: data}
	if list.head == nil {
		list.head = newNode
	} else {
		current := list.head
		for current.next != nil {
			current = current.next
		}
		current.next = newNode
	}
}
  • deleteNode:删除链表中的一个节点,被删除节点的数据值是传入的参数。
func (list *FreeLinkedList) deleteNode(data interface{}) {
	if list.head == nil {
		return
	}
	if list.head.data == data {
		list.head = list.head.next
		return
	}
	prev := list.head
	current := list.head.next
	for current != nil {
		if current.data == data {
			prev.next = current.next
			return
		}
		prev = current
		current = current.next
	}
}
  • printLinkedList:打印整个链表,从头节点开始依次输出每个节点的数据值。
func (n *Node) String() string {
	return fmt.Sprintf("%v", n.data)
}

func (list *FreeLinkedList) printLinkedList() {
	current := list.head
	for current != nil {
		fmt.Printf("%v->", current.data)
		current = current.next
		if current == nil {
			fmt.Print("nil\n")
		}
	}
}

核心代码object.go

定义了一个 Object 结构体,它包含了一个编号 no,一个引用计数器 refCnt,一个字节数组 data,以及一个子对象数组 children

type Object struct {
	no       string
	refCnt   int
	data     []byte
	children []*Object
}

该结构体提供了以下方法:

  • getInterface:返回编号和 data 字节数组的长度。这里之所以要写一个getter方法,是因为我之前在链表定义它存储data的类型是interface

因为在Go语言中, interface{}是一种统一类型,可以用来表示任何类型的值。
但是,由于interface{} 没有定义任何方法或属性,因此无法直接在其上
访问任何属性或方法。如果你希望在一个interface{}类型的变量上调用
属性,先将其转化成方法的类型,然后又一个执行某个结构体的interface{}类型的变量,
而该结构体具有GetInterface方法,使用断言转换获得其值。。

func (obj *Object) getInterface() (string, int) {
	return obj.no, len(obj.data)
}
  • updatePtr:更新一个指向 Object 的指针,同时更新相关的引用计数器,并将原始指针所指向的对象从其子对象数组中移除,并且在其引用计数器降为0时将其添加到空闲链表中。
func (obj *Object) updatePtr(oldptr *Object, ptr *Object, list *FreeLinkedList) {
	if ptr == nil {
		return
	}
	ptr.incRefCnt()
	oldptr.decRefCnt(list)
	obj.children = []*Object{ptr}
}
  • incRefCnt:增加引用计数器。
func (obj *Object) incRefCnt() {
	if obj == nil {
		return
	}
	obj.refCnt++
}
  • decRefCnt:减少引用计数器。如果引用计数器降为0,则对其所有子对象递归调用 decRefCnt 方法,并将其本身添加到空闲链表中。
func (obj *Object) decRefCnt(list *FreeLinkedList) {
	if obj == nil {
		return
	}
	obj.refCnt--
	if obj.refCnt == 0 {
		for _, child := range obj.children {
			child.decRefCnt(list)
		}
		obj.reclaim(list)
	}
}
  • AddRef:添加一个子对象指针,并增加其引用计数器(注意这里,创建对象时,我不会让refCnt变成1的)。
func (obj *Object) AddRef(ptr *Object) {
	if ptr == nil {
		return
	}
	obj.children = append(obj.children, ptr)
	ptr.incRefCnt()
}
  • reclaim:将对象添加到空闲链表中,并打印一条信息。
func (obj *Object) reclaim(list *FreeLinkedList) {
	obj.children = nil
	fmt.Printf("%s has been reclaimed\n", obj.no)
	//这里就加入空闲链表中
	list.insertAtEnd(obj)
}

分配allocate.go

实现了一个内存分配和回收机制。其中:

  • newObject 函数:接收对象的编号和大小,然后尝试从空闲链表中找到可用的空间分配给对象,并返回对象的指针。如果没有找到可用的空间,则会触发 allocation_fail 函数抛出 panic
func newObject(no string, size int, list *FreeLinkedList) *Object {
	obj := pickupChunk(no, size, list)
	if obj == nil {
		allocation_fail()
	} else {
		//注意我这里跟书本的伪代码不一样,因为我认为一开始新建的默认为0,而只要被引用了才改变refCnt
		obj.refCnt = 0
		return obj
	}
	return nil
}
  • pickupChunk 函数:在空闲链表中查找是否有足够大小的空间。如果找到,则返回一个对象指针,否则返回 nil。该函数还可以对剩余的空间进行分割并将其返回到空闲链表中。
func pickupChunk(no string, size int, list *FreeLinkedList) *Object {
	current := list.head
	for current != nil {
		var object interface{}
		temp := current.data
		object = temp
		if ms, ok := object.(*Object); ok {
			oldNo, allocate_size := ms.getInterface()
			if oldNo != "head" {
				if allocate_size == size {
					list.deleteNode(object)
					return &Object{no: no, data: make([]byte, size)}
				} else if allocate_size > size {
					list.deleteNode(object)
					remainingChunk := &Object{
						no:   oldNo,
						data: make([]byte, allocate_size-size),
					}
					list.insertAtEnd(remainingChunk)
					return &Object{no: no, data: make([]byte, size)}
				} else {
					allocation_fail()
				}
			}
		}
		current = current.next
	}
	return nil
}
  • mergeChunk 函数:会遍历空闲链表中的所有对象,并将它们合并为一个大的空闲块。合并后的块会添加到链表的末尾。
func mergeChunk(list *FreeLinkedList) {
	current := list.head
	var totalSize int = 0
	for current != nil {
		var object interface{}
		temp := current.data
		object = temp
		if ms, ok := object.(*Object); ok {
			//allocate_size可分配的
			oldNo, size := ms.getInterface()
			if oldNo != "head" {
				list.deleteNode(object)
				totalSize += size
			}
		}
		current = current.next
	}
	newNode := &Object{no: "No.0", data: make([]byte, totalSize)}
	list.insertAtEnd(newNode)
	list.printLinkedList()
}

注意

代码中还涉及到对象的引用计数 (refCnt) 的处理,当对象被新建时,它的引用计数被初始化为0;当对象被引用时,它的引用计数会增加;当对象不再被引用时,它的引用计数会减少。
当对象的引用计数变为0时,它就可以被回收了。
此外,当对象被回收时,它的子对象也会被递归回收。

生成数据和执行createData.go

利用前面定义的各种函数来模拟内存分配的过程。

具体地,代码中定义了一个名为 Execute 的函数,它接受一个参数 BASE_SPACE 表示可分配的初始空间大小,然后调用 InitData 函数来初始化空闲块链表。接着,它调用 Example1 函数模拟内存分配的过程,最后调用 mergeChunk 函数来合并已释放的空闲块,以减少内存碎片。

package PKG

import "fmt"

//书本图3.2的例子
//Num 数量,Size 总需要的空间
var Num int = 3
var Size int = 6
var heap []*Object

func Example1(Num, Size int, list *FreeLinkedList) {
	avgSize := Size / Num
	root := &Object{no: "root", refCnt: 1, data: make([]byte, 0)}
	for c, ch := 'A', 'A'+rune(Num); c < ch; c++ {
		heap = append(heap, newObject(string(c), avgSize, list))
	}
	//root->A
	root.AddRef(heap[0])
	//root->C
	root.AddRef(heap[2])
	//A->B
	heap[0].AddRef(heap[1])
	fmt.Println("创建书本图3.2(a)中update_prt()函数执行时的情况")
	fmt.Println(root)
	fmt.Println(heap[0])
	fmt.Println(heap[1])
	fmt.Println(heap[2])
	//让A->B => A->C
	heap[0].updatePtr(heap[1], heap[2], list)
	fmt.Println("最终结果显示正确,执行图3.2(b)的结果")
	fmt.Println(root)
	fmt.Printf("%p", heap[0])
	fmt.Println(heap[0])
	fmt.Printf("%p", heap[1])
	fmt.Println(heap[1])
	fmt.Printf("%p", heap[2])
	fmt.Println(heap[2])
}

//BASE_SPACE:初始可分配空间大小,以一个链表节点的形式出场
func InitData(BASE_SPACE int) *FreeLinkedList {
	head := &Node{data: &Object{no: "head"}}
	node0 := &Object{no: "No.0", data: make([]byte, 10)}
	list := &FreeLinkedList{head: head}
	list.insertAtEnd(node0)
	list.printLinkedList()
	return list
}

func Execute(BASE_SPACE int) {
	list := InitData(BASE_SPACE)
	Example1(Num, Size, list)
	list.printLinkedList()
	mergeChunk(list)
}

Example1 函数中,代码首先计算出每个块的平均大小,然后通过循环来创建多个块,并将它们添加到一个名为 heap 的切片中。接着,代码利用 AddRef 函数来建立指针之间的关系,模拟内存中对象之间的引用关系。具体地,代码先将 root 指向第一个块 heap[0],然后将 heap[0] 指向第二个块 heap[1],最后将 root 指向第三个块 heap[2]

就是下图的(a)
在这里插入图片描述

接下来,代码调用 updatePtr 函数来修改指针的指向,模拟内存中对象的移动。具体地,代码将 heap[0] 指向的对象从 heap[1] 移动到 heap[2],因此 heap[0] 应该指向 heap[2] 而不是 heap[1] 了。最后,代码打印出各个块的状态,以展示内存分配和指针移动的过程,即上图的(B)

最后,代码调用 mergeChunk 函数来合并已释放的空闲块,以减少内存碎片。具体地,代码利用循环来遍历整个空闲块链表,将每个空闲块的大小累加起来,并将它们合并成一个大块。最后,代码将这个大块插入到链表末尾,并打印出链表的状态。

执行结果main.go

package main

import (
	L "GCByGo/ReferenceCount/achieve/Considered/PKG"
)

func main() {
	L.Execute(10)
}

讲解结果:

在这里插入图片描述

可恶!被打脸了(13秒能有多快?

在这里插入图片描述

一开始显示的是空闲链表中可分配的空间,你可以看到[0 0 0 0 ....]这个数据,其实就代表了域,不过我实际上是创建多了一个children切片来存放引用关系的,因此域本身没什么含义。

&{head 0 [] []}->&{No.0 0 [0 0 0 0 0 0 0 0 0 0] []}->nil

接着就是创建书本图3.2(a)的例子,遵循着如下的结构:

&{名称 计数器 域的数值 [引用的地址]}

应该很直观吧?

&{root 1 [] [0xc000100230 0xc000100370]}
&{A 1 [0 0] [0xc0001002d0]}
&{B 1 [0 0] []}
&{C 1 [0 0] []}

然后我就清掉了B,将他放入到空闲链表中

B has been reclaimed

最终结果显示正确,执行图3.2(b)的结果,其中像0xc000100230这些意义不明的值就是地址,可以结合上面的结果知道,我们的A一开始指向B的地址0xc0001002d0,后面改了变成C的地址0xc000100370随之变化的是计数器的数值

&{root 1 [] [0xc000100230 0xc000100370]}
0xc000100230&{A 1 [0 0] [0xc000100370]}
0xc0001002d0&{B 0 [0 0] []}
0xc000100370&{C 2 [0 0] []}

再次看回这张图片:
在这里插入图片描述
让B进入到空闲链表中:

&{head 0 [] []}->&{No.0 0 [0 0 0 0] []}->&{B 0 [0 0] []}->nil

不过呢,我遵循着不浪费的原则,就把并入进空闲链表的B分块给合并掉了,于是得到下面的这个结果。

&{head 0 [] []}->&{No.0 0 [0 0 0 0 0 0] []}->nil

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/482494.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

PyTorch:深度学习框架的优雅演进与设计理念

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️ &#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…

【Linux】文件与路径

一、Linux相关软件 xftp&#xff1a;用来传文件 xshell&#xff1a;用来敲命令 二、Linux的文件结构 windows系统下设有盘符&#xff1a;如C盘、D盘等&#xff0c;Linux没有盘符的概念&#xff0c;只有一个根目录/&#xff0c;所有文件都在它下面。 在根目录下输入命令ls&am…

PHP 入门学习笔记

现在如果问什么行业最火&#xff0c;很多人第一反应肯定就是IT。的确&#xff0c;这些年随着互联网的不断发展&#xff0c;IT热门众所周知。那么就一起来说说&#xff0c;IT行业里&#xff0c;哪些技术更热门。 一、PHP技术&#xff1a; PHP 是一种创建动态交互性站点的强有力…

2. C 语言基础

2. C 语言基础 常考面试题 int main(int argc, char ** argv)函数中&#xff0c;参数argc和argv分别代表什么意思&#xff1f;⭐⭐⭐⭐ 第一个参数&#xff0c;int型的argc&#xff0c;为整型&#xff0c;用来统计程序运行时发送给main函数的命令行参数的个数。 第二个参数&am…

深度遍历模版与广度遍历模版

深度优先遍历 //void dfs(中间容器&#xff0c;数据) //{ // if(临界走到末尾) // { // 中间容器加到最终容器 // return&#xff1b; // } // for(做选择) // { // 改变中间容器 // dfs(中间容器&#xff0c;数据) // 撤回…

Linux下编译MySQL++/mysqlpp

一、简介 MySQL&#xff08;又名mysqlpp&#xff09;是对MySQL和MariaDB C api的c封装。它建立在与标准c库相同的规范之上&#xff0c;使得处理数据库与处理std容器一样简单。MySQL还提供了一些功能&#xff0c;使用户可以在自己的代码中避免最重复的SQL排序&#xff0c;为这些…

springboot 整合rabbitMq保证消息一致性方案

rabbitMq介绍 RabbitMQ是一种开源的消息代理软件&#xff0c;它实现了高级消息队列协议&#xff08;AMQP&#xff09;标准&#xff0c;可用于在应用程序之间传递消息。RabbitMQ最初由LShift开发&#xff0c;现在由Pivotal Software维护。 RabbitMQ可以在多个平台上运行&#x…

计算机网络笔记:TCP协议 和UDP协议(传输层)

TCP 和 UDP都是传输层协议&#xff0c;他们都属于TCP/IP协议族。 TCP 基本概念 TCP的全称是传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP 是面向连接的、可靠的流协议&#xff08;流就是指不间断的数据结构&#xff09; TCP报文格式 TCP报文是…

图神经网络:在Cora上动手实现图神经网络

文章说明&#xff1a; 1)参考资料&#xff1a;PYG官方文档。超链。 2)博主水平不高&#xff0c;如有错误还望批评指正。 3)我在百度网盘上传了这篇文章的jupyter notebook。超链。提取码8888。 文章目录 代码实操1&#xff1a;GCN的复杂实现代码实操2&#xff1a;GCN的简单实现…

C++语言练习题位运算

位运算(01)基础 位运算(02)从一个 16 位的单元中取出某几位 题目描述 从一个 16 位的单元中取出某几位&#xff08;即该几位保留原值&#xff0c;其余位为 0. 使用 value 存放该 16 位的数&#xff0c;n1 为欲取出的起始位&#xff0c;n2 为欲取出的结束位。&#xff…

thinkphp6 JWT报错 ‘“kid“ empty, unable to lookup correct key‘解决办法

文章目录 JWT简介安装问题先前的代码解决办法修改后的完整代码 JWT简介 JWT全称为Json Web Token&#xff0c;是一种用于在网络应用之间传递信息的简洁、安全的方式。JWT标准定义了一种简洁的、自包含的方法用于通信双方之间以JSON对象的形式安全的传递信息。由于它的简洁性、可…

[论文笔记] In Search of an Understandable Consensus Algorithm (Extended Version)

In Search of an Understandable Consensus Algorithm (Extended Version) 寻找可理解的共识算法 (扩展版) [Extended Paper] [Original Paper] ATC’14 (Original) 摘要 Raft 是一个用于管理复制日志的共识算法. Raft 更易于理解, 且为构建实际的系统提供了更好的基础. Raf…

apache hive release notes

hive release notes位置 https://github.com/apache/hive/blob/master/RELEASE_NOTES.txt 如何查看不同版本的release note

计算机是如何工作的

一、冯诺依曼体系&#xff1a; CPU中央处理器&#xff08;运算器控制器&#xff09;&#xff1a;CPU是计算机最核心的部分&#xff0c;进行算数运算和逻辑判断。CPU最重要的指标是“主频”&#xff0c;如&#xff1a;2.5Ghz&#xff0c;描述了CPU的运算速度&#xff0c;可以近…

【React】redux和React-redux

&#x1f380;个人主页&#xff1a;努力学习前端知识的小羊 感谢你们的支持&#xff1a;收藏&#x1f384; 点赞&#x1f36c; 加关注&#x1fa90; Redux和React-redux reduxredux的使用Redux的工作流Redux APIstoreactionreducerstore.dispatch()redux的方法使用 React-Redux…

python人工智能【隔空手势控制鼠标】“解放双手“

大家好&#xff0c;我是csdn的博主&#xff1a;lqj_本人 这是我的个人博客主页&#xff1a; lqj_本人的博客_CSDN博客-微信小程序,前端,python领域博主lqj_本人擅长微信小程序,前端,python,等方面的知识https://blog.csdn.net/lbcyllqj?spm1011.2415.3001.5343哔哩哔哩欢迎关注…

【计算机图形学基础教程】MFC上机操作步骤

MFC上机操作步骤 步骤1 在Visual Studio界面&#xff0c;选择文件-新建-项目&#xff1a; 步骤2 在新建项目对话框&#xff0c;选择MFC-MFC应用程序&#xff1a; 步骤3 创建一个带有下列特征的新控制台工程框架&#xff0c;主要内容如下&#xff1a; 基于Win32的单文档…

PMP/高项 05-项目进度管理

项目进度管理 概念 项目进度管理&#xff08;Schedule Management) 项目进度管理又叫项目工期管理&#xff08;Duration Management)或项目的时间管理(Time Management) 是一种为管理项目按时完成项目所需的各个过程 进度管理过程 规划进度管理 定义活动 排列活动顺序 估算活…

前端web3入门脚本五:decode input data

一、前言 作为一个前端&#xff0c;在调用合约调试的时候&#xff0c;在区块浏览器里拿到一串 hex 格式的 input data&#xff0c;我们应该怎么decode呢&#xff1f; 二、举例 解码交易需要拥有 对应合约的 abi 以及 input data 下面举例介绍怎么获得这两个信息&#xff1a; 参…

二叉搜索树中的众数

1题目 给你一个含重复值的二叉搜索树&#xff08;BST&#xff09;的根节点 root &#xff0c;找出并返回 BST 中的所有 众数&#xff08;即&#xff0c;出现频率最高的元素&#xff09;。 如果树中有不止一个众数&#xff0c;可以按 任意顺序 返回。 假定 BST 满足如下定义&…