JVM——垃圾回收

一、如何判断对象可以回收?

引用计数法

当一个对象被其他变量引用时,使其计数+1(若被引用两次,计数为2),若某个变量不在引用它时,使其计数-1;当这个对象引用计数变为0时意味着不再被引用,则可以作为垃圾进行回收

弊端:循环引用时,两个对象的计数都为1,导致两个对象都无法被释放
在这里插入图片描述
【早期Python虚拟机进行垃圾回收控制时采用引用计数算法,Java虚拟机则采用下面算法】

可达性分析算法

判断当前对象是否直接或间接的被根对象必定不能被当作垃圾回收的对象)所引用,如果没有,则进行垃圾回收
●JVM虚拟机中的垃圾回收器通过可达性分析来探索所有存活的对象
●扫描堆中的对象,看能否沿着GC Root对象为起点的引用链找到该对象,如果找不到,则表示可以回收
●哪些对象可以作为GC Root的对象

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

五种引用(四大引用中有五种)
在这里插入图片描述
强引用
只有GC Root都不引用(没有GC Root直接/间接引用)该对象时,才会回收强引用对象

● 如上图B、C对象都不引用A1对象时,A1对象才会被回收

软引用
当GC Root指向软引用对象(没有被直接的强引用所引用)时,在内存不足时,会回收软引用所引用的对象

● 如上图如果B对象不再引用A2对象且内存不足时,软引用所引用的A2对象就会被回收

软引用的使用

public class Demo1 {
	public static void main(String[] args) {
		final int _4M = 4*1024*1024;
		//使用软引用对象 list和SoftReference是强引用,而SoftReference和byte数组则是软引用
		List<SoftReference<byte[]>> list = new ArrayList<>();
		SoftReference<byte[]> ref= new SoftReference<>(new byte[_4M]);
	}
}

如果在垃圾回收时发现内存不足,在回收软引用所指向的对象时,软引用本身不会被清理

如果想要清理软引用,需要使用引用队列

public class Demo1 {
	public static void main(String[] args) {
		final int _4M = 4*1024*1024;
		//使用引用队列,用于移除引用为空的软引用对象
		ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
		//使用软引用对象 list和SoftReference是强引用,而SoftReference和byte数组则是软引用
		List<SoftReference<byte[]>> list = new ArrayList<>();
		SoftReference<byte[]> ref= new SoftReference<>(new byte[_4M]);

		//遍历引用队列,如果有元素,则移除
		Reference<? extends byte[]> poll = queue.poll();
		while(poll != null) {
			//引用队列不为空,则从集合中移除该元素
			list.remove(poll);
			//移动到引用队列中的下一个元素
			poll = queue.poll();
		}
	}
}

大概思路为:查看引用队列中有无软引用,如果有,则将该软引用从存放它的集合中移除(这里为一个list集合)

弱引用
只有弱引用(没有被直接的强引用所引用)引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用所引用的对象
● 如上图如果B对象不再引用A3对象,则A3对象会被回收
弱引用的使用和软引用类似,只是将 SoftReference 换为了 WeakReference

虚引用
当虚引用对象所引用的对象被回收以后,虚引用对象就会被放入引用队列中,调用虚引用的方法

  ● 虚引用的一个体现是释放直接内存所分配的内存,当引用的对象ByteBuffer被垃圾回收以后,
    虚引用对象Cleaner就会被放入引用队列中,然后调用Cleaner的clean方法来释放直接内存
  ● 如上图,B对象不再引用ByteBuffer对象,ByteBuffer就会被回收。但是直接内存中的内存还
    未被回收。这时需要将虚引用对象Cleaner放入引用队列中,然后调用它的clean方法来释放直接内存

终结器引用
所有的类都继承自Object类,Object类有一个finalize方法。当某个对象不再被其他的对象所引用时,会先将终结器引用对象放入引用队列中,然后根据终结器引用对象找到它所引用的对象,然后调用该对象的finalize方法。调用以后,该对象就可以被垃圾回收了

