首页 前端知识 JVM之回收堆区

JVM之回收堆区

2024-05-19 09:05:52 前端知识 前端哥 393 86 我要收藏

堆上的对象是否能被回收,是根据对象是否被阴影来决定的。如果对象被引用了,说明对象还被使用,不允许回收(这种说明不准确,后面修正,目前就先这么理解)。

判断堆上的对象可被回收的俩种方法:

 1、引用计数法:为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1。优点是实现简单,C++中的智能指针采用了引用计数器。    缺点:每次引用和取消引用都需要维护计数器,对系统有一定的影响。且存在循环引用问题,例子:a引用了b,b也引用了a,这样这俩个对象就都无法回收。

 2、可达性分析法:Java虚拟机就是使用的这个方法,将对象分为俩类:垃圾回收的根对象(GC Root)和普通对象,对象与对象之间存在引用关系。如果从某个普通对象通过引用链到某个GC Root对象是可达的,那么该普通对象就不可回收。根对象一般是不可被回收的,Java虚拟机上保存着一个根对象的列表。

 根对象有4大对象,不属于这4类任意一个就是普通对象:

    1、线程对象(Thread),引用线程栈帧中的方法参数、局部变量等。线程对象有对当前线程的栈内存的引用。

    2、系统类加载器加载的java.lang.Class对象(System Class),引用类中 的静态变量。

    3、监视器对象(Busy Monitor),用于保存同步锁synchronized关键字持有的对象。

    4、本地方法调用时使用的全局对象(JNI Global)。(由Java虚拟机调用,不需要程序员过多关注)

5种常见的对象引用:

   1、强引用:可达性算法中描述对象引用,一般就是强引用,即GC Root对象对普通对象有引用关系。

   2、软引用:相比于强引用是一种比较弱的引用关系,如果一个对象只有软引用关联它,当程序内存不够时,就会将软引用的对象回收。常用于缓存中,用softReference类(该类也需要GC Root对象来引用,不然也会被回收)来实现软引用。

软引用中对象如果存在内存空间不足时回收,softReference对象本身也需要被回收。SoftReference提供了一套机制:1、软引用创建时,通过构造器传入引用队列。2、在软引用中包含的对象被回收时,该软引用对象会被放入引用队列中。3、通过遍历引用队列,将softReference为空的强引用删除。

package org.example.principle.garbage.soft;

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

public class SoftReference3 {
    public static void main(String[] args) {
        //把堆内存最大值设为200m,这样for每循环一次,内存都会不足,都会回收上一个创建的软引用

        ArrayList<SoftReference> softReferences = new ArrayList<>();
        //创建一个引用队列.queues中存放的是已经释放掉的软引用
        ReferenceQueue<byte[]> queues = new ReferenceQueue<byte[]>();
        //循环10次,前9个被回收,所以最后count值为9.
        for (int i = 0; i < 10; i++) {
            byte[] bytes=new byte[1024*1024*100];
            SoftReference soft=new SoftReference<byte[]>(bytes,queues);
            //将创建的软引用对象存放到一个集合中,防止被回收.
            softReferences.add(soft);
        }

        SoftReference<byte[]> ref=null;
        int count=0;
        while ((ref=(SoftReference<byte[]>) queues.poll())!=null){
            count++;
        }
        System.out.println(count);
    }
}

   3、弱引用:整体机制与软引用基本一致,区别在于弱引用的对象在回收时,不够内存够不够用都会直接被回收。提供WeakReference类实现弱引用。主要用在ThreadLocal中使用。

package org.example.principle.garbage.weak;

import java.lang.ref.WeakReference;

public class WeakReference1 {
    public static void main(String[] args) {
        byte[] bytes=new byte[1024*1024*100];
        WeakReference<byte[]> reference=new WeakReference<byte[]>(bytes);
        bytes =null;
        System.out.println(reference.get());

        System.gc();
        System.out.println(reference.get());
    }
}

   4、虚引用(常规开发中,不会使用):也叫幽灵引用、幻影引用,不能通过虚引用对象获取到对象。唯一用途是当对象被垃圾回收时可以接收到对应的通知。使用PhantomReference类实现。

   5、终结器引用(常规开发中,不会使用):指对象需要被回收时,对象将会被放入在Finalizer类中的引用队列中,并稍后由一条由FinalizerThread线程从队列中获取对象,然后执行对象的finalizer方法。对象在被第二次回收时,才会被真正回收。这个过程中可以使用finalizer方法在将自身对象使用强引用关联上,但不建议这么做。

