JVM -- 垃圾回收;垃圾回收算法(三)

阅读前可参考

https://blog.csdn.net/MinggeQingchun/article/details/126947384

https://blog.csdn.net/MinggeQingchun/article/details/127066302

一、标记对象是否为垃圾

JVM的内存结构包括五大区域:程序计数器、虚拟机栈、本地方法栈、堆区、方法区。

其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生、随线程而灭,因此这几个区域的内存分配和回收都具备确定性,就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。

而Java堆区和方法区则不一样,这部分内存的分配和回收是动态的,正是垃圾收集器所需关注的部分

(一)引用计数法

引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,就将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1

  • 优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。

  • 缺点:无法检测出循环引用。 如A对象有一个对B对象的引用,B对象反过来引用A对象。这样,他们的引用计数永远不可能为0

JVM并未采取此种方式,而是可达性分析

(二)可达性分析

Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象

可达性算法是目前主流的虚拟机都采用的算法,程序把所有的引用关系看作一张图,从一个节点GC Roots开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象。

在Java语言中,可作为GC Roots的对象包括下面几种:

(1)虚拟机栈中引用的对象(栈帧中的本地变量表)

(2)方法区中类静态属性引用的对象

(3)方法区中常量引用的对象

(4)本地方法栈中JNI(Native方法)引用的对象

从上图中,reference1、 reference2、reference3都是 GC Root

reference1 --> 对象实例1

reference2 --> 对象实例2

reference3 --> 对象实例4 --> 对象实例6

可以得出对象实例1、2、4、6都具有对象可达性,也就是存活对象,不能被GC回收的对象。而随想实例3、5虽然直接相连,但并没有任何一个GC Roots与之相连,即GC Roots不可达对象,就会被GC回收的对象

二、垃圾回收原理

垃圾回收 GC (Garbage Collection)的基本原理:将内存中不再被使用的对象进行回收,GC中用于回收的方法称为收集器,由于GC需要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代、老年代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停

  • 堆内存被划分为两块,一块的年轻代,另一块是老年代。老年代:年轻代比例为2:1

  • 年轻代又分为Edensurvivor。他俩空间大小比例默认为8:2

  • 幸存区又分为s0(From Spaces1(To Space。这两个空间大小是一模一样的,就是一对双胞胎,他俩是1:1的比例

堆内存垃圾回收过程 

1、新生成的对象首先放到Eden区(伊甸园区,当Eden区满了会触发Minor GC

2、第一步GC活下来的对象,会被移动到survivor中的S0区From SpaceS0区满了之后会触发Minor GC,S0区存活下来的对象会被移动到S1区To Space,S0区空闲

S1满了之后再GC,存活下来的对象再次移动到S0区,S1区空闲,这样反反复复GC,每GC一次,对象的年龄就涨一岁,达到某个阈值后(15),就会进入老年代

3、在发生一次Minor GC后(前提条件),老年代可能会出现Major GC,这个视垃圾回收器而定

Full GC触发条件

  • 手动调用System.gc,会不断的执行Full GC

  • 老年代空间不足/满了

  • 方法区空间不足/满了

stop-the-world(STW)

stop-the-world会在任何一种GC算法中发生。stop-the-world 意味着JVM因为需要执行GC而停止应用程序的执行。

当stop-the-world 发生时,除GC所需的线程外,所有的线程都进入等待状态,直到GC任务完成。GC优化很多时候就是减少stop-the-world 的发生

JVM GC只回收堆内存方法区内的对象。而栈内存的数据,在超出作用域后会被JVM自动释放掉,所以其不在JVM GC的管理范围内

可参考

7种jvm垃圾回收器,这次全部搞懂 - 走看看

1、Minor GC

对新生代的对象的收集

Minor GC指新生代GC,即发生在新生代(包括Eden区和Survivor区)的垃圾回收操作,当新生代无法为新生对象分配内存空间的时候,会触发Minor GC。因为新生代中大多数对象的生命周期都很短,所以发生Minor GC的频率很高,虽然它会触发stop-the-world,但是它的回收速度很快

2、Major GC

对老年代的对象的收集

Major GC清理Tenured区,用于回收老年代,出现Major GC通常会出现至少一次Minor GC

3、Full GC

程序中主动调用System.gc()强制执行的GC

Full GC是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代 永久代perm gen)的全局范围的GC。Full GC不等于Major GC,也不等于Minor GC+Major GC,发生Full GC需要看使用了什么垃圾收集器组合,才能解释是什么样的垃圾回收

三、五种引用

1、强引用

2、软引用

3、弱引用

4、虚引用

5、终结器引用

(一)强引用

只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收

(二)软引用(SoftReference)

仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用 对象 可以配合引用队列来释放软引用自身

如下代码,VM设置对打堆内存20M,注释部分运行循环5次后就会报错堆内存溢出

/**
 * 软引用
 * -Xmx20m -XX:+PrintGCDetails -verbose:gc
 */
public class GC2SoftReference {
    private static final int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        //java.lang.OutOfMemoryError: Java heap space
        /*List<byte[]> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            list.add(new byte[_4MB]);
        }
        System.in.read();*/

        //软引用
        soft();
    }

    public static void soft() {
        // list --> SoftReference --> byte[]

        List<SoftReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
            System.out.println(ref.get());
            list.add(ref);
            System.out.println(list.size());

        }
        System.out.println("循环结束:" + list.size());
        for (SoftReference<byte[]> ref : list) {
            System.out.println(ref.get());
        }
    }
}

