JVM性能优化------可达性分析算法与四种引用

2021/4/15 1:25:16

本文主要是介绍JVM性能优化------可达性分析算法与四种引用,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

项目地址
jvm_04

在理解回收机制算法之前,我们需要了解。如何判断一个对象是否该被回收。

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

在了解java的回收机制之前,我们可以了解一下,初期python的垃圾回收机制。顺带一提,比如python/java等语言都是基于c/c++来写的,但是C、C++却不存在垃圾回收机制。
Java中垃圾回收是自动化的,但其可控性差,内存容易溢出。内存溢出是因为JVM内存分配的对象过多,这些对象所需内存超出了JVM内存大小。虽然Java中是自动的。但是程序员仍可调用System.gc( )来进行手动回收,调用此方法会尝试释放被丢弃的对象占用的内存,但结果无法保证,因此附带一个免责声明。下面我们将从如何确定需要被回收的对象、什么时候回收、怎样进行回收这三个方面进行分析。

怎么确定对象需要回收?

1.回收对象方法

  1. 引用计数法
    系统会为对象添加一个计数器,当有新的引用时加1,引用失效时减1。这个方法特别方便,也特别容易理解。但是此方法无法解决两个对象循环引用的问题。
  2. 可达性分析法
    通过对象的引用链来判断该对象是否需要被回收,通过一系列的GC Roots的对象作为起始点,从这些根节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,就需要回收此对象。

2.回收时间
CPU空闲时进行回收、堆内存满了后进行回收、程序运行中内存不够用时、调用System.gc()回收。

3.回收方法

  1. 标记-清除算法
    首先记录需要回收的对象,再来进行回收。
    缺点:回收速度慢,回收之后产后生大量不连续的内存碎片,后期运行过程中需要分配较大对象时无法找到足够的连续内存而造成内存空间浪费。
  2. 复制算法
    将内存空间等分为两份,每次只使用其中一份,当满了之后将还有效的对象复制到另一份内存中,然后把原来的空间进行清除,不会产生内存碎片。
    缺点:可用内存空间减半。
  3. 标记-整理算法
    不仅对需要回收的对象进行整理,还对有效对象进行整理,不会产生内存碎片。
    缺点:耗时
  4. 分代收集算法
    JVM使用最多的一种算法,其实不是一个新的算法,而是在具体的场景自动选择以上三种算法进行垃圾对象回收。
    新生代:目的是回收那些生命周期短的对象,主要存放新产生的对象。新生代按照8:1:1分为eden区、survivor0、survivor1,大部分对象在eden区中生成,当eden满时,将存活的对象复制到survivor0,然后清空eden,当eden、survivor0都满了时,将这两个区中存活的对象复制到survivor1,然后清空eden、survivor0,当着三个区都满了时则把存货对象复制到老年代,如果老年代也满了则触发FullGC。新生代的全回收叫MinorGC,MinorGC发生频率比较高,不一定等到新生代满了时才进行。
    老年代:存放对象生命周期较长,且内存大概是新生代的两倍,老年代存活对象生命周期长,因此MajorGC发生频率较低。
    永久代:主要存放静态文件,如Java类,方法等。永久带对垃圾回收基本没有影响,当应用动态生成或者调用一些类的时候,例如反射、动态代理CGLib等bytecode框架时需要永久带来保存新生成的类。后期在jdk1.8变成了元空间。

现在了解基本的概念。那么我们来看看可达性分析算法。至于引用计数法中的循环引用类似于循环依赖,这里就不再提及了。

怎么去了解这个可达性分析算法呢?
核心思想:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到 GC Roots 没有任何的引用链相连时(从 GC Roots 到这个对象不可达)时,证明此对象不可用。那我们怎么去查看这个引用链?

可以使用工具MemoryAnalyzer。
代码:

import java.io.IOException;

/**
 * @author 龙小虬
 * @date 2021/4/14 22:30
 */
public class MemoryAnalyzerTest {
    public static void main(String[] args) throws IOException {
        Entity user = new Entity("lxq");
        System.out.println("user拥有引用地址");
        System.in.read();
        user = null;
        System.out.println("user被清空了引用地址");
        System.in.read();
        System.in.read();
        System.out.println("end");
    }
}

运行代码。
项目目录下运行命令:jps获取线程的pid
在这里插入图片描述
并且运行命令:jmap -dump:format=b,live,file=a.bin 50416
获取到a.bin文件。打开MemoryAnalyzer,并且添加a.bin文件。配置:
在这里插入图片描述
之后就可以看到解析出了一部分数据。
在这里插入图片描述
在这里插入图片描述
系统class是永远不会释放的,因为他是属于main()方法定义的,只有在main()方法结束才会释放。
在这里插入图片描述
这里还有我们熟悉的锁,这个锁也是不会释放空间的,因为我们使用的synchronized就是这里面的。
好了,现在我们就直接找到当前的线程。
在这里插入图片描述
可以发现存在一个Entity对象被分配。
那我们返回到程序,随意输入一个数据,让程序向下运行。
在这里插入图片描述
使用命令jmap -dump:format=b,live,file=b.bin 50416
获取到b.bin文件,操作同上a.bin,最后可以找到当前的堆内存。
在这里插入图片描述
可以看到Entity已经消失了。这就是GC root回收,这些都是他的引用链。
我们在上面的图片可以看到
在Java中,可作为GC Root的对象包括以下几种:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中JNI(即一般说的Native方法)引用的对象