● 如上图,B对象不再引用A4对象。这是终结器对象就会被放入引用队列中,引用队列会根据它, 找到它所引用的对象。然后调用被引用对象的finalize方法。调用以后,该对象就可以被垃圾回收了
引用队列
● 软引用和弱引用可以配合引用队列
弱引用虚引用所引用的对象被回收以后,会将这些引用放入引用队列中,方便一起回收这些软/弱引用对象

● 虚引用和终结器引用必须配合引用队列
虚引用和终结器引用在使用时会关联一个引用队列

小结
在这里插入图片描述

二、垃圾回收算法

标记-清除

在这里插入图片描述
定义:标记清除算法顾名思义,是指在虚拟机执行垃圾回收的过程中,先采用标记算法确定可回收对象,然后垃圾收集器根据标识清除相应的内容,给堆内存腾出相应的空间(具有速度快的优点)

● 这里的腾出内存空间并不是将内存空间的字节清0,而是记录下这段内存的起始结束地址,将其放在空闲的地址列表中,下次分配新对象(内存)时在空闲地址列表中寻找,会直接覆盖这段内存

缺点容易产生大量的内存碎片(清除后不会再对空闲的内存空间做进一步的整理工作),可能无法满足大对象的内存分配,一旦导致无法分配对象,那就会导致jvm启动gc,一旦启动gc,我们的应用程序就会暂停,这就导致应用的响应速度变慢

标记-整理

在这里插入图片描述
标记-整理 会将不被GC Root引用的对象回收,清除其占用的内存空间。然后整理剩余的对象,可以有效避免因内存碎片而导致的问题,但是因为整体需要消耗一定的时间,所以效率较低(缺点)

复制

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
将内存分为等大小的两个区域,FROM和TO(TO中为空)。先将被GC Root引用的对象从FROM放入TO中,再回收不被GC Root引用的对象。然后交换FROM和TO。这样也可以避免内存碎片的问题,但是会占用双倍的内存空间(缺点)。

实际在JVM垃圾回收机制中都会根据不同的情况来采用,不会只采用其中一种算法,会结合其中多种算法共同实现垃圾回收

三、分代回收

将堆内存划分为两块区域(新生代、老年代
在这里插入图片描述

2、为什么要做这样的区域划分?
Java中有的对象可能需要长时间使用,将长时间使用的对象放入老年代中,而那些用完便可丢弃的对象则放入新生代中,这样便可以针对对象生命周期的不同特点进行不同的垃圾回收策略

回收流程

新创建的对象都被放在了新生代的伊甸园
在这里插入图片描述
当伊甸园中的内存不足时,就会进行一次垃圾回收,这时的回收叫做 Minor GC

Minor GC触发后采用可达性分析算法沿着GC Root引用链标记,标记成功后采用复制算法
Minor GC 会将伊甸园和幸存区FROM存活的对象复制到 幸存区 TO中, 并让其寿命加1,再交换两个幸存区
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
再次创建对象,若新生代的伊甸园又满了,则会再次触发 Minor GC(会触发 stop the world, 暂停其他用户线程,只让垃圾回收线程工作),这时不仅会回收伊甸园中的垃圾,还会回收幸存区中的垃圾,再将活跃对象复制到幸存区TO中。回收以后会交换两个幸存区,并让幸存区中的对象寿命加1
在这里插入图片描述

如果幸存区中的对象的寿命超过某个阈值(最大为15,4bit),就会被放入老年代
在这里插入图片描述

如果新生代老年代中的内存都满了,就会先触发Minor GC,再触发Full GC,扫描新生代和老年代中所有不再使用的对象并回收。若老年代分配空间不够会触发OutOfMemoryError:Java heap space(堆内存空间不足导致)

相关VM参数
在这里插入图片描述

GC 分析

大对象处理策略
当遇到一个较大的对象时,就算新生代的伊甸园为空,也无法容纳该对象时,会将该对象直接晋升为老年代,不会引起新生代GC(Minor GC),新生代老年代均不能容纳该对象时会触发内存溢出

线程内存溢出
某个线程的内存溢出了而抛异常(out of memory),不会让其他的线程结束运行

import java.util.ArrayList;

public class Demo {
    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 -Xmn=10M -XX:UseSerialGC -XX:PrintGCDetails -verbose:gc
    /*初始堆空间大小 最大堆空间大小 新生代 
    SerialGC垃圾回收器(JDK8下默认的回收器不是它,改为SerialGC幸存区的比例才不会调整)  
    打印GC详情  打印GC详情*/
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            ArrayList<byte[]> list=new ArrayList<>();
            list.add(new byte[_8MB]);
            list.add(new byte[_8MB]);
        }).start();
        System.out.println("sleep......");
        Thread.sleep(1000L);
    }
}

