java垃圾回收机制

jvm垃圾回收机制

JVM 垃圾回收

1、如何判断对象可以回收

引用计数法
  • 如果一个对象被其他变量所引用,则让该对象的引用计数+1,如果该对象被引用2次则其引用计数为2,依次类推。
  • 某个变量不再引用该对象,则让该对象的引用计数-1,当该对象的引用计数变为0时,则表示该对象没用被其他变量所引用,这时候该对象就可以被作为垃圾进行回收。

引用计数法弊端:循环引用时,两个对象的引用计数都为1,导致两个对象都无法被释放回收。最终就会造成内存泄漏!

img

可达性分析算法

可达性分析算法就是JVM中判断对象是否是垃圾的算法:该算法首先要确定GC Root(根对象,就是肯定不会被当成垃圾回收的对象)。

在垃圾回收之前,JVM会先对堆中的所有对象进行扫描,判断每一个对象是否能被GC Root直接或者间接的引用,如果能被根对象直接或间接引用则表示该对象不能被垃圾回收,反之则表示该对象可以被回收:

  • JVM中的垃圾回收器通过可达性分析来探索所有存活的对象。
  • 扫描堆中的对象,看能否沿着GC Root为起点的引用链找到该对象,如果找不到,则表示可以回收,否则就可以回收。

可以作为GC Root的对象

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

img

强引用

上图实心线表示强引用:比如,new 一个对象M,将对象M通过=(赋值运算符),赋值给某个变量m,则变量m就强引用了对象M。

强引用的特点:只要沿着GC Root的引用链能够找到该对象,就不会被垃圾回收;只有当GC Root都不引用该对象时,才会回收强引用对象。

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

img

上图中宽虚线所表示的就是软引用:

软引用的特点:当GC Root指向软引用对象时,若内存不足,则会回收软引用所引用的对象

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

软引用的使用

public class Demo1 {
	public static void main(String[] args) {
		final int _4M = 4*1024*1024;
        // 软引用对象内部包装new byte[_4M]对象
        SoftReference<byte[]> ref= new SoftReference<>(new byte[_4M]);
		// 这时List 跟 SoftReference之间是强引用,SoftReference 跟 byte[] 之间是软引用
		List<SoftReference<byte[]>> list = new ArrayList<>();
	}
}

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

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

img

public class Demo04 {
    final static int _4M = 4 * 1024 * 1024;

    public static void main(String[] args) {

        // List和SoftReference是强引用,而SoftReference和byte数组则是软引用
        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[_4M], 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());
        }
    }
}

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

弱引用

img

只有当弱引用引用该对象,在垃圾回收时,无论内存是否充足,都会回收弱引用所引用的对象。

  • 如上图如果B对象不再引用A3对象,则A3对象会被回收。

弱引用的使用和软引用类似,只是将 SoftReference 换为了 WeakReference。

虚引用

img

当引用的对象ByteBuffer被垃圾回收以后,虚引用对象Cleaner就会被放入引用队列中:

img

然后调用Cleaner的clean方法(Unsafe.freeMemory())来释放直接内存:

img

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

img

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

img

  • 如上图,B对象不再引用A4对象。这是终结器对象就会被放入引用队列中,引用队列会根据它,找到它所引用的对象。然后调用被引用对象的finalize()方法。调用以后,该对象就可以被垃圾回收了。
引用队列
  • 软引用和弱引用可以配合引用队列(也可以不配合):
    • 弱引用虚引用所引用的对象被回收以后,会将这些引用放入引用队列中,方便一起回收这些软/弱引用对象。
  • 虚引用和终结器引用必须配合引用队列:
    • 虚引用和终结器引用在使用时会关联一个引用队列。
小结

img

  • 强引用:无论内存是否足够,不会回收。

  • 软引用:内存不足时,回收该引用关联的对象。(可以选择配合引用队列使用,也可以不配合)。

  • 弱引用:垃圾回收时,无论内存是否足够,都会回收。(可以选择配合引用队列使用,也可以不配合)。

  • 虚引用:任何时候都可能被垃圾回收器回收。(必须配合引用队列使用)。

2、垃圾回收算法

标记-清除

img

定义:标记清除算法顾名思义,是指在虚拟机执行垃圾回收的过程中,先采用标记算法确定可回收对象,然后垃圾收集器根据标识,清除相应的内容,给堆内存腾出相应的空间。

  • 这里的腾出内存空间并不是将内存空间的字节清 0,而是记录下这段内存的起始结束地址,下次分配内存的时候,会直接覆盖这段内存。