该代码仅仅是为了更好的演示终结器引用中的finalize自救,在开发中是决定不会这么去写代码的。

package org.example.principle.garbage.finalize;

public class FinalizerReference {
    private static FinalizerReference reference=null;
    public void alive(){
        System.out.println("还活着");
    }

    //该finalize方法只能被调用一次。
    //所以第一次调用test方法,被救活了,第二次调用test方法就不会在调用finalize方法了,所以就被回收了
    @Override
    protected void finalize() throws Throwable {
        try {
            System.out.println("finalizer 执行了");
            //设置强引用自救
            reference=this;
        }finally {
            super.finalize();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        reference=new FinalizerReference();
        test();
        test();
    }
    private static void test() throws InterruptedException {
        reference=null;

        System.gc();
        //执行finalize方法的优先级比较低,休眠一会等一下。
        Thread.sleep(500);
        if (reference!=null){
            reference.alive();
        }else {
            System.out.println("对象被回收");
        }
    }
}

该finalize方法只能被调用一次。所以第一次调用test方法,被救活了,第二次调用test方法就不会在调用finalize方法了,所以就被回收了。

垃圾回收过程会通过一个单独的GC线程来完成,但不管使用哪一种GC算法,都有部分阶段需要停止所有用户线程,这个过程称为Stop The World 简称STW,如果STW时间过长则影响用户使用。

设置虚拟机参数  -XX:+UseSerialGC参数使用分代垃圾的垃圾回收器,运行程序.新生代和老年代都使用串行回收器。

package org.example.principle.garbage.coreIdea;

import lombok.SneakyThrows;

import java.util.LinkedList;
import java.util.List;

public class StopTheWorldText {
    public static void main(String[] args) {
        
        new PrintThread().start();
        new ObjectThread().start();
    }
}

class PrintThread extends Thread{

    @SneakyThrows
    @Override
    public void run() {
        //记录开始时间

        long last=System.currentTimeMillis();
        while (true) {
            long now=System.currentTimeMillis();
            System.out.println("时间差"+(now-last));
            last=now;
            Thread.sleep(100);
        }
    }
}

class ObjectThread extends Thread{

    @SneakyThrows
    @Override
    public void run() {
        List<byte[]> bytes=new LinkedList<>();
        while (true) {
            //最多存放1g,然后删除强引用,垃圾回收释放1g
            if (bytes.size()>=10){
                bytes.clear();
                System.out.println("回收了");
            }
            bytes.add(new  byte[1024*1024*100]);
            Thread.sleep(10);
        }
    }
}

大部分时间差都很短,有的时间差特=却有的长,超过了1s,这就是STW阶段。

判断GC是否优秀,可以从3个方面来考虑:

  1、吞吐量:指的是CPU用于执行用户代码的时间与CPU总执行时间的比值,吞吐量越高,垃圾回收效率越高。

  2、最大暂停时间:指的是所有垃圾回收过程中的STW时间的最大值。最大暂停时间越短,用户使用系统收到的影响越小。

  3、堆使用效率:不同垃圾回收算法,对堆的使用效率不同。比如标记清除算法可以使用完整的内存,复制算法只能使用一半内存。

以上这3个方面,同一种GC不可兼得,不同的GC算法适用于不同的场景。

介绍一下分代垃圾回收算法,应用最广的垃圾回收算法:

  分代垃圾回收算法将整个内存分为年轻代和老年代。

 年轻代,也叫新生代,young区(-Xmn 设置新生代的大小):用于存放存活时间较短的对象。该区中有个伊甸园(Eden区)用于存放刚创建出来的对象;还有俩块幸存区(Survivor)s0(初始为from区)和s1(初始为to区)。虚拟机参数 -XX:SurvivorRatio=x 设置伊甸园和幸存区的比例,默认为8,就是新生代1g的内存,伊甸园800m,s0和s1各100m。

