Java内存回收机制(状态、引用、GC)

  • 2
  • 2021年5月24日20:45:18

在Java中,它的内存管理包括两方面:内存分配(创建Java对象的时候)和内存回收,这两方面工作都是由JVM自动完成的。这篇文章,我将分为以下内容说明:Java对象在内存中的状态、Java程序对对象的四种引用、垃圾回收机制。

Java对象在内存中的状态

通过代码来看:
Person.java:

package test;

import java.io.Serializable;

public class Person implements Serializable {

    static final long serialVersionUID = 1L;

    String name; // 姓名

    Person friend;    //朋友

    public Person() {}

    public Person(String name) {
        super();
        this.name = name;
    }
}

Test.java:

package test;


public class Test{

    public static void main(String[] args) {
        Person p1 = new Person("Kevin");
        Person p2 = new Person("Rain");
        Person p3 = new Person("Sunny");

        p1.friend = p2;
        p3 = p2;
        p2 = null;
    }
}

Test.java中的对象的引用关系:

对象在内存中的三种状态:

1)可达状态:在一个对象创建后,有一个以上的引用变量引用它。在有向图中可以从起始顶点导航到该对象,那它就处于可达状态。

2)可恢复状态:如果程序中某个对象不再有任何的引用变量引用它,它将先进入可恢复状态,此时从有向图的起始顶点不能再导航到该对象。在这个状态下,系统的垃圾回收机制准备回收该对象的所占用的内存,在回收之前,系统会调用finalize()方法进行资源清理,如果资源整理后重新让一个以上引用变量引用该对象,则这个对象会再次变为可达状态;否则就会进入不可达状态。

3)不可达状态:当对象的所有关联都被切断,且系统调用finalize()方法进行资源清理后依旧没有使该对象变为可达状态,则这个对象将永久性失去引用并且变成不可达状态,系统才会真正的去回收该对象所占用的资源。

对象的引用

1)强引用 :创建一个对象并把这个对象直接赋给一个变量,eg :
Person person = new Person("sunny");
不管系统资源有么的紧张,强引用的对象都绝对不会被回收,即使他以后不会再用到

2)软引用 :通过SoftReference类实现,eg :
SoftReference<Person> p = new SoftReference<Person>(new Person("Rain"));,
内存非常紧张的时候会被回收,其他时候不会被回收,所以在使用之前要判断是否为null从而判断他是否已经被回收了。

3)弱引用:通过WeakReference类实现,eg :
WeakReference<Person> p = new WeakReference<Person>(new Person("Rain"));
不管内存是否足够,系统垃圾回收时必定会回收。

4)虚引用 :不能单独使用,主要是用于追踪对象被垃圾回收的状态。通过PhantomReference类和引用队列ReferenceQueue类联合使用实现,eg :

package test;

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;


public class Test{

    public static void main(String[] args) {
        //创建一个对象
        Person person = new Person("Sunny");
        //创建一个引用队列
        ReferenceQueue rq = new ReferenceQueue();
        //创建一个虚引用,让此虚引用引用到person对象
        PhantomReference pr = new PhantomReference(person, rq);
        //切断person引用变量和对象的引用
        person = null;
        //试图取出虚引用所引用的对象
        //发现程序并不能通过虚引用访问被引用对象,所以此处输出为null
        System.out.println(pr.get());
        //强制垃圾回收
        System.gc();
        System.runFinalization();
        //因为一旦虚引用中的对象被回收后,该虚引用就会进入引用队列中
        //所以用队列中最先进入队列中引用与pr进行比较,输出true
        System.out.println(rq.poll() == pr);
    }
}

结果:

垃圾回收

如何确定某个对象是“垃圾”?

在java中是通过引用来和对象进行关联的,也就是说如果要操作对象,必须通过引用来进行。那么很显然一个简单的办法就是通过引用计数来判断一个对象是否可以被回收。不失一般性,如果一个对象没有任何引用与之关联,则说明该对象基本不太可能在其他地方被使用到,那么这个对象就成为可被回收的对象了。这种方式成为引用计数法。

这种方式的特点是实现简单,而且效率较高,但是它存在循环引用的问题,因此在Java中并没有采用这种方式(Python采用的是引用计数法)。

引用计数法的循环依赖是可以解决的,参考:https://blog.csdn.net/yrwan95/article/details/82829186

为了避免这个问题,在Java中采取了可达性分析法。该方法的基本思想是通过一系列的“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。

典型的垃圾收集算法

关于JVM的内容我已经发过一篇文章了:https://blog.ysboke.cn/archives/242.html

1、标记清除

把要回收的对象标记,然后清除。优点是比较简单明了,但是会产生内存碎片(碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作)

2、标记整理

同样先将可回收的对象标记,然后将存活对象移动到内存一端,接着清理掉端边界外的内存。好处是减少了内存碎片,缺点是移动对象会消耗性能。

3、复制

将空间分为两个大小相同的 From 和 To 两个半区,同一时间只会使用其中一个,每次进行回收时将一个半区的存活对象通过复制的方式转移到另一个半区,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题,缺点也很明显,可用的内存直接少了一半。

如果存活对象很多的话效率会很低。

4、分代算法

堆区划分为新生代(Young Generation)、老年代(Tenured Generation)。老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

一般新生代使用复制算法(因为会有大量的对象被回收)、老年代使用标记整理算法。

此处的复制算法不是按照一比一来分,一般是将新生代分为一个大的Eden和两个较小的Survivor区,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。


关于JVM的内容,你可看这篇文章:https://blog.ysboke.cn/archives/242.html

也可以看这篇关于《深入理解Java虚拟机》的总结:https://blog.csdn.net/qq_41701956/article/details/81664921

本文来自凡蜕博客(https://blog.ysboke.cn), 转载请带上地址.。
匿名

发表评论

匿名网友