垃圾回收器之一——深入理解对象存活算法


用简单通俗易懂的话来记录自己对对象存活判断算法的理解

1.前言

1.垃圾回收是一个多数编程语言中都带有的内存管理机制。与非托管性语言相反:C, C++ 和 Objective C,用户需要手动收集内存,带有 GC 机制的语言:Java, javaScript 和 PHP 可以自动管理内存。
2.在Java中提起JVM,总是不能避免垃圾回收机制,而垃圾回收机制中最为重要的第一步就是判断对象是否存活,只有不存活的对象,才会使用对应的算法,进行内存的分配和回收(比如新生代使用复制算法,老年代使用标记-清理算法)。现有对对象是否存活的方法有两种:引用计数法和可达性分析。PHP中使用的就是引用计数法,Java中使用的是可达性分析

2.引用计数法

1.原理

首先我们知道一个对象除了包括对象头、实例数据和对齐填充,大家关于对象由疑问,可以看下
https://blog.csdn.net/FullStackDeveloper/article/details/116905098这篇博客关于对象的详解。引用计数法会在每个对象中添加一个计数字段,PHP中就是refcount这个字段,用于记录这个对象被引用的次数,对象被引用一次,该字段+1,取消对对象的引用,该字段-1.任何一个时刻,如果该字段为0时,该对象就会被回收。这种方式,毫不犹豫的来说,避免的咱们可达性分析中的Stop the word操作,这是它的优势。

class A{
}
public static void main(String[] args){
  	A a1 = new A(); //1
  	A a2  = a1; //2
  	a1 = null;//3
}

main函数
执行第1行代码 // 将对象A,复制给引用a1,引用+1
执行第2行代码 // 将对象A,复制给引用a1,引用+1
执行第3行代码 // 局部变量a1引用置为null,引用-1

2.优点

1.实现简单,垃圾对象便于辨识;
2.判定效率高,回收没有延迟性

3.缺点

该算法存在两个问题:
1.是循环引用时会导致内存泄漏:比如PHP5.3之前就对这个没有进行处理,因为Web使用PHP处理时间比较短,处理结束,就释放内存,但是,随着PHP的应用范围扩大,现在服务端跑脚本也是使用它。服务端玩命跑可不是一会,可能XX天。所以,PHP5.3以及PHP7都对其进行了优化,在下个章节中,咱们扯扯如何优化。
举个例子,我们来卢克卢克循环引用

example 1:
对象A引用对象B,对象B引用对象A
class A{
	`private B b = new B();
}

class B{
	`private A a = new A();
}

public void main(String[] args){
	A a = new A();
	B b = new B();
}

内存图
在这里插入图片描述