这个我们了解了之后,来认识一下四种引用。

  1. 强引用
    强引用就是指在程序代码中普遍存在的,类似Object obj = new Object()这类似的引用,只要强引用在,垃圾搜集器永远不会搜集被引用的对象。也就是说,宁愿出现内存溢出,也不会回收这些对象。
    代码示例:
/**
 * @author 龙小虬
 * @date 2021/4/14 23:03
 */
public class StrongReference {
    public static void main(String[] args) {
        Entity user1 = new Entity("lxq");
        Entity user2 = user1;
        user1=null;
        System.out.println(user2);
    }
}

我们可以发现,我们这里已经把user1清空了,但是user2依旧存在数据。
在这里插入图片描述
这就是强引用,在Entity user1 = new Entity("lxq");也属于强引用,但是为了更好的理解所以才有后面的代码。

  1. 软引用
    软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。
    代码示例:
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.ArrayList;

/**
 * @author 龙小虬
 * @date 2021/4/14 23:13
 * -Xmx8m
 */
public class SoftReferences {
    public static void main(String[] args) {
        ReferenceQueue<byte[]> referenceQueue = new ReferenceQueue<byte[]>();
        ArrayList<SoftReference<byte[]>> objects = new ArrayList<SoftReference<byte[]>>();
        for (int i = 0; i < 10; i++) {
            SoftReference<byte[]> softReference = new SoftReference<byte[]>(new byte[4 * 1024 * 1024], referenceQueue);
            objects.add(softReference);
        }
        Reference<? extends byte[]> poll = referenceQueue.poll();
        System.out.println("打印结果:");
        objects.forEach((t) -> {
            System.out.println(t.get());
        });
    }
}

我们可以看到结果:
在这里插入图片描述
前面的数据全部变为了null,为什么会这样?我们打印的是objects数据,也就是对象ArrayList,它存储的是ReferenceQueue<byte[]>对象的地址,因为内存不够,所以相应地址的内存被释放,所以前面的变为了null,那设置的是30M,不应该有两个不为null?因为不可能所有的内存都给ArrayList,所以只剩下一个不为null。
我们也可以看看是不是真的是这样。加入-XX:+PrintGCDetails -verbose:gc
在这里插入图片描述
可以看到有很多的Full GC,这也验证了我们这个软引用,是在内存不足的时候被回收了内存,那么这么多的null,我们要怎么办?怎么去清理null?加入代码:

while (poll != null) {
    objects.remove(poll);
    poll = referenceQueue.poll();
}

清除null数据即可。
最终代码:

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.ArrayList;

/**
 * @author 龙小虬
 * @date 2021/4/14 23:13
 * -Xmx8m
 * -Xmx8m -XX:+PrintGCDetails -verbose:gc
 */
public class SoftReferences {
    public static void main(String[] args) {
        ReferenceQueue<byte[]> referenceQueue = new ReferenceQueue<byte[]>();
        ArrayList<SoftReference<byte[]>> objects = new ArrayList<SoftReference<byte[]>>();
        for (int i = 0; i < 10; i++) {
            SoftReference<byte[]> softReference = new SoftReference<byte[]>(new byte[4 * 1024 * 1024], referenceQueue);
            objects.add(softReference);
        }
        Reference<? extends byte[]> poll = referenceQueue.poll();
        while (poll != null) {
            objects.remove(poll);
            poll = referenceQueue.poll();
        }
        System.out.println("打印结果:");
        objects.forEach((t) -> {
            System.out.println(t.get());
        });
    }
}

  1. 弱引用
    弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。
    代码示例:
import java.lang.ref.WeakReference;

/**
 * @author 龙小虬
 * @date 2021/4/14 23:06
 */
public class WeakReferences {
    public static void main(String[] args) {
        Entity user1 = new Entity("lxq");
        WeakReference<Entity> user2 = new WeakReference<Entity>(user1);
        user1 = null;
        System.gc();
        System.out.println(user2.get());
    }
}

这里可以发现一旦强引用user1被清空,那么user2在被调用GC回收之后,也会为空,因为new Entity("lxq");已经被回收了,而user2与它只是存在弱引用的关系,所以无法将它留下。
在这里插入图片描述

  1. 虚引用
    虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。
    代码示例:
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

/**
 * @author 龙小虬
 * @date 2021/4/14 23:24
 */
public class PhantomReferenceTset {
    public static void main(String[] args) {
        Entity user = new Entity("lxq");
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
        PhantomReference<Entity> phantomReference = new PhantomReference<Entity>(user, referenceQueue);
        System.out.println(phantomReference.get());
    }
}


这篇关于JVM性能优化------可达性分析算法与四种引用的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程