缺点容易产生大量的内存碎片,可能无法满足大对象的内存分配,一旦导致无法分配对象,那就会导致jvm启动gc,一旦启动gc,我们的应用程序就会暂停,这就导致应用的响应速度变慢。

标记-整理

img

标记-整理:会将不被GC Root引用的对象回收,清除其占用的内存空间。然后整理剩余的对象,可以有效避免因内存碎片而导致的问题,但是牵扯到对象的整理移动,需要消耗一定的时间,所以效率较低。

复制

img

当需要回收对象时,先将GC Root直接引用的的对象(不需要回收)放入TO中:

img

img

然后清除FROM中的需要回收的对象:

img

最后 交换 FROMTO 的位置:(FROM换成TO,TO换成FROM)

img

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

3、分代垃圾回收

长时间使用的对象放在老年代中(长时间回收一次,回收花费时间久),用完即可丢弃的对象放在新生代中(频繁需要回收,回收速度相对较快):

img

回收流程

新创建的对象都被放在了新生代的伊甸园中:

img

img

当伊甸园中的内存不足时,就会进行一次垃圾回收,这时的回收叫做 Minor GC

Minor GC 会将伊甸园和幸存区FROM仍需要存活的对象复制到 幸存区 TO中, 并让其寿命加1,再交换FROM和TO

img

伊甸园中不需要存活的对象清除:

img

交换FROM和TO

img

同理,继续向伊甸园新增对象,如果满了,则进行第二次Minor GC:

流程相同,仍需要存活的对象寿命+1:(下图中FROM中寿命为1的对象是新从伊甸园复制过来的,而不是原来幸存区FROM中的寿命为1的对象,这里只是静态图片不好展示,只能用文字描述了)

img

再次创建对象,若新生代的伊甸园又满了,则会再次触发 Minor GC(会触发 stop the world, 暂停其他用户线程,只让垃圾回收线程工作),这时不仅会回收伊甸园中的垃圾,还会回收幸存区中的垃圾,再将活跃对象复制到幸存区TO中。回收以后会交换两个幸存区,并让幸存区中的对象寿命加1!

如果幸存区中的对象的寿命超过某个阈值(最大为15,4bit),就会被放入老年代中:

img

如果新生代老年代中的内存都满了,就会先触发Minor Gc,再触发Full GC,扫描新生代和老年代中所有不再使用的对象并回收:

img

小结:

  • 新创建的对象首先会被分配在伊甸园区域。
  • 新生代空间不足时,触发Minor GC,伊甸园和 FROM幸存区需要存活的对象会被COPY到TO幸存区中,存活的对象寿命+1,并且交换FROM和TO。
  • Minor GC会引发 Stop The World:暂停其他用户的线程,等待垃圾回收结束后,用户线程才可以恢复执行。
  • 当对象寿命超过阈值15时,会晋升至老年代。
  • 如果新生代、老年代中的内存都满了,就会先触发Minor GC,再触发Full GC,扫描新生代和老年代中所有不再使用的对象并回收。
相关JVM参数

img

GC 分析

大对象处理策略:
当遇到一个较大的对象时,就算新生代的伊甸园为空,也无法容纳该对象时,会将该对象直接晋升为老年代。

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

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

4、垃圾回收器

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

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

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

下面来了解一下垃圾回收器的分类:

4.1 串行
  • 单线程
  • 适用场景:内存较小,个人电脑(CPU核数较少)。

串行垃圾回收器开启语句:-XX:+UseSerialGC = Serial + SerialOld

Serial:表示新生代,采用复制算法SerialOld:表示老年代,采用的是标记整理算法

img

安全点:让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象。

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

4.1.1 Serial 收集器

Serial(新生代)收集器是最基本的、发展历史最悠久的收集器:

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

4.1.2 ParNew 收集器

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

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

4.1.3 Serial Old 收集器

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

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

4.2 吞吐量优先
  • 多线程
  • 适用场景:堆内存较大,多核CPU
  • 单位时间内,让STW(stop the world,停掉其他所有工作线程)时间最短
  • JDK1.8默认使用的垃圾回收器
// 1.吞吐量优先垃圾回收器开关:(默认开启)
-XX:+UseParallelGC~-XX:+UseParallelOldGC
// 2.采用自适应的大小调整策略:调整新生代(伊甸园 + 幸存区FROM、TO)内存的大小
-XX:+UseAdaptiveSizePolicy  
// 3.调整吞吐量的目标:吞吐量 = 垃圾回收时间/程序运行总时间
-XX:GCTimeRatio=ratio
// 4.垃圾收集最大停顿毫秒数:默认值是200ms
-XX:MaxGCPaiseMillis=ms
// 5.控制ParallelGC运行时的线程数
-XX:ParallelGCThreads=n