运行结果:
在这里插入图片描述

这是因为当一个线程抛出OOM异常后,它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行,进程依然正常

四、垃圾回收器

① 串行

● 单线程
● 堆内存较小,个人电脑(CPU核数较少)
在这里插入图片描述
安全点:让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象

因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态

Serial 收集器
Serial收集器是最基本的、发展历史最悠久的收集器

特点:单线程、简单高效(与其他收集器的单线程相比),采用复制算法。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程手机效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)

ParNew 收集器
ParNew收集器其实就是Serial收集器的多线程版本

特点:多线程、ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。和Serial收集器一样存在Stop The World问题

Serial Old 收集器
Serial Old是Serial收集器的老年代版本

特点:同样是单线程收集器,采用标记-整理算法

② 吞吐量优先

● 多线程
● 堆内存较大,多核CPU
● 单位时间内,STW(stop the world,停掉其他所有工作线程)时间最短
● JDK1.8默认使用的垃圾回收器
在这里插入图片描述

Parallel Scavenge 收集器
与吞吐量关系密切,故也称为吞吐量优先收集器

特点:属于新生代收集器也是采用复制算法的收集器(用到了新生代的幸存区),又是并行的多线程收集器(与ParNew收集器类似)

该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与ParNew收集器最重要的一个区别)

GC自适应调节策略:Parallel Scavenge收集器可设置-XX:+UseAdptiveSizePolicy参数。当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。

Parallel Scavenge收集器使用两个参数控制吞吐量:
● XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间
● XX:GCRatio 直接设置吞吐量的大小

Parallel Old 收集器
● 是Parallel Scavenge收集器的老年代版本

特点:多线程,采用标记-整理算法(老年代没有幸存区)

③ 响应时间优先

● 多线程
● 堆内存较大,多核CPU
● 尽可能让单次STW时间变短(尽量不影响其他线程运行)
在这里插入图片描述
CMS 收集器
Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的老年代收集器

特点:基于标记-清除算法实现。并发收集、低停顿,但是会产生内存碎片

应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务

CMS收集器的运行过程分为下列4步

1、初始标记:标记GC Roots能直接到的对象(根对象)。速度很快但是仍存在Stop The World问题

2、并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行

3、重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在Stop The World问题

4、并发清除:对标记的对象进行清除回收

CMS收集器的内存回收过程是与用户线程一起并发执行的

重点: 后两种垃圾回收器都适合堆内存较大的场景,且一般需要多核CPU支持
【多核CPU===>若有多个线程,假设只有一个CPU,工作时多个线程轮流争抢单核CPU的时间片,其效率还不如单线程,好比有多个保洁工人打扫卫生,但只有一把扫帚,此时要打扫卫生需轮流使用这把扫帚,此时效率与一个人打扫是相同的。因此后两种垃圾回收器工作使用场景堆内存较大且为多核CPU才能充分发挥作用】

相关概念

并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态

并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个CPU上

吞吐量:即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )),也就是。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

new一个对象_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值