JVM 虚拟机 ---->垃圾收集算法

文章目录

    • JVM 垃圾收集器
      • 一、判断对象是否存活
        • 1、引用计数算法
          • (1)基本思路
          • (2)存在的问题
        • 2、可达性分析算法
          • (1)基本思路
      • 二、Java 中的四种引用方式
        • 1、强引用(Strong Reference)
          • (1)弱化方式1
          • (2)弱化方式2
        • 2、软引用(Soft Reference)
        • 3、弱引用(Weak Reference)
        • 4、虚引用(Phantom Reference)
      • 二、垃圾收集算法
        • 1、分代收集理论
          • (1)分代存储
          • (2)分代收集
        • 2、==垃圾收集算法==
        • (1)标记-清除算法(Mark-Sweep)
            • 算法实现思路
            • 存在的问题
          • (2)标记-复制算法(Copying)
            • 算法实现思路
            • 算法特点
            • 存在的问题
          • 3、标记-整理算法(Mark-Compact)
            • 算法实现思路
          • 4、总结

JVM 垃圾收集器

一、判断对象是否存活

1、引用计数算法

(1)基本思路
  • 在对象中添加一个引用计数器
  • 每当有一个地方引用这个对象时,计数器就+1
  • 每当有一个引用失效时,计数器就会-1
  • 当计数器为0的时候,那么该对象就是可被 GC 回收的对象
(2)存在的问题

对象循环引用

a对象引用b对象,b对象也引用a对象,a、b对象没有再被其他对象引用,正常来说这两个对象已经是垃圾了,因为没有被其他对象再引用,但是计数器的数值却不是 0 ,所以使用此算法就无法被回收

JVM 虚拟机 ---->垃圾收集算法_第1张图片

2、可达性分析算法

(1)基本思路
  • 通过定义了一系列称为“GC Roots”的根对象作为起始节点集
  • GC Roots 开始,根据引用关系往下进行搜索,查找的路径我们把它称为 “引用链”
  • 当一个对象到 GC Roots之间没有任何引用链相连时(对象与GC Roots之间不可达),那么该对象就是可被GC回收的垃圾对象

可达性分析算法也是JVM 默认使用的寻找垃圾算法。

比如执行一个方法,这个方法就被当做 GC Roots 根对象,方法内的所有对象都会与跟根对象产生关联,当这些对象与 GC Roots 有关联时,不会被当做垃圾回收,当没有关联时,就会被回收

例如

Object 6Object 7Object 8彼此之前有引用关系,但是没有与"GC Roots"相连,那么就会被当做垃圾所回收。

JVM 虚拟机 ---->垃圾收集算法_第2张图片

二、Java 中的四种引用方式

1、强引用(Strong Reference)

  • 强引用是最普遍的引用
  • 当一个对象是强引用,垃圾回收器绝不会回收他
  • 当内存不足时,JVM 宁愿抛出 OutOfMemoryError 错误,也不会回收具有强引用的对象,来解决内存不足问题
Object strongReference = new Object();
  • 如果强引用对象不使用时,需要 弱化从而使 GC 能够回收
(1)弱化方式1
  • 显示的设置 strongReference 对象为null,则 gc 认为该对象不存在引用,这时就可以回收这个对象

  • 如果 strongReference 是 全局变量,就需要在不用这个对象时赋值为null,因为强引用不会被垃圾回收。

strongReference = null;

应用场景:在ArrayList集合类中定义elementData数组,在调用clear()方法清空集合元素时,将每个数组元素被赋值为null。目的是为了将内存数组中存放的引用类型进行内存释放,可以及时释放内存。不选择将elementData=null,是为了避免在后续调用add()等方法添加新元素时,需要进行内存的重新分配

//ArrayList 源码
public void clear() {
    modCount++;

    // clear to let GC do its work
    for (int i = 0; i < size; i++)
        elementData[i] = null;

    size = 0;
}
(2)弱化方式2

让对象超出作用域范围,即缩小强引用的作用范围

public void test() {
    Object strongReference = new Object();
    // 省略其他操作
}

应用场景:在一个方法的内部有一个强引用,这个引用保存在VM Stack中(GC Root*),而真正的引用对象(Object)保存在中。当这个方法运行完成后,就会退出方法栈,则这个对象会被回收。

2、软引用(Soft Reference)

  • 如果一个对象只具有软引用,则 内存空间充足 时,垃圾回收器就不会回收它
  • 如果 内存空间不足时 ,就会回收这些对象,从而释放内存
  • 软引用可以用来实现内存敏感的高速缓存

使用 SoftReference 创建软引用

// 定义和访问软引用
// 强引用
String strongReference = new String("abc");

// 软引用
String str = new String("abc");
SoftReference<String> softReference = new SoftReference<String>(str);

// 访问软引用
softReference.get();
  • 软引用对象是在jvm内存不够的时候才会被回收,我们调用System.gc()方法只是起通知作用,最终何时回收,由JVM决定。
  • 所以,当内存不足时,JVM首先将软引用中的对象引用置为null,然后通知垃圾回收器进行回收:
//回收软引用
// 软引用
String str = new String("abc");
SoftReference<String> softReference = new SoftReference<>(str);

str = null;

// Notify GC
System.gc();

try {
    byte[] buff1 = new byte[900000000]; // 内存充沛
    // byte[] buff2 = new byte[900000000]; // 内存不足
} catch (Error e) {
    e.printStackTrace();
}

System.out.println(softReference.get()); // abc 或 null

**应用场景:**短视频APP中的视频缓存,后退时,显示的短视频内容是重新进行请求还是从缓存中取出呢?

  1. 如果一个短视频在播放结束时,就进行内容的回收,则后退查看前面播放的短视频时,需要重新请求。
  2. 如果将播放过的短视频存储到内存中,会造成内存的开销,甚至会造成内存溢出。