img

4.2.1 Parallel Scavenge 收集器

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

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

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

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

Parallel Scavenge收集器使用两个参数控制吞吐量:

  • XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间。
  • XX:GCRatio 直接设置吞吐量的大小。
4.2.2 Parallel Old 收集器

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

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

4.3 响应时间优先
  • 多线程
  • 适用场景:堆内存较大,多核CPU
  • 尽可能单次STW时间变短(尽量不影响其他线程运行)

虚拟机参数:

// 开关:
-XX:+UseConMarkSweepGC~-XX:+UseParNewGC~SerialOld  
// ParallelGCThreads=n并发线程数 
// ConcGCThreads=threads并行线程数
-XX:ParallelGCThreads=n~-XX:ConcGCThreads=threads
// 执行CMS垃圾回收的内存占比:预留一些空间保存浮动垃圾
-XX:CMSInitiatingOccupancyFraction=percent
// 重新标记之前,对新生代进行垃圾回收
-XX:+CMSScavengeBeforeRemark

img

4.3.1 CMS 收集器

Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的老年代收集器:

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

应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务。
CMS收集器的运行过程分为下列4步:

  • 初始标记:标记GC Roots能直接到的对象。速度很快但是仍存在Stop The World问题。
  • 并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。
  • 重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在Stop The World问题。
  • 并发清除:对标记的对象进行清除回收。

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

4.4 G1
定义

Garbage First,JDK 9以后默认使用,而且替代了CMS 收集器:

img

适用场景

  • 同时注重吞吐量和低延迟(响应时间)。
  • 超大堆内存(内存大的),会将堆内存划分为多个大小相等的区域。
  • 整体上是标记-整理算法,两个区域之间是复制算法。

相关参数:JDK8 并不是默认开启的,需要参数开启:

// G1开关
-XX:+UseG1GC
// 所划分的每个堆内存大小:
-XX:G1HeapRegionSize=size
// 垃圾回收最大停顿时间
-XX:MaxGCPauseMillis=time
4.4.1 G1垃圾回收阶段

img

新生代伊甸园垃圾回收—–>内存不足,新生代回收+并发标记—–>回收新生代伊甸园、幸存区、老年代内存——>新生代伊甸园垃圾回收(重新开始)。

4.4.2 Young Collection 新生代垃圾回收

分区算法region

分代是按对象的生命周期划分,分区则是将堆空间划分连续几个不同小区间,每一个小区间独立回收,可以控制一次回收多少个小区间,方便控制 GC 产生的停顿时间。

E:伊甸园 S:幸存区 O:老年代

  • 会触发STW:

img

垃圾回收时,会把伊甸园(E)的幸存对象复制到幸存区(S):

img

当幸存区(s)中的对象也比较多触发垃圾回收,且幸存对象寿命超过阈值时,幸存区(S)中的一部分对象(寿命达到阈值)会晋升到老年代(O),寿命未达到阈值的会被再次复制到另一个幸存区(S):

img

4.4.3 Young Collection + CM 新生代垃圾回收和并发标记

CM:并发标记!

  • 在 Young GC 时会对 GC Root 进行初始标记。
  • 老年代占用堆内存的比例达到阈值时,进行并发标记(不会STW),阈值可以根据用户来进行设定:
-XX:InitiatingHeapOccupancyPercent=percent // 默认值45%

img

4.4.4 Mixed Collection 混合收集

会对E、S 、O 进行全面的回收

  • 最终标记
  • 拷贝存活
//  用于指定GC最长的停顿时间
-XX:MaxGCPauseMillis=ms

:为什么有的老年代被拷贝了,有的没拷贝?

因为指定了最大停顿时间,如果对所有老年代都进行回收,耗时可能过高。为了保证时间不超过设定的最大停顿时间,会根据最大停顿时间,有选择的回收最有价值的老年代(回收后,能够得到更多内存)。

img

G1在老年代内存不足时(老年代所占内存超过阈值):

  • 如果垃圾产生速度慢于垃圾回收速度,不会触发Full GC,还是并发地进行清理。
  • 如果垃圾产生速度快于垃圾回收速度,便会触发Full GC。