使用软引用输出,取值查看时前4个对象都被回收,只有第5个对象存在 

VM加入参数查看输出

-Xmx20m -XX:+PrintGCDetails -verbose:gc

在输出第3个对象时就触发了GC 

软引用队列

/**
 * 软引用, 配合引用队列
 * -Xmx20m -XX:+PrintGCDetails -verbose:gc
 */
public class GC3SoftReferenceQueue {
    private static final int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) {
        List<SoftReference<byte[]>> list = new ArrayList<>();

        // 引用队列
        ReferenceQueue<byte[]> queue = new ReferenceQueue<>();

        for (int i = 0; i < 5; i++) {
            // 关联了引用队列, 当软引用所关联的 byte[]被回收时,软引用自己会加入到 queue 中去
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
            System.out.println(ref.get());
            list.add(ref);
            System.out.println(list.size());
        }

        // 从队列中获取无用的 软引用对象,并移除
        Reference<? extends byte[]> poll = queue.poll();
        while( poll != null) {
            list.remove(poll);
            poll = queue.poll();
        }

        System.out.println("===========================");
        for (SoftReference<byte[]> reference : list) {
            System.out.println(reference.get());
        }

    }
}

(三)弱引用(WeakReference)

仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象,可以配合引用队列来释放弱引用自身

/**
 * 弱引用
 * -Xmx20m -XX:+PrintGCDetails -verbose:gc
 */
public class GC4WeakReference {
    private static final int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) {
        //  list --> WeakReference --> byte[]
        List<WeakReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
            list.add(ref);
            for (WeakReference<byte[]> w : list) {
                System.out.print(w.get()+" ");
            }
            System.out.println();

        }
        System.out.println("循环结束:" + list.size());
    }
}

(四)虚引用(PhantomReference)

必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队, 由 Reference Handler 线程调用虚引用相关方法释放直接内存

(五)终结器引用(FinalReference)

所有对象都继承自Object,而Object中有一个finalize()方法,对象可以重写finalize()方法,在对象进行垃圾回收时该方法将被调用。但是对象已经没有强引用了,finalize()方法其实就是通过终结器引用实现的。在B对象断开A4的强引用后,终结器引用会被加入引用队列,由一个优先级很低的finalizeHandler进行扫描,当扫描到引用队列中的终结器引用后,会执行其所引用的A4对象的finalize()方法。由于finalize()方法不会被立刻执行,而是先进行入队,并且负责扫描的finalizeHandler优先级低,可能导致finalize()迟迟得不到执行,因此不推荐使用它进行资源回收

无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象 暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象

四、垃圾回收算法

(一)标记-清除算法(Mark Sweep)

标记阶段:

标记的过程其实就是前面介绍的可达性分析算法的过程,遍历所有的 GC Roots 对象,对从 GCRoots 对象可达的对象都打上一个标识,一般是在对象的 header 中,将其记录为可达对象

清除阶段:

清除的过程是对堆内存进行遍历,如果发现某个对象没有被标记为可达对象(通过读取对象header 信息),则将其回收

缺点:

(1)会产生内存碎片

