分享

JVM真香系列:如何判断对象是否可被回收?

 田维常 2020-11-13

回复“000”获取大量电子书

JVM中程序寄存器、Java虚拟机栈、本地方法栈,这三个区是随着线程的创建而创建,随着线程结束而销毁。

其实就是这三个的生命周期和线程的生命周期一样。都是每个线程私有的。

每次方法的调用就会向栈里入栈一个栈帧,方法调用结束,跟着就出栈。

对象也是有生命周期的,所以对于不需要的对象要进行必要的清楚,否则久而久之,我们的内存就被一点一点的消耗完。

今天来学习,如何判断对象是否已经可以被回收?以及回收有哪些算法?

如何判断对象已死?

引用计数法

给对象添加一个引用计数器,每当一个地方引用它object时技术加1,引用失去以后就减1,计数为0说明不再引用。

  • 优点:实现简单,判定效率高;

  • 缺点:无法解决对象相互循环引用的问题,对象A中引用了对象B,对象B中引用对象A。

public class A {
 public B b; 
}
public class B {
 public C c; 
}
public class C {
 public A a; 
}
public class Test{

 private void test(){
  A a = new A();
  B b = new B();
  C c = new C();

  a.b=b;
  b.c=c;
  c.a=a;
 }
}


可达性分析算法

当一个对象到GC Roots没有引用链相连,即就是GC Roots到这个对象不可达时,证明对象不可用。

GC Roots种类:

Java 线程中,当前所有正在被调用的方法的引用类型参数、局部变量、临时值等。也就是与我们栈帧相关的各种引用。
所有当前被加载的 Java 类。
Java 类的引用类型静态变量。
运行时常量池里的引用类型常量(String 或 Class 类型)。
JVM 内部数据结构的一些引用,比如 sun.jvm.hotspot.memory.Universe 类。
用于同步的监控对象,比如调用了对象的 wait() 方法。

public class Test{
 private void test(C c){
  A a = new A();
  B b = new B();
  a.b=b;
  //这里的a/b/c都是GC Root;
 }
}



对象的引用类型

强引用:

User user=new User();

我们开发中使用最多的对象引用方式。

特点:我们平常典型编码Object obj = new Object()中的obj就是强引用。

通过关键字new创建的对象所关联的引用就是强引用。

当JVM内存空间不足,JVM宁愿抛出OutOfMemoryError运行时错误(OOM),使程序异常终止,也不会靠随意回收具有强引用的“存活”对象来解决内存不足的问题。

对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,就是可以被垃圾收集的了,具体回收时机还是要看垃圾收集策略。

软引用:

SoftReference object=new SoftReference(new Object());

特点:软引用通过SoftReference类实现。软引用的生命周期比强引用短一些。

只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象:即JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

后续,我们可以调用ReferenceQueue的poll()方法来检查是否有它所关心的对象被回收。如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。

应用场景:软引用通常用来实现内存敏感的缓存。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

弱引用:

WeakReference object=new WeakReference (new Object();

ThreadLocal中有使用。

弱引用通过WeakReference类实现。弱引用的生命周期比软引用短。

在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。由于垃圾回收器是一个优先级很低的线程,因此不一定会很快回收弱引用的对象。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。应用场景:弱应用同样可用于内存敏感的缓存。

虚引用:

几乎没见过使用, ReferenceQueue 、PhantomReference。

finalize方法

这个方法就有点类似于“某个人被判了死刑,但是不一定会死”的情景。

即使在可达性分析算法中不可达的对象,也并非一定是“非死不可”的,这时候他们暂时处于“缓刑”阶段,真正宣告一个对象死亡至少要经历两个阶段:

1、如果对象在可达性分析算法中不可达,那么它会被第一次标记并进行一次刷选,刷选的条件是是否需要执行finalize()方法(当对象没有覆盖finalize()或者finalize()方法已经执行过了(对象的此方法只会执行一次)),虚拟机将这两种情况都会视为没有必要执行)。

2、如果这个对象有必要执行finalize()方法会将其放入F-Queue队列中,稍后GC将对F-Queue队列进行第二次标记,如果在重写finalize()方法中将对象自己赋值给某个类变量或者对象的成员变量,那么第二次标记时候就会将它移出“即将回收”的集合。

方法区的回收

在堆中,尤其是在新生代中,一次垃圾回收一般可以回收 70% ~ 95% 的空间,而方法区的垃圾收集效率远低于此。

方法区垃圾回收主要两部分内容:废弃的常量和无用的类。

垃圾回收算法

标记-清除

第一步:就是找出活跃的对象。我们反复强调 GC 过程是逆向的, 根据 GC Roots 遍历所有的可达对象,这个过程,就叫作标记。

第二步:除了上面标记出来的对象以外,其余的都清除掉。

  • 缺点:标记和清除效率不高,标记和清除之后会产生大量不连续的内存碎片

复制

新生代使用,新生代分中Eden:S0:S1= 8:1:1,其中后面的1:1就是用来复制的。

当其中一块内存使用完了,就将还存活的对象复制到另外一块上面,然后把已经使用过的内存空间一次清除掉。

一般对象分配都是进入新生代的eden区,如果Minor GC还存活则进入S0区,S0S1不断对象进行复制。对象存活年龄最大默认是15,大对象进来可能因为新生代不存在连续空间,所以会直接接入老年代。任何使用都有新生代的10%是空着的。

  • 缺点:对象存活率高时,复制效率会较低,浪费内存。

标记整理

它的主要思路,就是移动所有存活的对象,且按照内存地址顺序依次排列,然后将末端内存地址以后的内存全部回收。 但是需要注意,这只是一个理想状态。对象的引用关系一般都是非常复杂的,我们这里不对具体的算法进行描述。我们只需要了解,从效率上来说,一般整理算法是要低于复制算法的。这个算法是规避了内存碎片和内存浪费。

让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

从上面的三个算法来看,其实没有绝对最好的回收算法,只有最适合的算法。

STW

STW=Stop The world,字面翻译过来就是整个世界都停止了。

在JVM中也有这么个说法,就是STW,是指JVM垃圾收集器在收集垃圾对象的时候,其他所有线程都被挂起(除了垃圾收集器之外),JVM中一种全局暂停现象。

----全局停顿,想想就很可怕,所有的Java代码停止执行,native代码可以执行,但是不能与JVM进行交互,这些基本上都是由于GC引起的。

但是也还有另外的几种场景也可以导致STW:

1.Garbage collection pauses
2.Code deoptimization
3.Flushing code cacheClass redefinition (e.ghot swap or instrumentation)
4.Biased lock revocation
5.Various debug operation (e.gdeadlock check or stacktrace dump)

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多