4.4.5 Full GC
  • SerialGC
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • ParallelGC
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • CMS
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集,需要分2种情况,这里不做详细介绍
  • G1
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集,需要分2种情况,这里不做详细介绍
4.4.6 Young Collection 跨代引用
  • 新生代回收的跨代引用(老年代引用新生代)问题:

img

  • 卡表与Remembered Set

    • Remembered Set 存在于E中,用于保存新生代对象对应的脏卡:
    • 脏卡:O被划分为多个区域(一个区域512K),如果该区域引用了新生代对象,则该区域被称为脏卡。
  • 在引用变更时通过post-write barried + dirty card queue。

  • concurrent refinement threads 更新 Remembered Set。

img

4.4.7 Remark
  • 重新标记阶段
  • 在垃圾回收时,收集器处理对象的过程中:
    • 黑色:已被处理,需要保留的
    • 灰色:正在处理中的
    • 白色:还未处理的

img

但是在并发标记过程中,有可能A被处理了以后未引用C,但该处理过程还未结束,在处理过程结束之前A引用了C,这时就会用到remark。

过程如下:

  • 之前C未被引用,这时A引用了C,就会给C加一个写屏障,写屏障的指令会被执行,将C放入一个队列当中,并将C变为 处理中 状态。
  • 在并发标记阶段结束以后,重新标记阶段会STW,然后将放在该队列中的对象重新处理,发现有强引用引用它,就会处理它。

img

img

4.4.8 JDK 8u20 字符串去重

优点与缺点

  • 优点:节省了大量内存。
  • 缺点:新生代回收时间增加,导致略微多占用CPU。

字符串去重开启指令 -XX:+UseStringDeduplication

案例分析:

String s1 = new String("hello");// 底层存储为:char[]{'h','e','l','l','o'}
String s2 = new String("hello");// 底层存储为:char[]{'h','e','l','l','o'}
  • 将所有新分配的字符串(底层是char[])放入一个队列。
  • 当新生代回收时,G1并发检查是否有重复的字符串。
  • 如果字符串的值一样,就让他们引用同一个字符串对象。
  • 注意,其与String.intern()的区别:
    • intern关注的是字符串对象。
    • 字符串去重关注的是char[]数组。
    • 在JVM内部,使用了不同的字符串标。

img

4.4.9 JDK 8u40 并发标记类卸载

在所有对象经过并发标记阶段以后,就能知道哪些类不再被使用。如果一个类加载器的所有类都不在使用时,则卸载它所加载的所有类。

并发标记类卸载开启指令:-XX:+ClassUnloadWithConcurrentMark 默认启用。

4.4.10 JDK 8u60 回收巨型对象

H表示巨型对象,当一个对象占用大于region的一半时,就称为巨型对象。
G1不会对巨型对象进行拷贝。
回收时被优先考虑。
G1会跟踪老年代所有incoming引用,如果老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉。

img

巨型对象越早回收越好,最好是在新生代的垃圾回收就回收掉~

4.4.11 JDK 9 并发标记起始时间的动态调整
  • 并发标记必须在堆空间占满前完成,否则就退化为 Full GC。
  • JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent 设置阈值,默认是 45%。
  • JDK 9 可以动态调整:(目的是尽可能的避免并发标记退化成 Full GC)
    • -XX:InitiatingHeapOccupancyPercent:用来设置初始阈值。
    • 在进行垃圾回收时,会进行数据采样并动态调整阈值。
    • 总会添加一个安全的空挡空间,用来容纳那些浮动的垃圾。

并发标记类卸载开启指令:-XX:+ClassUnloadWithConcurrentMark 默认启用。

4.4.10 JDK 8u60 回收巨型对象

H表示巨型对象,当一个对象占用大于region的一半时,就称为巨型对象。
G1不会对巨型对象进行拷贝。
回收时被优先考虑。
G1会跟踪老年代所有incoming引用,如果老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉。

[外链图片转存中…(img-2775SAwR-1639636370837)]

巨型对象越早回收越好,最好是在新生代的垃圾回收就回收掉~

4.4.11 JDK 9 并发标记起始时间的动态调整
  • 并发标记必须在堆空间占满前完成,否则就退化为 Full GC。
  • JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent 设置阈值,默认是 45%。
  • JDK 9 可以动态调整:(目的是尽可能的避免并发标记退化成 Full GC)
    • -XX:InitiatingHeapOccupancyPercent:用来设置初始阈值。
    • 在进行垃圾回收时,会进行数据采样并动态调整阈值。
    • 总会添加一个安全的空挡空间,用来容纳那些浮动的垃圾。
  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值