example 1:
对象A的成员变量引用A
class A{
	`private A a= new A();
}

public void main(String[] args){
	A a = new A();
}

这种应用场景我还没见过,大家如果有实际应用,可以告诉我。

2.堆对象的每次引用赋值和引用置空,都会伴随字段加减操作,会带来一定的性能开销。额,这种说法怎么说呢?如果说它损耗了性能,不也就是把集中损耗的性能分布在每个对象上么?好吧,算是一个缺点。

4.优化

1.同步周期回收

这种方法是PHP5.3采用的回收方式,目的就是解决循环引用问题。它的原理是:
(1)如果一个对象的refcount增加,那么表明该变量引用的对象还在使用,不属于垃圾
(2)如果一个对象的refcount减少到0,那么对象可以被释放掉,可以清除,不属于垃圾
(3)在经过模拟删除后一个对象的refcount减1,如果该对象的引用次数为是大于0,那么此对象不能被释放,可能是一个垃圾
它的实现步骤:
在这里插入图片描述
上面是个经典的步骤图,不过,对于非PHP同学估计还是看不懂。那就用最简单的话来描述这个过程。
本质原理就是当一个对象的refCount减1并且不为0时,添加到根缓冲区(根缓冲区默认大小为1000),当达到默认大小时,进行所有的对象遍历,对对象的成员引用进行减1,减1后,如果对象的refCount为0,则认为垃圾,进行回收。

2.智能指针

让我们来think一下,如果Java使用引用计数法,那么,是否有方案可以优化循环引用问题呢?
答案是有的,就是可以通过强、弱引用计数结合方式解决引用计数的循环引用问题。android中的智能指针就是这样实现的。
1.智能指针
智能指针是Android的Native层的小型GC实现。基于sp和wp。

 sp<IBinder> result = new BpBinder(handle);

 wp<IBinder> result = new BpBinder(handle);

强指针 sp 重载了 “=” 运算符,在引用其他对象时将强引用计数 +1,在 sp 析构函数中将强引用计数 -1,当强引用计数减至 0 时销毁引用的对象,这样就实现了对象的自动释放。

弱指针引用其他对象时将弱引用计数 +1,在 wp 析构函数(对应于构造函数,当对象销毁时,会调用)中将弱引用计数 -1,当强引用计数为 0 时,不论弱引用计数是否为 0 都销毁引用的对象

2.如何解决循环引用问题?
A调用B时,使用sp,对于B调用A时,使用wp,当对象sp的为0时,进行回收。
此时,会不会引发其它问题?
是的,会引发野指针问题。B调用A时,A已经被销毁,那么,怎么办?这就涉及策略,可以考虑重新创建A等方式。
3.是不是只有强指针为0时,才进行销毁?
其实,这只是一个策略,和实际情况还是有出入的。
智能指针并不是固定的 “当强引用计数为 0 时,不论弱引用计数是否为 0 都销毁引用的对象” ,而是可以自定义规则。RefBase 提供了 extendObjectLifetime() 方法,可以用来设置引用计数器的规则,不同规则对删除目标对象的时机判断也是不一样的,包括以下三种规则:

OBJECT_LIFETIME_STRONG:只有在这个对象内存空间中的强计数器值为 0 的时候才会销毁对象

OBJECT_LIFETIME_WEAK:只有在这个对象内存空间中的强计数器和弱计数器的值都为 0 的时候才会销毁对象

OBJECT_LIFETIME_MASK:不管这两个计数器是不是都为 0,都不销毁对象,即与一般指针无异,还是要自己手动去释放对象

4.提供强弱引用就一定能解决内存泄漏吗?
其实,这不一定,你推出了强弱引用机制,但是, 我不这么用也是不行的,有个教育开发者的过程

3.可达性分析

这是Java说使用的判断对象是否存活的方式。

1.算法思路

通过一系列的称为"GC Roots“的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Referece Chain),当一个对象到GC Roots没有任何引用链时相连时,说明该对象无使用,可以进行GC回收。

2.GC Roots对象

1.虚拟机栈(栈帧中的本地变量表)中引用的对象。
2.方法区中类静态属性引用的对象。
3.方法区中常量引用的对象。
4.本地方法栈中JNI(即一般说的Native方法)引用的对象。
在这里插入图片描述
上面的图纠正一下,这里偷了懒,直接从网络上down下来图,这个不是模型图,只是内存区域分布图。内存模型图是多线程的内存图。

有没有同学有困惑,为什么对象的成员变量不能作为GCRoots对象。根本原因其实就是类的加载流程。大家都知道ART虚拟机是基于栈帧的,也就是说执行是从把类的常量和静态变量加载,然后是栈帧执行,所以,他们说也引用的对象是GCRoots,而对于成员变量,则只能作为引用链。在下面也会进行成员变量的验证,当然,它只是验证结果了。

3.GC Roots对象验证

1.前提

要清晰验证GC Root对象,要进行GC日志的打印,这样才可以清晰看处理.
需要把下面的配置在AS中进行配置

-XX:+PrintGC 输出简要GC日志 

2.原理

实际上就是通过GC发生后的内存变化,从而验证刚刚的静态变量、常量、栈帧的局部变量等是GCRoots

3.GC日志解读

4.实操

1.验证虚拟机栈(栈帧中的局部变量)中引用的对象 作为GC Roots

/**
 * GCRoots 测试:虚拟机栈(栈帧中的局部变量)中引用的对象作为GCRoots 
 * -Xms1024m -Xmx1024m -Xmn512m -XX:+PrintGCDetails
 * 
 * 扩展:虚拟机栈中存放了编译器可知的八种基本数据类型,对象引用,returnAddress类型(指向了一条字节码指令的地址)
 * @author ljl
 */
public class TestGCRoots01 {
	private int _10MB = 10 * 1024 * 1024;
	private byte[] memory = new byte[8 * _10MB];
 
	public static void main(String[] args) {
		method01();
		System.out.println("返回main方法");
		System.gc();
		System.out.println("第二次GC完成");
	}
 
	public static void method01() {
		TestGCRoots01 t = new TestGCRoots01();
		System.gc();
		System.out.println("第一次GC完成");
	}
}

分析:
2.验证方法区中的静态变量引用的对象作为GC Roots

/**
 * 测试方法区中的静态变量引用的对象作为GCRoots
 * -Xms1024m -Xmx1024m -Xmn512m -XX:+PrintGCDetails
 * 
 * 扩展:方法区存与堆一样,是各个线程共享的内存区域,用于存放已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。
 * @author ljl
 * */
public class TestGCRoots02 {
	private static int _10MB = 10 * 1024 * 1024;
	private byte[] memory;
 
	private static TestGCRoots02 t;
 
	public TestGCRoots02(int size) {
		memory = new byte[size];
	}
 
	public static void main(String[] args) {
		TestGCRoots02 t2 = new TestGCRoots02(4 * _10MB);
		t2.t = new TestGCRoots02(8 * _10MB);
		t2 = null;
		System.gc();
	}
}```

3.验证方法区中常量引用对象作为GC Roots

```bash
/**
 * 测试常量引用对象作为GCRoots 
 * 注意:t修饰符如果只是final会被回收,static final不会被回收,所以static final 才是常量的正确写法
 * -Xms1024m -Xmx1024m -Xmn512m -XX:+PrintGCDetails
 * @author ljl
 */
public class TestGCRoots03 {
	private static int _10MB = 10 * 1024 * 1024;
	private static final TestGCRoots03 t = new TestGCRoots03(8 * _10MB);
	private byte[] memory;
 
	public TestGCRoots03(int size) {
		memory = new byte[size];
	}
 
	public static void main(String[] args) {
		TestGCRoots03 t3 = new TestGCRoots03(4 * _10MB);
		t3 = null;
		System.gc();
	}
}

4.测试成员变量是否可作为GC Roots

/**
 * 测试成员变量引用对象是否可作为GCRoots
 * -Xms1024m -Xmx1024m -Xmn512m -XX:+PrintGCDetails
 *
 * @author ljl
 */
public class TestGCRoots04 {
	private static int _10MB = 10 * 1024 * 1024;
	private TestGCRoots04 t;
	private byte[] memory;
 
	public TestGCRoots04(int size) {
		memory = new byte[size];
	}
 
	public static void main(String[] args) {
		TestGCRoots04 t4 = new TestGCRoots04(4 * _10MB);
		t4.t = new TestGCRoots04(8 * _10MB);
		t4 = null;
		System.gc();
	}
}

4.站在巨人的肩膀

1.引用计算法详解
https://zhuanlan.zhihu.com/p/81673709
2.智能指针优化
https://zhuanlan.zhihu.com/p/347539705
3.PHP的回收机制
https://www.cnblogs.com/hld123/p/13385443.html
https://blog.csdn.net/weixin_33695082/article/details/88626117
4.GCRoot验证
https://www.cnblogs.com/szw906689771/p/14719195.html
5.gc日志解读
https://blog.csdn.net/yinni11/article/details/102591431
https://www.cnblogs.com/lzmrex/articles/9507568.html

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值