JVM垃圾回收算法

1 什么时候回收垃圾?

1、什么场景下该使用什么垃圾回收策略?

在对内存要求苛刻的场景:想办法提高对象的回收效率,多回收掉一些对象,腾出更多内存。

在CPU使用率高的情况下:降低高并发时垃圾回收的频率,让CPU更多的去执行你的业务而不是垃圾回收。

2、垃圾回收发生在哪些区域?

堆(回收对象)、方法区(不用的常量、类)

3、对象什么时候被回收?

引用计数法:通过对象的引用计数器来判断对象是否被引用。无法处理循环引用问题。

A --> B --> C --> D --> B 此时B被引用了两次
当A不再引用B
A     B --> C --> D --> B BCD循环引用无法被回收。

可达性分析法:以根对象(GC Roots)为起点向下搜索,形成引用链。如果对象不在引用链上,便视作对象不可达,可以回收。

image

哪些对象可以作为GC Roots对象?

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

注意的是,一个对象不可达,也不一定会被回收。

image

public class GCTest {
    private static GCTest obj;

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize方法被调用了");
        obj = this; // 重新引用对象,从FQ队列中出队
    }

    public static void main(String[] args) throws InterruptedException {
        obj = new GCTest();
        obj = null;
        System.gc();

        TimeUnit.MILLISECONDS.sleep(1000);
        extracted();

        TimeUnit.MILLISECONDS.sleep(1000);
        obj = null; // finalize()方法已被调用过,对象会被直接回收
        System.gc();
        extracted();
    }

    private static void extracted() {
        if (obj == null) {
            System.out.println("obj == null");
        } else {
            System.out.println("obj可用");
        }
    }
}
finalize方法被调用了
obj可用
obj == null

finalize()的建议

  • 避免使用finalize()方法,操作不当可能会导致问题。上面的例子中,第二次回收时如果不设置obj==null,对象很难被回收掉。

  • finalize()优先级低,何时会被调用无法确定,因为什么时间发生GC不确定。

  • 建议使用try...catch...finally来替代finalize()。

4、四种引用

强引用,例如new对象,永远不会回收对象。

软引用,描述有用但是非必需对象,只有内存不足时被回收。

弱引用,描述非必需对象,无论内存情况都会被回收。

虚引用,主要用来跟踪对象被垃圾回收器回收的活动,结合引用队列使用。不影响对象的生命周期。

Object obj = new Object();
SoftReference<String> sr = new SoftReference<>("helllo");
WeakReference<String> sr = new WeakReference<>("helllo");
ReferenceQueue< String> queue = new ReferenceQueue<>();
PhantomReference< String> pr= new PhantomReference<>(“hello“queue)

2 垃圾回收算法

垃圾回收算法

基础垃圾回收算法

  • 标记清除法(Mark-Sweep):标记后直接删除。

    • 容易产生内存碎片;分配大内存时速度受到影响,需要找到适合它大小的空间。
  • 标记整理法(Mark-Compact):标记,内存整理到一侧,删除三步。

    • 整理存在开销,没有碎片。
  • 复制(Copy):内存分为两块,每次只使用其中一块;将存活对象复制到另一块内存,然后清空当前内存;交换使用另一块内存。

    • 内存利用率低,性能好,没有碎片。

综合垃圾回收算法

  • 分代回收算法:把内存分为多个区域,不同区域使用不同的回收算法。
  • 增量算法:每次只收集一小块内存区域的垃圾。

分代收集算法

商业虚拟机普遍采用分代收集算法,根据对象的存活周期,把内存分为多个区域,不同区域使用不同的回收算法。

分代的好处是更有效的清除不再需要的对象,提升垃圾回收的效率。

  • 新生代回收(Minor GC | Young GC)
  • 老年代回收(Major GC),通常伴随Full GC,Major GC≈ Full GC
  • 清理整个堆(Full GC)

典型的对象分配流程如下。
image

新生代采用的是复制算法,存放存活周期短的对象。老年代对象存活时间长,采用标记-清除或标记-整理算法。

堆的年轻代和老年代,内存比例1:2。年轻代分为Eden和Survivor,内存比例8:1:1。
1. 对象首先存放到Eden中,当Eden内存满后,垃圾收集线程执行minor gc,通过GC Root进行可达性分析,找到非垃圾对象,复制到Survivor S0区(对象头中分代年龄标记为1),清空Eden区域,Eden又可以存储新对象。
2. 当Eden再次满后,同时对Eden和S0执行minor gc,将非垃圾对象复制到S1(分代年龄加1),清空Eden和S0。S0和S1,同一时间只有一个被使用。
3. 当分代年龄达到15后,对象会被复制到老年代。
4. 当老年代满了,会对年轻代和老年代执行full GC。full GC不起作用,则会OOM。

命令行输入jvisualvm,打开Java VisualVM工具的Visual GC插件可以看到程序执行过程中堆容量变化。每次Eden满进行GC,调整堆区域。

image

非典型的情况:

  1. 新建的对象不一定分配到伊甸园。
  • 对象大于-XX:PretenureSizeThreshold,就会直接分配到老年代

  • 新生代空间不够。例如大数组。

  1. 对象也不一定要达到年龄才进入老年代

动态年龄:如果Survivor空间中所有相同年龄对象大小的总和大于Survivor空间的一半,那么年龄大于等于该年龄的对象就可以直接进入老年代。

触发垃圾回收的条件

Minor GC触发条件:伊甸园空间不足

Full GC触发条件:

  1. 老年代空间不足(老年代已满;空间碎片太多,没有连续内存分配对象)
  2. 元空间不足
  3. 要转成老年代对象所需的空间大于老年代剩余空间。
  4. 显示调用System.gc(),建议垃圾回收器执行垃圾回收。设置-XX:+DisableExplicitGC 参数忽略该操作。
分代收集调优思路
  • 让GC尽量发生在新生代,尽量减少Full GC的发生。
  • 合理设置Survior区域的大小,避免内存浪费。(复制算法)
posted @ 2021-12-28 23:58  Awecoder  阅读(371)  评论(0编辑  收藏  举报