注册

如何判断一个对象是否可以被回收

在c++中,当我们使用完某个对象的时候,需要显示的将对象回收,如果忘记回收,则会导致无用对象一直在内存里,导致内存泄露。在java中,jvm会帮助我们进行垃圾回收,无需程序员自己写代码进行回收。


首先jvm需要解决的问题是:如何判断一个对象是否是垃圾,是否可以被回收呢?一般都是通过引用计数法,可达性算法。


引用计数法


对每个对象的引用进行计数,每当有一个地方引用它时计数器+1、引用失效(改为引用其他对象,赋值为null,或者生命周期结束)则-1,引用的计数放到对象头中,大于0的对象被认为是存活对象,一旦某个对象的引用计数器为 0,则说明该对象已经死亡,便可以被回收了。


public void f(){
Object a = new Object(); // 对象a引用计数为1
g(a);
// 退出g(a),对象b的生命周期结束,对象a引用计数为1
}// 退出f(), 对象a的生命周期结束,引用计数为0

public void g(Object a){
Object b = a; // 对象a引用计数为2
Object c = a; // 对象a引用计数为3
Object d = a; // 对象a引用计数为4
d = new Object(); // 对象a引用计数为3
c = null; // 对象a引用计数为2
}

引用计数法实现起来比较容易,但是存在一个严重的问题,那就是无法检测循环依赖。如下所示:


public class A{
public B b;
public A(){

}
}

public class A{
public A a;
public B(){

}
}

A a = new A(); // a的计数为1
B b = new B(); // b的计数为1
a.b = b; // b的计数为2
b.a = a; // a的计数为2
a = null; // a的计数为1
b = null; // b的计数为1

最终a,b的计数都为1,无法被识别为垃圾,所以无法被回收。


Python使用的就是引用计数算法,Python的垃圾回收机制,很大一部分是为了处理可能产生的循环引用,是对引用计数的补充。


虽然循环引用的问题可通过Recycler算法解决,但是在多线程环境下,引用计数变更也要进行昂贵的同步操作,性能较低,早期的编程语言会采用此算法。


可达性算法


介绍


Java最终并没有采用引用计数算法,JVM的主流垃圾回收器采取的是可达性分析算法。


我们把对象之间的引用关系用数据结构中的有向图来表示。图中的顶点表示对象。如果对象A中的变量引用了对象B,那么,我们便在对象A对应的顶点和对象B对应的顶点之间画一条有向边。


在有向图中,有一组特殊的顶点,叫做GC Roots。哪些对象可以作为GC Roots呢?



  1. 系统加载的类:rt.jar。
  2. JNI handles。
  3. 线程运行栈上所有引用,包括方法参数,创建的局部变量等。
  4. 已启动未停止的java线程。
  5. 已加载类的静态变量。
  6. 用于同步的监控,调用了对象的wait()/notify()/notifyAll()。

JVM以GC Roots为起点,遍历(深度优先遍历或广度优先遍历)整个图,可以遍历到的对象为可达对象,也叫做存活对象,遍历不到的对象为不可达对象,也叫做死亡对象。死亡对象会被虚拟机当做垃圾回收。


JVM实际上采用的是三色算法来遍历整个图的,遍历走过的路径被称为reference chain。



  • Black: 对象可达,且对象的所有引用都已经扫描了(“扫描”在可以理解成遍历过了或加入了待遍历的队列)
  • Gray: 对象可达,但对象的引用还没有扫描过(因此 Gray 对象可理解成在搜索队列里的元素)
  • White: 不可达对象或还没有扫描过的对象


引用级别


遍历到的对象一定会存活吗?事实上,JVM会根据对象A对对象B的引用强不强烈作出相应的回收措施。


基于此JVM根据引用关系的强烈,将引用关系分为四个等级:强引用,软引用,弱引用,虚幻引用。


强引用


类似Object obj = new Object() 这类的引用都属于强引用,只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象,只有在和GC Roots断绝关系时,才会被回收。


如果要对强引用进行垃圾回收,需要设置强引用对象为 null,或者让其超出对象的生命周期范围,则认为改对象不存在引用。类似obj = null;


参考代码:


public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;

size = 0;
}

软引用


用于描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。可以使用SoftReference 类来实现软引用。


Object obj = new Object();
SoftReference<Object> softRef = new SoftReference(obj);

弱引用


也是用于描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。可以使用WeakReference 类来实现弱引用。


Object obj = new Object();
WeakReference<Object> weakReference = new WeakReference<>(obj);
obj = null;
System.gc();
TimeUnit.SECONDS.sleep(200);
System.out.println(weakReference.get());
System.out.println(weakReference.isEnqueued());

虚引用


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


Object obj = new Object();
ReferenceQueue<Object> refQueue = new ReferenceQueue<>();
PhantomReference<Object> phantomReference = new PhantomReference<>(obj, refQueue);
System.out.println(phantomReference.get());
System.out.println(phantomReference.isEnqueued());

基于虚引用,有一个更加优雅的实现方式,那就是Java 9以后新加入的Cleaner,用来替代Object类的finalizer方法。


STW


虽然可达性分析的算法本身很简明,但是在实践中还是有不少其他问题需要解决的。我们把运行应用程序的线程叫做用户线程,把执行垃圾回收的线程叫做垃圾回收线程,如果在执行垃圾回收线程的同时还在执行用户线程,那么对象的引用关系可能会在垃圾回收途中被用户线程修改,从而造成误报(将引用设置为 null)或者漏报(将引用设置为未被访问过的对象)


误报并没有什么伤害,Java 虚拟机至多损失了部分垃圾回收的机会。漏报则比较麻烦,因为垃圾回收器可能回收事实上仍被引用的对象内存,导致程序出错。


为了解决漏报的问题,保证垃圾回收线程不会被用户线程打扰,最简单粗暴的方式就是在垃圾回收的过程中,暂停用户线程,直到垃圾回收结束,再恢复用户线程,这就是STW(STOP THE WORLD)。


但是如果STW的时间过程,就会严重影响程序的性能,因此优化垃圾回收过程,尽量减少STW的时间,是垃圾回收器努力优化的方向,


安全点


上述除了STW的响应时间的问题,还有另外一个问题,就是如何从一个正确的状态停止,再从这个状态正确恢复。Java虚拟机中的STW是通过安全点(safepoint)机制来实现的。当Java虚拟机收到STW请求,它便会等待所有的线程都到达安全点,才允许请求Stop-the-world的线程进行独占的工作。


当然,安全点的初始目的并不是让用户线程立刻停下,而是找到一个稳定的执行状态。在这个执行状态下,JVM的堆栈不会发生变化。这么一来,垃圾回收器便能够“安全”地执行可达性分析,才能找到完整GC Roots。


是不是所有的用户线程在垃圾回收的时候都要停止呢?实际上,JVM也做了优化,如果某个线程处于安全区(不会改变对象引用关系的一段连续的代码区间),那么这个线程不需要停止,可以和垃圾回收线程并行执行。一旦离开安全区,JVM会检查是否处于STW阶段,如果是,则需要阻塞该线程,等垃圾回收完再恢复。


作者:Shawn_Shawn
来源:juejin.cn/post/7304181581303447589

0 个评论

要回复文章请先登录注册