(2)有大对象需要分配连续内存空间时,可能二次触发垃圾回收机制

结论:适用于老年代,存活对象较多的情况下比较高效

(二)标记-整理/压缩算法(Mark Compact)

对标记的垃圾进行清理后,将零散的内存空间进行压缩,不断的把活动内存、不连续的内存复制到近似连续的内存空间中去,保证被使用的内存,尽量有空洞存在

优点:避免产生大量内存碎片

缺点:整体效率较低

(三)复制算法(Copy)

标记出所有存活的对象,并将这些存活的对象复制到一块新的内存(图中右边内存空间),之后将运来的内存(图中左边内存空间)全部回收

优点:

(1)效率高,没碎片

(2)仅扫描整个空间一次

缺点:

(1)需要一块空的内存空间

(2)需要复制移动对象

(3)内存利用率较低,且不适合在对象存活率较高的老年底使用

适用于新生代,即”朝生夕死“

五、分代回收算法

分代收集算法就是目前虚拟机使用的回收算法。它解决了标记整理不适用于老年代的问题,将内存分为各个年代。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)

在JDK8以前,它对方法区的实现叫做永久代,它就是使用了堆的一部分,作为方法区

而在JDK8以后,移除了永久代的实现,换了一种元空间的实现,元空间使用了操作系统的一部分(一些内存 )作为了方法区,而不再是堆的一部分

在不同年代使用不同的算法,从而使用最合适的算法,新生代存活率低,可以使用复制算法。而老年代对象存活率搞,没有额外空间对它进行分配担保,所以只能使用标记清除或者标记整理算法

对象首先分配在伊甸园区域

新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的 对象年龄加 1并且交换 from to

minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行

当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)

当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时 间更长

设置VM相关参数 

参数 含义
-Xms 堆初始大小(堆内存初始大小,单位m、g)
-Xmx 或 -XX:MaxHeapSize=size 堆最大大小,一般不要大于物理内存的80%
-Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size ) 新生代大小
-XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy 幸存区比例(动态)
-XX:SurvivorRatio=ratio 幸存区比例
-XX:MaxTenuringThreshold=threshold 晋升老年代阈值
-XX:+PrintTenuringDistribution 晋升详情
-XX:+PrintGCDetails -verbose:gc GC详情
-XX:+ScavengeBeforeFullGC FullGC 前 MinorGC
-XX:PermSize 非堆内存初始大小,一般应用设置初始化200m,最大1024m就够了
-XX:MaxPermSize 非堆内存最大允许大小
-XX:SurvivorRatio=8 年轻代中Eden区与Survivor区的容量比例值,默认为8,即8:1
-XX:+DisableExplicitGC 关闭System.gc()
-XX:+CollectGen0First FullGC时是否先YGC,默认false
-XX:TLABWasteTargetPercent TLAB占eden区的百分比,默认是1%
-Xnoclassgc 禁用垃圾回收

TLAB 内存 

TLAB全称是Thread Local Allocation Buffer线程本地分配缓存,从名字上看是一个线程专用的内存分配区域,是为了加速对象分配而生的。

每一个线程都会产生一个TLAB,该线程独享的工作区域,Java虚拟机使用这种TLAB区来避免多线程冲突问题,提高了对象分配的效率。

TLAB空间一般不会太大,当大对象无法在TLAB分配时,则会直接分配到堆上

参数 含义
-Xx:+UseTLAB 使用TLAB
-XX:+TLABSize 设置TLAB大小
-XX:TLABRefillWasteFraction 设置维护进入TLAB空间的单个对象大小,他是一个比例值,默认为64,即如果对象大于整个空间的1/64,则在堆创建
-XX:+PrintTLAB 查看TLAB信息
Xx:ResizeTLAB 自调整TLABRefillWasteFraction阀值
/**
 * 分代回收
 * -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC
 */
public class GC5Generational {
    private static final int _512KB = 512 * 1024;
    private static final int _1MB = 1024 * 1024;
    private static final int _6MB = 6 * 1024 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    private static final int _8MB = 8 * 1024 * 1024;

    // -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC
    public static void main(String[] args) throws InterruptedException {

    }
}

伊甸园 

伊甸园对象到From、to空间中, From、to空间交换

大对象直接晋升老年代

OOM

猜你喜欢

转载自blog.csdn.net/MinggeQingchun/article/details/127089533