 老年代:也叫old区,用于存放存活时间较长的对象。

分代垃圾回收流程:

 创建出来的对象会放入Eden伊甸园区,随着对象增多,如果Eden满了,新建的对象放不下,就会触发年轻代的GC,成为Minor GC或者Young GC。Minor GC会把伊甸园中会from区需要回收的对象回收,把没有回收的放入to区。接下来,s0变为to区,s1变为from区,当Eden慢时,,会再次发生Minor GC。

 每次Minor GC中都会为对象记录它的年龄,每次从from转移到to区,年龄都会加1,初始值为0.

如果对象的年龄达到阀值(最大值为15,是机器最大只能识别15),对象就会被转移到老年代。不是必须年龄到阀值才能进入老年代,当Minor后年轻代空间还是不足时,就会把一些年龄相对于年长的放入老年代。

当老年代空间不足时,就会触发Full GC ,Full GC会对整个堆进行回收。如果Full GC后,空间还是不足,就会抛出内存溢出异常。

分代GC算法将堆分为年轻代和老年代的原因:

 1、可以通过调整年轻代和老年代的比例适应不同的应用程序,提供内存的利用和性能

 2、新生代和老年代使用不同的垃圾回收算法,新生代一般使用选择复制算法,老年代使用标记清除和标记清理算法,灵活度高。

 3、分代设计中允许只回收年轻代(Minor GC),减少了Full GC的次数,就减少了STW时间。

下面介绍一个现在普遍使用的,推荐使用的垃圾回收器:G1垃圾回收器

G1垃圾回收器优点:1、支持巨大的堆内存回收,并由较高的吞吐量;2、支持多CPU并垃圾回收;3、运行用户设置最大暂停时间。

G1将整个堆内存划分成多个大小相等的区域成为Region,区域不是连续的,分为Eden、Survivor、old区。Region区的大小通过堆空间代下/2048得到,也可以通过-XX:G1HeapRegionSize=32m指定(32m指的是Region的大小),Region的大小必须为2的倍数,取值为1m到32m。

G1垃圾回收器由=有俩种回收方式:

 1、年轻代回收(young GC):回收Eden和Survivor区中不用的对象。会导致STW,G1中可以通过参数 -XX:MaxGCPauseMills=值 (默认为200)设置每次垃圾回收时的最大暂停时间毫秒数,G1会尽可能的保证最大暂停时间

  2、混合回收(Mixed GC)

执行流程:

1、创建出来的对象会放入Eden伊甸园区,当G1判断年轻代不足时(max 默认为60%,就是Eden和survivor的内存占总堆内存的60%),无法分配对象时需要回收时触发Young GC。

 2、标记出Eden和Survivor区域中的存活对象。

 3、根据配置的最大暂停时间选择某些区域将其中的存活对象复制到一个新的survivor中(年龄加1),清空这些区域。         注:G1在young GC过程中会记录每次垃圾回收时的每个Eden区和Survivor区的平均时耗,以作为下次回收的依据。

  4、后续的Young GC与之前的相同,只不过是从一个Survivor转移到了另一个Survivor中。

  5、当某个存活对象的年龄达到阀值(默认为15),就会放入老年代。

  6、部分大对象如果大小超过一个Region的一半,就会直接放入老年代,这类老年代被称为Humongous区。如果对象过于大,就会横跨多个Region。

 7、多次回收之后,会出现很多old老年代区,此时总堆占有率达到阀值时(-XX:InitiatingHeapOccupancyPercent 默认为45%)会触发混合回收。回收所有年轻代和部分老年代对象以及大对象区。采用复制算法完成。

 如果清理过程中发现没有足够的空Region存放转移的对象,会出现Full GC。单线程执行标记-整理算法,此时会导致用户线程的暂停就是STW。

转载请注明出处或者链接地址:https://www.qianduange.cn//article/8849.html
标签
jvm
评论
会员中心 联系我 留言建议 回顶部
复制成功!