此时,可以使用软引用解决这个实际问题:

//软引用案例
// 获取视频播放器对象
Player videoAlayer = new Player();

// 加载短视频
Video video = audioAlayer.getVideo();

// 将播放完毕的短视频设置为软引用
SoftReference softReference = new SoftReference(video);

// 回退或者再次播放时
if(softReference.get() != null) {
    // 内存充足,还没有被回收器回收,直接获取缓存
    video = softReference.get();
} else {
    // 内存不足,软引用的对象已经回收
    video = audioAlayer.getVideo();
    // 重新构建软引用
    softReference = new SoftReference(video);
}

3、弱引用(Weak Reference)

  • 只具有 弱引用 的对象拥有 更短暂生命周期
  • 在垃圾回收器扫描的时候,不管内存是否足够,都会回收它的内存

创建弱引用,使用 WeakReference

String str = new String("abc");
WeakReference<String> weakReference = new WeakReference<>(str);
str = null;

System.gc();
		
// 一旦发生GC,弱引用一定会被回收
System.out.println(weakReference.get());

4、虚引用(Phantom Reference)

  • 虚引用是最弱的一种引用关系
  • 如果一个对象仅持有虚引用,完全不会对其生存时间构成影响,它就和没有任何引用一样,随时可能会被回收
  • 主要用来跟踪对象被垃圾回收的活动,可以在垃圾收集时收到一个系统通知
public class PhantomReference<T> extends Reference<T> {
    public T get() {
        return null;
    }
}

二、垃圾收集算法

1、分代收集理论

目前主流JVM虚拟机中的垃圾收集器,都遵循分代收集理论

  • 弱分代:绝大多数对象都是朝生夕灭
  • 强分代:经历越多次垃圾收集过程的对象,越难以回收,难以消亡

按照分代收集理论设计的“分代垃圾收集器”,所采用的设计原则:收集器应该将Java堆划分成不同的区域,然后将回收对象依据其年龄(年龄即对象经历过垃圾收集过程的次数)分配到不同的区域存储

(1)分代存储
  • 如果一个区域中大多数对象都是朝生夕灭(新生代),难以熬过垃圾收集过程的话,把它们集中存储在一起,每次回收时,只关注如何保留少量存活对象,而不是去标记大量将要回收的对象,就能以较低代价回收到大量的空间。
  • 如果一个区域中大多数对象都是难以回收(老年代),那么把它们集中放在一起,JVM虚拟机就可以使用较低的频率,来对这个区域进行回收。
  • 这样设计的好处是,兼顾垃圾收集的时间开销和内存空间的有效利用。
(2)分代收集

堆区按照分代存储的好处:

Java堆区划分成不同区域后,垃圾收集器才可以每次只回收其中某一个或者某些区域,所以才有MinorGCMajorGCFullGC等垃圾收集类型划分。

Java堆区划分成不同区域后,垃圾收集器才可以针对不同的区域,安排与该区域存储对象存亡特征相匹配的垃圾收集算法:标记-复制算法标记-清除算法标记-整理算法

垃圾收集类型划分:

  • 部分收集(Partial GC):没有完整收集整个Java堆的垃圾收集,其中又分为:

    • 新生代收集(Minor GC / Young GC
    • 老年代收集(Major GC / Old GC
    • 混合收集(Mixed GC):收集整个新生代和部分老年代的垃圾收集。
  • 整堆收集(Full GC):收集整个Java堆的垃圾收集

JVM 虚拟机 ---->垃圾收集算法_第3张图片

2、垃圾收集算法

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

最基础的收集算法

算法实现思路
  1. 该算法分为“标记”和“清除”阶段
  2. 首先标记出所有不需要(或需要)回收的对象
  3. 标记完成后,清除所有没有(或有)被标记的对象
存在的问题
  1. 执行效率不稳定

    如果执行垃圾收集的区域,大部分对象是需要被回收的,则需要大量的标记和清除动作,导致效率变低

  2. 内存空间碎片化

    标记清除后会产生大量的不连续的内存空间碎片,会导致分配较大对象时,无法找到祖足够的连续空间,从而会触发新的垃圾收集动作

JVM 虚拟机 ---->垃圾收集算法_第4张图片

(2)标记-复制算法(Copying)
算法实现思路

简称 “复制算法” ,为了解决“标记-清除”面对大量可回收对象时执行效率低下的问题

  1. 将内存分为两个大小相同的空间,每次使用其中的一块
  2. 当其中一块内存使用完成后,就将还存活的对象复制到另一块,然后清空当前已经使用的空间
算法特点

当内存中大多数对象都是可回收的情况,推荐使用此算法

存在的问题
  1. 对象存活率较高,需要进行较多的内存间复制,效率降低
  2. 浪费过多的内存,使现有的可用空间变为原来的一半

JVM 虚拟机 ---->垃圾收集算法_第5张图片

3、标记-整理算法(Mark-Compact)
算法实现思路

标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向内存空间一端移动, 然后直接清理边界以外的内存,这样清理的机制,不会像标记-清除那样留下大量的内存碎片。

JVM 虚拟机 ---->垃圾收集算法_第6张图片

4、总结

当前虚拟机的垃圾收集都基于分代收集思想,根据对象存活周期的不同,将内存分为几个不同的区域,在不同的区域选择使用合适的垃圾收集算法

  • 在新生代中,每次收集都会有大量垃圾对象被回收,所以可以选择“标记-复制”算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集
  • 在老年代中,对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以选择“标记-清除”或“标记-整理”算法进行垃圾收集

你可能感兴趣的:(jvm,算法)