在垃圾回收机制里,引用计数实际上是个挺“偷懒”的记数员。它不关心事物长啥样,只记着哪位在手里拿着它。你把它当成一个自增计数器,对象创建时加一,销毁时减一。
只要计数器没变成负数,它就认定对象还活着;要是到了零要么负数,立马就该当垃圾回收走。
这种玩法在内存数据量不大的时候挺神,比如堆区那些刚建好的小对象,全靠这个数儿活着,不用其他复杂的逻辑去管它。 不过,这种记法有个致命的短板,就是它忒依赖对象本身的生命周期了。一旦对象坏死了,计数器直接变负,但这事儿形成的概率忒低,毕竟对象要是坏了,那得是卡住了,挺难说哪位先坏哪位的先坏。
这就造成了一个尴尬的局面:你明明对象已经死透了,计数器却还在吹牛说有活,这时候回收器还得费劲去瞎查,效率大打折扣。 更好的做法实际上更直观,直接把对象当成一个“资产包”。
这个资产包只记录了持有者是哪位,而不是它本身有多复杂。
要是持有者死了,资产包自然就没有了意义,务必回收。并且,要是持有者死了,资产包里的数据可能还在,这时候直接把持有者删掉,资产包里的数据就跟着一起没了,彻底清干净利落。 这种思路在 C++ 里就有了完美表达,就是虚函数指针。想象一下,一个被调用的函数,它自己不会调用别的函数,但能够被别的东西“借”来调用。
这个“借”的过程,本质上就是引用计数。编译器在编译时会生成一些虚函数表,每个表项对应一个对象。当你创建对象时,编译器会在虚表里把这个对象塞进去,这时候引用计数就 +1。当对象销毁时,编译器会在虚表里把它删掉,这时候计数就 -1。
只要计数为正,编译器就知道对象还在,别动它;要是计数归零了,编译器就变动它,对象也就彻底消亡了。 这种机制在 C++ 里特别管用,出于它不需求解释器去猜对象内部是不是确实坏了。解释器去猜那玩意儿能算出整数,编译器去改表结构,好办直接。并且,虚函数表里的计数是实时的,哪位调用哪位不用管,哪位删哪位哪位也不管,彻底跟哪位没关系。 这就引出了一个难题:要是对象本身死了,如何知道它的虚表里还能放多少数据?这里有个经典的例子。假设你有一个对象 A 和 B,A 里面包含一个对象 C。
要是你先删了 A,B 里的“哪位调用哪位”关系就断了,B 里的计数就不算 A 了。
这时候,要是 C 的对象本身都没了,B 里的计数又没自动减一,那 B 的计数里就剩下了 C 的数据。
这时候 B 看起来还活着,但实际数据已经空了。 这时候要是直接删 C,B 的计数就少了一。
要是直接删 B,C 就少了一。
只要一次删掉,B 和 C 都少了一,数据就没了。
这就是间接引用的妙用,把“哪位调用哪位”变成了“哪位持有哪位”,彻底解决了持有者消亡后数据残留的难题。 这种思路在 Java 里也有体现,就是垃圾回收器。它也是先存状态,再存东西。对象出生时,JVM 给对象打一个标记,告诉它“嘿,你活着”。
然后给对象存一份状态。
要是对象销毁了,JVM 就把它打掉。
要是引用还在,JVM 就拿着引用去找对象。对象里要是没存状态,它就没法被回收。 要是对象内部有状态,比如一个类里有个对象,那 JVM 就得存两份。一份存状态,一份存引用。引用存了多少,状态就对应多少。 这就回到了最初的难题,如何判断对象是不是确实死了。
实际上不用多难,根本得靠外部去判断。
要是外部知道对象已经死了,那它自己就自己销毁,垃圾回收器不用管它。
要是外部不知道,那它就得自己判断。 比如,你调用了一个函数,定义了参数。函数回一个值,你把这个值存个变量里。
这时候,这个变量里还活着,出于变量自己没死。
要是函数内部那个对象死了,但变量没死,那变量里的数据就保住了。
这时候要是你再调用那个函数,别看对象死了,但变量里的数据还在,函数还得接着用。 这时候,如何知道变量里的数据是不是确实空了?一般有几种办法。
第一种,检查变量本身。
要是变量跟对象区分开了,那对象死了,变量肯定还活着。
第二种,检查引用计数。
要是外部引用没了,要么外部知道对象死了,那变量可能就没法再用了。
第三种,直接靠外部逻辑。别管对象里没数据了,直接把这个变量删掉要么赋个空值。 还有一种思路是,对象创建时,先让它有个“状态”,状态为 0 要么空。
这时候对象是“无生命”的,是垃圾。对象活了,状态变成非空要么 1,这时候对象是有生命的,能被回收。 比如一个类,构造函数里有个成员变量。构造函数运行时,先把这个变量置 0。
这时候,对象是活的。对象内部它自己还能用。
要是对象被销毁了,成员变量就自动变成 0,对象就彻底没了。 还有一种更实用的做法,就是在对象创建的时候,给它挂个状态标签。
比如一个“活着”的标签。对象出生时,标签设为“活”。对象被销毁时,标签设为“死”。
要是标签是“活”,那这个对象就在,别动它。
要是标签是“死”,那这个对象就没了,所有持有者都得赶紧删掉相关的引用。 这种标签法在 Java 的 GC 里特别常见。对象引用计数的时候,就是把对象当成一个“资产”。资产本身有值,资产代表一个生命周期。 比如一个计数器,它是整数类型。对象创建时,计数器加一。对象销毁时,计数器减一。
只要计数器没归零,计数器就说对象还在。 还有一种办法是把对象拆成两局部。一局部是“身份”,一局部是“数据”。身份只记录哪位在持有它,数据就全放了。
要是持有者死了,数据自然就没了。 比如一个数组,它只记录哪位在持有数组。数组里的元素,哪位持有哪位负责清理。
要是数组本身没了,里面的数据就跟着没了。 这种思想在 C++ 里特别通透。引用计数是好办的数字加减,虚函数表是结构体的增减。两者都是“持有者”的概念。 实际上引用计数的核心,就是把“生命周期”和“数据所有权”分开了。数据所有权在持有者手里,生命周期在计数手里。
只要持有者没死,数据就保险;持有者死了,数据就跟着死了。 这种思路的益处是,数据不占用额外的内存。出于数据直接存有持有者内存里,不需求额外开一个“数据表”去存。持有者删了数据,垃圾回收器也不用管,直接删。 反过来想,要是数据不占用内存,那引用计数是如何算的?它只是记录“哪位在手里”,不需求计算数据本身有多大。 比如一个数组,它只记录哪位在持有。数组里有没有数据,跟数组本身没关系。数组本身死了,数据就没了。 还有一种更高级的做法,叫做“集合引用计数”。集合里存数据,数据里存引用。引用直接存集合里。
要是集合里没有数据,引用就是 0。 比如一个链表,它只存头指针。头指针存集合里。集合里要是没元素,头指针就是 0。
这时候,链表里的链表头就没了。 这种思路在 Java 里也有,就是“标记 - 清除”机制加上引用计数。 对象创建时,给对象打个标记,说“嘿,你活着”。对象销毁时,标记变 0,对象彻底消亡。 要是对象内部有数据,那数据也要跟着标记。 比如一个对象,它自己存个状态。状态是“活”。对象出生时,状态变“活”。对象被销毁时,状态变“死”。
要是状态是“活”,那这个对象就在。 还有一种思路,就是“外部引用计数”。外部引用没了,对象就没了。 比如调用了一个函数,定义了参数。函数回一个值,你把这个值存个变量里。
这时候,这个变量里还活着,出于变量自己没死。
要是函数内部那个对象死了,但变量没死,那变量里的数据就保住了。 这时候,如何知道变量里的数据是不是确实空了?一般有几种办法。
第一种,检查变量本身。
要是变量跟对象区分开了,那对象死了,变量肯定还活着。
第二种,检查引用计数。
要是外部引用没了,要么外部知道对象死了,那变量可能就没法再用了。
第三种,直接靠外部逻辑。别管对象里没数据了,直接把这个变量删掉要么赋个空值。 还有一种思路是,对象创建时,先让它有个“状态”,状态为 0 要么空。
这时候对象是“无生命”的,是垃圾。对象活了,状态变成非空要么 1,这时候对象是有生命的,能被回收。 比如一个类,构造函数里有个成员变量。构造函数运行时,先把这个变量置 0。
这时候,对象是活的。对象内部它自己还能用。
要是对象被销毁了,成员变量就自动变成 0,对象就彻底没了。 还有一种更实用的做法,就是在对象创建的时候,给它挂个状态标签。
比如一个“活着”的标签。对象出生时,标签设为“活”。对象被销毁时,标签设为“死”。
要是标签是“活”,那这个对象就在,别动它。
要是标签是“死”,那这个对象就没了,所有持有者都得赶紧删掉相关的引用。 这种标签法在 Java 的 GC 里特别常见。对象引用计数的时候,就是把对象当成一个“资产”。资产本身有值,资产代表一个生命周期。 比如一个计数器,它是整数类型。对象创建时,计数器加一。对象销毁时,计数器减一。
只要计数器没归零,计数器就说对象还在。 还有一种办法是把对象拆成两局部。一局部是“身份”,一局部是“数据”。身份只记录哪位在持有它,数据就全放了。
要是持有者死了,数据自然就没了。 比如一个数组,它只记录哪位在持有数组。数组里的元素,哪位持有哪位负责清理。
要是数组本身没了,里面的数据就跟着没了。 这种思想在 C++ 里特别通透。引用计数是好办的数字加减,虚函数表是结构体的增减。两者都是“持有者”的概念。 实际上引用计数的核心,就是把“生命周期”和“数据所有权”分开了。数据所有权在持有者手里,生命周期在计数手里。
只要持有者没死,数据就保险;持有者死了,数据就跟着死了。 这种思路的益处是,数据不占用额外的内存。出于数据直接存有持有者内存里,不需求额外开一个“数据表”去存。持有者删了数据,垃圾回收器也不用管,直接删。 反过来想,要是数据不占用内存,那引用计数是如何算的?它只是记录“哪位在手里”,不需求计算数据本身有多大。 比如一个数组,它只记录哪位在持有。数组里有没有数据,跟数组本身没关系。数组本身死了,数据就没了。 还有一种更高级的做法,叫做“集合引用计数”。集合里存数据,数据里存引用。引用直接存集合里。
要是集合里没有数据,引用就是 0。 比如一个链表,它只存头指针。头指针存集合里。集合里要是没元素,头指针就是 0。
这时候,链表里的链表头就没了。 这种思路在 Java 里也有,就是“标记 - 清除”机制加上引用计数。 对象创建时,给对象打个标记,说“嘿,你活着”。对象销毁时,标记变 0,对象彻底消亡。 要是对象内部有数据,那数据也要跟着标记。 比如一个对象,它自己存个状态。状态是“活”。对象出生时,状态变“活”。对象被销毁时,状态变“死”。
要是状态是“活”,那这个对象就在。 还有一种思路,就是“外部引用计数”。外部引用没了,对象就没了。 比如调用了一个函数,定义了参数。函数回一个值,你把这个值存个变量里。
这时候,这个变量里还活着,出于变量自己没死。
要是函数内部那个对象死了,但变量没死,那变量里的数据就保住了。 这时候,如何知道变量里的数据是不是确实空了?一般有几种办法。
第一种,检查变量本身。
要是变量跟对象区分开了,那对象死了,变量肯定还活着。
第二种,检查引用计数。
要是外部引用没了,要么外部知道对象死了,那变量可能就没法再用了。
第三种,直接靠外部逻辑。别管对象里没数据了,直接把这个变量删掉要么赋个空值。 还有一种思路是,对象创建时,先让它有个“状态”,状态为 0 要么空。
这时候对象是“无生命”的,是垃圾。对象活了,状态变成非空要么 1,这时候对象是有生命的,能被回收。 比如一个类,构造函数里有个成员变量。构造函数运行时,先把这个变量置 0。
这时候,对象是活的。对象内部它自己还能用。
要是对象被销毁了,成员变量就自动变成 0,对象就彻底没了。 还有一种更实用的做法,就是在对象创建的时候,给它挂个状态标签。
比如一个“活着”的标签。对象出生时,标签设为“活”。对象被销毁时,标签设为“死”。
要是标签是“活”,那这个对象就在,别动它。
要是标签是“死”,那这个对象就没了,所有持有者都得赶紧删掉相关的引用。 这种标签法在 Java 的 GC 里特别常见。对象引用计数的时候,就是把对象当成一个“资产”。资产本身有值,资产代表一个生命周期。 比如一个计数器,它是整数类型。对象创建时,计数器加一。对象销毁时,计数器减一。
只要计数器没归零,计数器就说对象还在。 还有一种办法是把对象拆成两局部。一局部是“身份”,一局部是“数据”。身份只记录哪位在持有它,数据就全放了。
要是持有者死了,数据自然就没了。 比如一个数组,它只记录哪位在持有数组。数组里的元素,哪位持有哪位负责清理。
要是数组本身没了,里面的数据就跟着没了。 这种思想在 C++ 里特别通透。引用计数是好办的数字加减,虚函数表是结构体的增减。两者都是“持有者”的概念。 实际上引用计数的核心,就是把“生命周期”和“数据所有权”分开了。数据所有权在持有者手里,生命周期在计数手里。
只要持有者没死,数据就保险;持有者死了,数据就跟着死了。 这种思路的益处是,数据不占用额外的内存。出于数据直接存有持有者内存里,不需求额外开一个“数据表”去存。持有者删了数据,垃圾回收器也不用管,直接删。 反过来想,要是数据不占用内存,那引用计数是如何算的?它只是记录“哪位在手里”,不需求计算数据本身有多大。 比如一个数组,它只记录哪位在持有。数组里有没有数据,跟数组本身没关系。数组本身死了,数据就没了。 还有一种更高级的做法,叫做“集合引用计数”。集合里存数据,数据里存引用。引用直接存集合里。
要是集合里没有数据,引用就是 0。 比如一个链表,它只存头指针。头指针存集合里。集合里要是没元素,头指针就是 0。
这时候,链表里的链表头就没了。 这种思路在 Java 里也有,就是“标记 - 清除”机制加上引用计数。 对象创建时,给对象打个标记,说“嘿,你活着”。对象销毁时,标记变 0,对象彻底消亡。 要是对象内部有数据,那数据也要跟着标记。 比如一个对象,它自己存个状态。状态是“活”。对象出生时,状态变“活”。对象被销毁时,状态变“死”。
要是状态是“活”,那这个对象就在。 还有一种思路,就是“外部引用计数”。外部引用没了,对象就没了。 比如调用了一个函数,定义了参数。函数回一个值,你把这个值存个变量里。
这时候,这个变量里还活着,出于变量自己没死。
要是函数内部那个对象死了,但变量没死,那变量里的数据就保住了。 这时候,如何知道变量里的数据是不是确实空了?一般有几种办法。
第一种,检查变量本身。
要是变量跟对象区分开了,那对象死了,变量肯定还活着。
第二种,检查引用计数。
要是外部引用没了,要么外部知道对象死了,那变量可能就没法再用了。
第三种,直接靠外部逻辑。别管对象里没数据了,直接把这个变量删掉要么赋个空值。 还有一种思路是,对象创建时,先让它有个“状态”,状态为 0 要么空。
这时候对象是“无生命”的,是垃圾。对象活了,状态变成非空要么 1,这时候对象是有生命的,能被回收。 比如一个类,构造函数里有个成员变量。构造函数运行时,先把这个变量置 0。
这时候,对象是活的。对象内部它自己还能用。
要是对象被销毁了,成员变量就自动变成 0,对象就彻底没了。 还有一种更实用的做法,就是在对象创建的时候,给它挂个状态标签。
比如一个“活着”的标签。对象出生时,标签设为“活”。对象被销毁时,标签设为“死”。
要是标签是“活”,那这个对象就在,别动它。
要是标签是“死”,那这个对象就没了,所有持有者都得赶紧删掉相关的引用。 这种标签法在 Java 的 GC 里特别常见。对象引用计数的时候,就是把对象当成一个“资产”。资产本身有值,资产代表一个生命周期。 比如一个计数器,它是整数类型。对象创建时,计数器加一。对象销毁时,计数器减一。
只要计数器没归零,计数器就说对象还在。 还有一种办法是把对象拆成两局部。一局部是“身份”,一局部是“数据”。身份只记录哪位在持有它,数据就全放了。
要是持有者死了,数据自然就没了。 比如一个数组,它只记录哪位在持有数组。数组里的元素,哪位持有哪位负责清理。
要是数组本身没了,里面的数据就跟着没了。 这种思想在 C++ 里特别通透。引用计数是好办的数字加减,虚函数表是结构体的增减。两者都是“持有者”的概念。 实际上引用计数的核心,就是把“生命周期”和“数据所有权”分开了。数据所有权在持有者手里,生命周期在计数手里。
只要持有者没死,数据就保险;持有者死了,数据就跟着死了。 这种思路的益处是,数据不占用额外的内存。出于数据直接存有持有者内存里,不需求额外开一个“数据表”去存。持有者删了数据,垃圾回收器也不用管,直接删。 反过来想,要是数据不占用内存,那引用计数是如何算的?它只是记录“哪位在手里”,不需求计算数据本身有多大。 比如一个数组,它只记录哪位在持有。数组里有没有数据,跟数组本身没关系。数组本身死了,数据就没了。 还有一种更高级的做法,叫做“集合引用计数”。集合里存数据,数据里存引用。引用直接存集合里。
要是集合里没有数据,引用就是 0。 比如一个链表,它只存头指针。头指针存集合里。集合里要是没元素,头指针就是 0。
这时候,链表里的链表头就没了。 这种思路在 Java 里也有,就是“标记 - 清除”机制加上引用计数。 对象创建时,给对象打个标记,说“嘿,你活着”。对象销毁时,标记变 0,对象彻底消亡。 要是对象内部有数据,那数据也要跟着标记。 比如一个对象,它自己存个状态。状态是“活”。对象出生时,状态变“活”。对象被销毁时,状态变“死”。
要是状态是“活”,那这个对象就在。 还有一种思路,就是“外部引用计数”。外部引用没了,对象就没了。 比如调用了一个函数,定义了参数。函数回一个值,你把这个值存个变量里。
这时候,这个变量里还活着,出于变量自己没死。
要是函数内部那个对象死了,但变量没死,那变量里的数据就保住了。 这时候,如何知道变量里的数据是不是确实空了?一般有几种办法。
第一种,检查变量本身。
要是变量跟对象区分开了,那对象死了,变量肯定还活着。
第二种,检查引用计数。
要是外部引用没了,要么外部知道对象死了,那变量可能就没法再用了。
第三种,直接靠外部逻辑。别管对象里没数据了,直接把这个变量删掉要么赋个空值。 还有一种思路是,对象创建时,先让它有个“状态”,状态为 0 要么空。
这时候对象是“无生命”的,是垃圾。对象活了,状态变成非空要么 1,这时候对象是有生命的,能被回收。 比如一个类,构造函数里有个成员变量。构造函数运行时,先把这个变量置 0。
这时候,对象是活的。对象内部它自己还能用。
要是对象被销毁了,成员变量就自动变成 0,对象就彻底没了。 还有一种更实用的做法,就是在对象创建的时候,给它挂个状态标签。
比如一个“活着”的标签。对象出生时,标签设为“活”。对象被销毁时,标签设为“死”。
要是标签是“活”,那这个对象就在,别动它。
要是标签是“死”,那这个对象就没了,所有持有者都得赶紧删掉相关的引用。 这种标签法在 Java 的 GC 里特别常见。对象引用计数的时候,就是把对象当成一个“资产”。资产本身有值,资产代表一个生命周期。 比如一个计数器,它是整数类型。对象创建时,计数器加一。对象销毁时,计数器减一。
只要计数器没归零,计数器就说对象还在。 还有一种办法是把对象拆成两局部。一局部是“身份”,一局部是“数据”。身份只记录哪位在持有它,数据就全放了。
要是持有者死了,数据自然就没了。 比如一个数组,它只记录哪位在持有数组。数组里的元素,哪位持有哪位负责清理。
要是数组本身没了,里面的数据就跟着没了。 这种思想在 C++ 里特别通透。引用计数是好办的数字加减,虚函数表是结构体的增减。两者都是“持有者”的概念。 实际上引用计数的核心,就是把“生命周期”和“数据所有权”分开了。数据所有权在持有者手里,生命周期在计数手里。
只要持有者没死,数据就保险;持有者死了,数据就跟着死了。