首页 > 其他分享 >JVM学习笔记——垃圾回收篇

JVM学习笔记——垃圾回收篇

时间:2022-11-04 07:44:28浏览次数:93  
标签:笔记 对象 内存 回收 引用 JVM 我们 垃圾

JVM学习笔记——垃圾回收篇

在本系列内容中我们会对JVM做一个系统的学习,本片将会介绍JVM的垃圾回收部分

我们会分为以下几部分进行介绍:

  • 判断垃圾回收对象
  • 垃圾回收算法
  • 分代垃圾回收
  • 垃圾回收器
  • 垃圾回收调优

判断垃圾回收对象

本小节将会介绍如何判断垃圾回收对象

引用计数法

首先我们先来介绍引用计数法的定义:

  • 我们为对象附上一个当前使用量
  • 当有线程使用时,我们将该值加一;当线程停止使用时,我们将该值减一
  • 当当前使用量大于零时,我们创建该对象;当当前使用量减少为零时,我们将该对象当作垃圾回收对象

但该方法存在一个致命问题:

  • 当两个对象互相调用对方时,就会导致当前使用量一直不为空,占用内存

可达性分析算法

同样我们先来简单介绍可达性分析算法:

  • 我们首先判定一些对象为Root对象
  • 我们根据这些对象来选择判定其他对象是否为垃圾回收对象
  • 当该对象直接或间接被Root对象所引用时,我们不设置为垃圾回收对象;当没有被Root对象连接时,设置为垃圾回收对象

然后我们来简单介绍一下Root对象的分类(来自MAT工具统计):

  • System Class:直属于Java包下的相关类,包括有Object,String,Stream,Buffer等
  • Native Stack:直属于操作系统交互的类,包括有wait等
  • Busy Monitor:读锁机制相关的类,当前状态下被锁定的对象是无法当作垃圾回收对象的
  • Thread:互动线程,线程相关的类也无法当作垃圾回收对象

可达性分析算法就是目前Java虚拟机所使用的垃圾回收器判定方法

五种引用

下面我们将会介绍JVM中常用的五种引用方法,他们分别对应着不同的回收对象判定情况:

我们下面来一一介绍

强引用

上述图片中的A1对象就是强引用示例

我们下面介绍强引用的概念:

  • 强引用就是由Root对象直接引用的对象

然后我们介绍强引用的回收概念:

  • 只有当所有强引用连接都消失时,该对象才会被列为垃圾回收对象
  • 例如上图,A1对象由B,C两个对象所强引用连接,只有当两个对象都取消引用后,A1对象才会被列入回收对象

软引用

上述图片中的A2对象就是软引用示例

我们下面介绍软引用的概念:

  • 软引用不是由根Root直接引用,而是采用一个软引用对象SoftReference连接

然后我们介绍软引用的回收概念:

  • 当该对象没有被强引用连接,被软引用连接时有可能会被回收
  • 每次发生垃圾回收,如果垃圾回收后的内存够用,则不进行软引用对象的垃圾回收;若内存不足,则进行软引用对象的垃圾回收

此外我们的软引用对象也是会占用内存的,所以我们也需要采用其他方法将软引用对象回收:

  • 我们通常将软引用对象绑定一个引用队列
  • 当该软引用对象不再连接任何对象时,将其放入引用队列,引用队列会进行检测,检测到软引用对象就会对其进行垃圾回收

我们首先给出软引用对象的相关测试代码:

package cn.itcast.jvm.t2;

import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;

/**
 * 演示软引用
 * -Xmx20m -XX:+PrintGCDetails -verbose:gc
 */
public class Demo2_3 {

    private static final int _4MB = 4 * 1024 * 1024;



    public static void main(String[] args) throws IOException {
        
        // 这部分是强引用对象,我们会发现所有内存都放在内部,导致内存不足
        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++) {
            // 首先new一个SoftReference并赋值
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
            System.out.println(ref.get());
            // 将SoftReference加入list
            list.add(ref);
            System.out.println(list.size());

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

/*
调试过程:
如果我们采用强引用方法,正常情况下会在第五次循环时报错
但是如果我们采用软引用,我们会在第五次循环时发生gc清理,这时我们前四次的添加(list的前四位)就会被软引用清除
所以我们在最后循环结束后查看数组会发现:
null
null
null
null
[B@330bedb4
*/

我们再给出软引用对象回收的相关测试代码:

package cn.itcast.jvm.t2;

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;

/**
 * 演示软引用, 配合引用队列
 */
public class Demo2_4 {
    private static final int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) {
        // 这里设置了List,里面的SoftReference是软引用对象,再在里面添加的数据就是软引用对象所引用的A2对象
        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());
        }

    }
}

/*
和之前那此调试相同,前四次正常运行,在第五次时进行了gc清理
但是在循环结束之后,我们将软引用对象放入到了引用队列中并进行了清理,所以这时我们的list中前四次软引用对象直接消失
我们只能看到list中只有一个对象:
[B@330bedb4
*/

弱引用

上述图片中的A3对象就是弱引用示例

我们下面介绍强弱引用的概念:

  • 弱引用不是由根Root直接引用,而是采用一个弱引用对象WeakReference连接

然后我们介绍弱引用的回收概念:

  • 当该对象没有被强引用连接,被弱引用连接时在进行Full gc时会被强制回收
  • 每次进行老年代的Full gc(后面会讲到Full gc,这里就当作大型垃圾回收)时都会被强制回收

此外我们的弱引用对象也是会占用内存的,所以我们也需要采用相同方法将弱引用对象回收:

  • 我们通常将弱引用对象绑定一个引用队列
  • 当该弱引用对象不再连接任何对象时,将其放入引用队列,引用队列会进行检测,检测到弱引用对象就会对其进行垃圾回收

我们同样给出弱引用对象的垃圾回收示例代码:

package cn.itcast.jvm.t2;

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;

/**
 * 演示弱引用
 * -Xmx20m -XX:+PrintGCDetails -verbose:gc
 */
public class Demo2_5 {
    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 < 10; 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());
    }
}

/*
这时我们的小型gc(新生代gc)是不会触发弱引用全部删除的(新生代我们后面会讲到)
只有当内存全部占满后,触发的Full gc才会导致弱引用的必定回收
例如我们在第5,7次新生代发生内存占满,这时触发了新生代的gc,但是只会删除部分WeakReference
当我们第9次新生代,老生代内存全部占满后会发生一次Full gc,这时就会引起全部弱引用数据删除,所以我们的数据会变成:
null
null
null
null
null
null
null
null
null
[B@330bedb4
*/

虚引用

上述图片中的ByteBuffer对象就是虚引用示例

我们下面介绍虚引用的概念:

  • 虚引用实际上就是直接内存的引用,我们内存结构篇所学习的ByteBuffer就是例子
  • 系统首先会创建一个虚引用,然后这个虚引用会创建一个ByteBuffer对象,ByteBuffer对象通过unsafe来管理直接内存
  • 此外,我们的虚引用必定需要绑定一个引用队列,因为我们的byteBuffer对象是无法控制直接内存的,我们需要检测虚引用来删除

然后我们介绍虚引用的回收概念:

  • 首先我们会手动删除或者系统垃圾回收掉ByteBuffer对象
  • 这时我们的虚引用和直接内存是不会消失的,但是我们的虚引用会被带到引用队列中
  • 虚引用中携带者Cleaner对象,引用队列会一直检测是否有Cleaner对象进入,当检测到时会执行这个Cleaner方法来删除直接内存

我们需要注意的是:

  • 引用队列中检测Cleaner对象的优先级较高,所以效率相关而言比较快

终结器引用

上述图片的A4对象就是终结器引用

我们下面介绍终结器引用的概念:

  • 终结器引用实际上是对象自己定义的finallize方法
  • 终结器对象同样也需要绑定引用队列,因为他需要靠终结器对象来清除内部对象

然后我们介绍终结器引用的回收概念:

  • 如果我们希望清除终结器引用的对象,那么我们需要先将终结器引用对象导入到引用队列中
  • 引用队列中同样也会一直检测是否出现终结器对象,若出现终结器对象,那么针对该终结器对象调用其内部对象的finallize方法删除

我们需要注意的是:

  • 引用队列中检测终结器对象的优先级较低,所以效率相关而言比较慢

垃圾回收算法

本小节将会介绍垃圾回收的三种基本回收算法

标记清除法

我们首先给出简单图示:

我们来做简单解释:

  • 首先我们找出需要进行垃圾回收的部分并进行标记
  • 然后我们将该标记地址部分清除即可(注意:这里的清除仅仅是记录起始地址和终止地址,然后在其他内存占用时再次覆盖)

该算法的优缺点:

  • 执行速度极快
  • 但会产生内存碎片,当内存碎片逐渐增多会导致问题

标记整理法

我们首先给出简单图示:

我们来做简单解释:

  • 首先我们根据Root标记出需要垃圾回收的部分
  • 然后我们将垃圾回收的部分抛出之后,将后面的部分进行地址腾挪,使其紧凑

该算法的优缺点:

  • 不会产生内存碎片,导致内存问题
  • 速度较慢,同时整理过程中其他进程全部停止(因为会涉及内存地址重塑,进行其他进程可能会导致内存放置地址错误)

区域复制法

我们首先给出简单图示:

我们来做简单解释:

  • 我们准备两块完全相同的区间,将他们分为From和To区间
  • 我们首先在from区间存储数据,我们直接进行垃圾回收判定
  • 然后将需要保存的数据直接放入To区间,垃圾回收的部分不需要管理
  • 最后我们将From和To区间的定义交换,将新添加的数据放入现在的From区间(之前腾挪的To区间)

该算法的优缺点:

  • 不会产生内存碎片,相对而言比较迅速
  • 但需要占用两块相同的地址空间,导致占用空间较多

分代垃圾回收机制

本小节将会介绍垃圾回收的常用机制

分代垃圾回收机制介绍

我们前面已经介绍了三种垃圾回收算法,但实际上我们的垃圾回收采用的是三种方法的组合方法:

我们首先对大概念进行介绍:

  • 新生代:用于存放新产生的内存数据,清除频繁
  • 老生代:用于存放一直使用的内存数据,只有当内存占满时才会清理

然后我们对小概念进行介绍:

  • 伊甸园:用于存放所有的新产生的内存数据
  • 幸存区From:用于存放未被垃圾回收的数据
  • 幸存区To:用于进行未被垃圾回收的数据的复制方法
  • 幸存值:用于表示内存数据的常用程度,所有内存数据进入时默认值为0,

然后我们对整个回收机制进行介绍:

  • 首先我们的新数据都会进入到新生代的伊甸园中去,默认幸存值为0
  • 当伊甸园数据满后,会进行gc,这时我们进行标记清除法,将不需要的内存筛出
  • 同时将幸存下来的内存数据放入到幸存区From,幸存值+1,同时进行From和To区间的对调
  • 我们继续进行储存直到伊甸园再次占满,对整个新生代进行gc
  • 首先将幸存区From的幸存内存放入To中并将伊甸园的幸存数据放入To,进行区间调换,幸存值+1
  • 直到幸存值达到一个阈值(默认为6或者15),该内存数据就会被移动到老年代,新生代仍旧继续工作
  • 直至新生代和老年代全部都占满后,这时我们就需要进行大型的垃圾回收,也就是我们之前提到的Full gc!

分代垃圾回收相关VM参数

我们下面介绍一下分代垃圾回收机制的相关参数:

含义 参数
堆初始大小 -Xms
堆最大大小 -Xmx 或 -XX:MaxHeapSize=size
新生代大小 -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size )
幸存区比例(动态) -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
幸存区比例 -XX:SurvivorRatio=ratio
晋升阈值 -XX:MaxTenuringThreshold=threshold
晋升详情 -XX:+PrintTenuringDistribution
GC详情 -XX:+PrintGCDetails -verbose:gc
FullGC 前 MinorGC (小gc) -XX:+ScavengeBeforeFullGC

分代垃圾回收案例展示

我们通过一个简单的实例来展示分代垃圾回收的实际演示:

// 相关配置信息:配置默认大小,设置回收方法,显示GC详情,开启FullGC前进行gc
// -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC

/*
首先我们展示不添加内存的状况
*/

package cn.itcast.jvm.t2;

import java.util.ArrayList;

public class Demo2_1 {
    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;

    
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            ArrayList<byte[]> list = new ArrayList<>();
        }).start();

        System.out.println("sleep....");
        Thread.sleep(1000L);
    }
}

/*
其中def new generation,eden space是新生代,tenured generation是老年代,from,to幸存区
Heap
 def new generation   total 9216K, used 4510K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  55% used [0x00000000fec00000, 0x00000000ff067aa0, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,   0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
 Metaspace       used 4362K, capacity 4714K, committed 4992K, reserved 1056768K
  class space    used 480K, capacity 533K, committed 640K, reserved 1048576K
*/


/*
然后我们展示添加1mb的情况
*/

package cn.itcast.jvm.t2;

import java.util.ArrayList;

public class Demo2_1 {
    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;

    
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            ArrayList<byte[]> list = new ArrayList<>();
            list.add(new byte[_1MB]);
        }).start();

        System.out.println("sleep....");
        Thread.sleep(1000L);
    }
}

/*
我们可以发现新生代数据增加,老年代未发生变化
Heap
 def new generation   total 9216K, used 5534K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  67% used [0x00000000fec00000, 0x00000000ff167a40, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,   0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
 Metaspace       used 4354K, capacity 4714K, committed 4992K, reserved 1056768K
  class space    used 480K, capacity 533K, committed 640K, reserved 1048576
*/

/*
最后需要补充讲解一点:当我们的新生代不足以装载数据内存时,我们会直接将其装入老年代(老年代能够装载情况下)
*/

package cn.itcast.jvm.t2;

import java.util.ArrayList;

public class Demo2_1 {
    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;

    
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            ArrayList<byte[]> list = new ArrayList<>();
            list.add(new byte[_8MB]);
        }).start();

        System.out.println("sleep....");
        Thread.sleep(1000L);
    }
}

/*
我们会发现eden的值未发生变化,但是tenured generation里面装载了8192K
Heap
 def new generation   total 9216K, used 4510K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  55% used [0x00000000fec00000, 0x00000000ff067a30, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 8192K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  80% used [0x00000000ff600000, 0x00000000ffe00010, 0x00000000ffe00200, 0x0000000100000000)
 Metaspace       used 4360K, capacity 4714K, committed 4992K, reserved 1056768K
  class space    used 480K, capacity 533K, committed 640K, reserved 1048576K
*/

/*
当然,当我们的新生代和老年代都不足以装载时,系统报错~
*/


package cn.itcast.jvm.t2;

import java.util.ArrayList;

public class Demo2_1 {
    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;

    
    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);
    }
}

/*
我们首先会看到他在Full gc之前做了一次小gc,然后做了一次Full gc,可是这并无法解决问题

[GC (Allocation Failure) [DefNew: 4345K->999K(9216K), 0.0016573 secs][Tenured: 8192K->9189K(10240K), 0.0022899 secs] 12537K->9189K(19456K), [Metaspace: 4352K->4352K(1056768K)], 0.0039931 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [Tenured: 9189K->9124K(10240K), 0.0018331 secs] 9189K->9124K(19456K), [Metaspace: 4352K->4352K(1056768K)], 0.0018528 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

然后系统进行报错
Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
	at cn.itcast.jvm.t2.Demo2_1.lambda$main$0(Demo2_1.java:20)
	at cn.itcast.jvm.t2.Demo2_1$$Lambda$1/1023892928.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:750)
	
最后我们可以看到老年代占用了89%,第一个数据仍旧保存,但第二个数据无法保存导致报错	
Heap
 def new generation   total 9216K, used 366K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,   4% used [0x00000000fec00000, 0x00000000fec5baa8, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 tenured generation   total 10240K, used 9124K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  89% used [0x00000000ff600000, 0x00000000ffee93c0, 0x00000000ffee9400, 0x0000000100000000)
 Metaspace       used 4379K, capacity 4704K, committed 4992K, reserved 1056768K
  class space    used 480K, capacity 528K, committed 640K, reserved 1048576K
  
我们还需要注意的是:即使内存不足发生报错,但该程序不会结束;系统只会释放自己当前项目的进程而不会影响其他进程
*/

垃圾回收器

前面我们已经介绍了垃圾回收机制,现在我们来介绍常用的垃圾回收器

STW概念

我们在正式讲解垃圾回收器之前,我们先来回顾一个概念STW:

  • STW即Stop The World,意思是暂停所有进程处理
  • 因为我们在进行垃圾处理时,会涉及到地址空间的整合(标记整理法),这时所有CPU都需要停止操作

串行垃圾回收器

我们首先来介绍串行垃圾回收器的特点:

  • 单线程
  • 适用于堆内存较小,适合单人电脑

我们给出串行垃圾回收器的展示图:

我们所需配置:

// 设置 新生代回收方法复制 老年代回收方法为标记整理法
-XX:+UseSerialGC = Serial + SerialOld

我们来简单解释一下:

  • 串行操作属于单核CPU处理
  • 我们在处理该CPU的垃圾回收时,只有该线程的CPU进行操作
  • 但同时老年代采用标记整理法会涉及到内存地址重新规划,所以其他CPU也需要暂停操作,即STW

吞吐量优先垃圾回收器

我们首先来介绍吞吐量优先垃圾回收器的特点:

  • 多线程
  • 适用于堆内存较大,需要多核CPU
  • 让单位时间内,STW时间最短,例如每次STW0.2秒,但执行两次,共用0.4s(总时间最短)

我们给出吞吐量优先垃圾回收器的展示图:

我们所需配置:

// 设置垃圾回收器方法 
XX:+UseParallelGC ~ -XX:+UseParallelOldGC
    
// 自适应新生代晋升老年代的阈值处理
-XX:+UseAdaptiveSizePolicy
    
// 设置垃圾回收时间占总时间的比例(与-XX:MaxGCPauseMillis=ms冲突)
-XX:GCTimeRatio=ratio
    
// 设置最大STW时间(与-XX:GCTimeRatio=ratio冲突)
-XX:MaxGCPauseMillis=ms

// 设置最大同时进行CPU个数
-XX:ParallelGCThreads=n

我们来简单解释一下:

  • 吞吐量优先垃圾回收器是多核CPU处理回收器
  • 当一个进程发生垃圾回收时,我们会将所有CPU都用于垃圾回收,这时CPU利用率为100%

响应时间优先垃圾回收器

我们首先来介绍响应时间优先垃圾回收器的特点:

  • 多线程

  • 适用于堆内存较大,需要多核CPU

  • 让单次STW时间最短,例如每次STW0.1秒,但执行五次,共用0.5s(单次时间最短)

我们给出响应时间优先垃圾回收器的展示图:

我们所需配置:

// +UseConcMarkSweepGC:设置并发标记清除算法,允许用户进程单独进行,但部分时间还需要阻塞
// -XX:+UseParNewGC:设置新生代算法,
// SerialOld:当老年代并发失败,采用单线程方法
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
    
// -XX:ParallelGCThreads=n:并行数设置为n
// -XX:ConcGCThreads=threads:并发线程最好设置为CPU的1/4个数,相当于只有1/4个CPU在处理垃圾回收
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
    
// 预留空间(因为并发清理时其他进程可能会产生一些垃圾,这些垃圾目前无法处理,我们需要预留一定空间进行储存)
-XX:CMSInitiatingOccupancyFraction=percent
    
// 我们在重新标记阶段前,先对新生代进行垃圾回收,节省其标记量
-XX:+CMSScavengeBeforeRemark

我们来简单解释一下:

  • 响应时间优先垃圾回收器是多核CPU处理回收器
  • 首先我们的CPU1进行初始标记,其他进程阻塞,仅标记一些Root对象(时间短)
  • 然后我们CPU1进行并发标记,其他进程继续运行,这时用来标记所有的垃圾回收对象(时间长)
  • 然后由于我们的并发标记可能会导致一些内存混乱,所以我们将所有CPU需要进行重新标记(时间短)
  • 最后只需要对CPU1进行并发清理即可,其他进程继续运行

G1垃圾回收器

下面我们将会针对jdk1.9默认垃圾回收器做一个详细的介绍

G1垃圾回收器简介

首先我们先来简单介绍一下G1垃圾回收器:

  • G1回收器:Garbage First
  • 在2017成为JDK9的默认垃圾回收器

下面我们来介绍G1垃圾回收器的特点:

  • 同时注重吞吐量和低延迟,默认的暂停目标是200ms
  • 超大堆内存,将堆划分为多个大小相等的Region
  • 整体上是标记整理法,但两个区域之间是复制算法

相关JVM参数:

  • -XX:+UseG1GC 使用G1垃圾回收器(JDK9之前都不是默认回收器)
  • -XX:G1HeapRegionSize=size 设置Region的大小
  • -XX:MaxGCPauseMillis=time 设置最大的G1垃圾回收时间

G1垃圾回收器阶段简介

我们通过一张图来简单介绍G1垃圾回收器的过程:

我们可以看到整个流程分为三个阶段:

  • YoungCollection:新生代阶段
  • YoungCollection+ConcurrentMark:新生代阶段+并发标记阶段
  • MixedCollection:混合收集阶段

Young Collection

我们首先给出该阶段的展示图:

我们对其进行解释:

  • E就是伊甸园,S就是幸存区,O就是老年代
  • 其产生的正常流程就和分代垃圾回收机制一样,但这阶段不会产生GC

Young Collection + CM

我们首先给出该阶段的展示图:

我们对其进行解释:

  • 其字符含义完全相同
  • 当新生代内存占满后进行Young GC时会同时进行GC Root的初始标记
  • 老年代占用堆空间比例达到阈值时,进行并发标记(不会产生STW),阈值可以控制

我们给出并发标记阈值控制语句:

// 阈值控制
-XX:InitiatingHeapOccupancyPercent=percent (默认45%)

Mixed Collection

我们首先给出该阶段的展示图:

我们对其进行解释:

  • 其字符含义完全相同
  • 但该阶段会对E,S,O进行全面垃圾回收
  • 其中最终标记(Remark)和拷贝存活(Evacation)都会STW(我们均会在后面解释)

我们需要注意一点:

  • Mixed Collection可能并不会将所有老年代的数据都删除
  • 它会根据你设置的最大暂停时间来进行抉择,如果时间不足以删除所有老年代数据,就会挑选部分较大的内存数据进行回收

Full GC

我们需要重新总结一下Full GC操作:

  1. SerialGC(串行垃圾回收)
  • 新生代内存不足时发生的垃圾收集 - minor gc
  • 老年代内存不足时发生的垃圾收集 - full gc
  1. ParalllelGC(吞吐量优先垃圾回收)
  • 新生代内存不足时发生的垃圾收集 - minor gc
  • 老年代内存不足时发生的垃圾收集 - full gc
  1. CMS(响应时间优先垃圾回收)
  • 新生代内存不足时发生的垃圾收集 - minor gc
  • 老年代内存不足时优先进行标记操作同步垃圾回收,当内存完全占满后才采用full gc
  1. G1(Garbage First)
  • 新生代内存不足时发生的垃圾收集 - minor gc
  • 老年代内存不足时优先进行MixedCollection同步垃圾回收,当内存完全占满后才采用full gc

G1知识点补充

我们在前面已经提到了我们将堆划分为多个Region

但其实这个Region并不仅仅只分为了E,S,O三个空间,此外还包括以下空间:

  1. RSet(Remember Set :记忆集合)
/*
每一个Region都会划出一部分内存用来储存记录其他Region对当前持有Rset Region中Card的引用
针对G1的垃圾回收时间设置较短,在进行标记过程中可能会导致时间过长,所以我们设置了RSet来储存部分信息
我们可以直接通过扫描每块Region里面的RSet来分析垃圾比例最高的Region区,放入CSet中,进行回收。
*/
  1. CSet(Collection Set 回收集合)
/*
收集集合代表每次GC暂停时回收的一系列目标分区。
在任意一次收集暂停中,CSet所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。
年轻代收集CSet只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到CSet中。
*/

新生代跨代引用

由于我们的初次标记时会去寻找Root部分

但其实大部分的Root都放入了老年代,但老年底数据较多难以查找,所以G1提供了一种方法:

  • 将老年代O再次划分为多个区间,名为卡
  • 如果该卡中存储了Root部分,那么就将该卡标记为脏卡,同时放于RSet中存储起来便于查找

我们给出简单图示:

同时如果该Root地址发生变化,G1给出了另外的方法进行更换:

  • 在引用变更时通过post-write barrier + dirty card queue
  • concurrent refinement threads 更新 Remembered Set

Remark重新标记

我们在进行标记时通常采用三色标记法:

我们做简单介绍:

  • 黑色是已经标记结束的内存
  • 灰色是正在标记的内存
  • 白色是未标记的内存

我们针对上述进行分析:

  • 最左侧是根,黑色,已经被标记
  • 上方为根的直接/间接引用对象,黑色,已经被标记
  • 下方为根的直接/间接引用对象,灰色,正在标记,白色,还未被标记当后面会被标记
  • 最右侧孤零零的白色方块,没有被引用,不会被标记,最后会被当作垃圾回收对象处理掉

这时我们就会发现一个问题:

  • 如果最右侧的方块在针对自身的CPU的并发标记结束后,又被其他进程所调用了(并发标记其他CPU正常运行)
  • 但是此时它是白色的,最终会被这次的垃圾回收操作清除掉,就会导致影响其他进程操作

所以我们设计了Remark重新标记操作:

  • 如果在该方块针对自身的并发标记结束后又被其他进程调用,这时将他拖入一个队列中,并将其变为灰色
  • 在并发标记结束后进入重新标记阶段,就会检查该队列,若发现灰色对象,在队列中将它变为黑色对象并排出队列

G1垃圾回收器重要更新

下面我们将会针对G1垃圾回收器在各个版本的重要更新做个介绍

JDK 8u20 字符串去重

我们首先要明白字符串在底层是采用char数组形成的:

String s1 = new String("hello"); // char[]{'h','e','l','l','o'}
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}

如果重复的字符串都存放在内存中肯定会导致内存多余占用,所以提供了解决方案:

  • 将所有新分配的字符串放入一个队列
  • 当新生代回收时,G1并发检查是否有字符串重复
  • 如果它们值一样,让它们引用同一个 char[]
  • 注意与 String.intern() 不一样:一个底层针对String类型,一个底层针对char[]类型

其优缺点:

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

JDK 8u40 并发标记类卸载

当所有的类都经过并发标记后,就会直到哪些类不再被使用

这时如果一个类加载器的所有类都不再使用时,我们就可以卸载它所加载的所有类

JDK 8u60 回收巨型对象

首先我们介绍一下巨型对象的定义:

  • 一个对象大于 region 的一半时,称之为巨型对象

    然后我们再来介绍G1对巨型对象的处理方法:

  • 回收时被优先考虑

  • G1 不会对巨型对象进行拷贝

  • G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为0 的巨型对象就可以在新生代垃圾回收时处理掉

垃圾回收调优

本小节将会介绍垃圾回收的调优机制

基本调优概念

我们进行调优需要掌握的基本知识:

  • 掌握相关工具使用
  • 掌握基本的空间调整
  • 掌握GC相关的VM参数
  • 明白调优并非固定公式,而是需要结合应用,环境

我们调优的领域并非只有垃圾回收,但是这个部分的调优确实会给项目带来很大的速率优化,此外还有其他方法的调优:

  • IO调优
  • 锁竞争调优
  • CPU占用调优

此外我们需要确定调优的目标:

  • 是为了保证低延迟还是为了保证高吞吐量

最快的GC是不发生GC

首先我们需要明白GC是花费时间的,如果我们能够控制好内存保证不发生GC,那么才是最快的

如果我们频繁发生GC操作,那么我们就需要先进行自我反思:

  1. 存放的数据是否过多?
/*
例如我们是否设置了相同元素筛选?错误账号禁止缓存?
*/
  1. 数据表示是否臃肿?
/*
例如我们调取数据时是否只调取了我们所需数据还是全盘托出?
例如我们选择数据类型时是否是以最低标准为要求,数据库能采用tiny不要使用int
*/
  1. 是否存在内存泄露?
/*
例如我们是否设置缓存数据时采用了Static形式的Map并不断存储数据?
*/

新生代调优

首先我们先来回顾一下新生代的优点:

  • 所有的new操作的内存分配十分廉价:直接new出来存放在伊甸区即可
  • 死亡对象的回收代价为零:我们直接采用复制将幸存内存复制出来即可,其他垃圾回收部分不用过问
  • 大部分对象都是垃圾:我们实际上幸存下来的内存数据是小部分数据,所以大部分都是垃圾
  • 垃圾回收时间相对短:Minor GC的时间远低于Full GC

那么我们该怎么进行调优呢:

  • 最简单的方法就是适当扩大新生代空间即可
  • 新生代不易太小:如果新生代过小就会导致不断发生minor GC浪费时间,且幸存区过小导致无用数据都存入老年代
  • 新生代不易太大:如果新生代过大相对老年代空间变小,容易发生Full GC,Full GC的运行时间过长

那么官方认可的新生代大小为多少:

  • 新生代能容纳所有[并发量*(请求-响应)]的数据

新生代幸存区调优

首先我们需要直到新生代幸存区存放的数据主要分为两部分:

  • 当前活跃对象:并不应该晋级到老年代,只是目前阶段需要使用的内存数据
  • 需要晋升对象:我们一直使用的内存数据,需要传递到老年代

首先我们需要保证具有一定的幸存区大小:

  • 如果幸存区过小,就会导致幸存区数据提前进入到老年代
  • 但如果是当前活跃对象进入到老年代,既不能发挥作用,并且也难以排出老年代

其次我们需要控制晋升标准:

  • 设置一定规格的晋升标准,防止部分当前活跃对象进入到老年代,理由同上

老年代调优

最后我们介绍一下老年代调优,我们这里以CMS为例:

  • 首先CMS老年代内存越大越好
  • 其次在不做调优的情况下,如果没有发生Full GC就不需要调优了,否则优先调优新生代
  • 如果经常发生Full GC,我们就需要将老年代空间增大了,官方推荐增大目前老年代空间大小的1/4~1/3即可

调优案例展示

最后我们介绍三个调优方法的案例:

  1. Full GC和Minor GC频繁
/*
主要是因为新生代空间不足
因为新生代空间不足,经常发生minor GC,同时幸存区空间不足导致大量数据直接进入到老年代,最后导致老年代也产生Full GC
*/
  1. 请求高峰期发生 Full GC,单次暂停时间特别长 (CMS)
/*
首先我们已经直到是CMS的垃圾回收方法
我们在之前的学习中得知Full GC主要分为三个阶段:初始标记,并发标记,重新标记
在请求高峰期期间,数据较多,我们的重新标记由于需要重新扫描所有数据空间,所以会导致单次暂停时间长
我们只需要保证在进行重新扫描前先进行一次Minor GC消除掉无用数据就可以加快暂停速度:-XX:+CMSScavengeBeforeRemark
*/
  1. 老年代充裕情况下,发生 Full GC (CMS jdk1.7)
/*
首先我们需要注意是jdk1.7版本
在1.7版本是由永久代负责管理方法区以及常量池,如果永久代内存满了也会产生Full GC
所以我们只需要增加永久代的内存大小即可
*/

结束语

到这里我们JVM的垃圾回收篇就结束了,希望能为你带来帮助~

附录

该文章属于学习内容,具体参考B站黑马程序员满老师的JVM完整教程

这里附上视频链接:01_垃圾回收概述_哔哩哔哩_bilibili

标签:笔记,对象,内存,回收,引用,JVM,我们,垃圾
From: https://www.cnblogs.com/qiuluoyuweiliang/p/16856467.html

相关文章

  • Flask学习笔记(十八)-Flask 信号机制
    一、说明官方文档-信号参考Flask框架中的信号基于blinker,其主要就是让开发者可是在flask请求过程中定制一些用户行为安装:pip3installblinker二、自定义信号自定义......
  • go gin笔记
       ​​https://liwenzhou.com/posts/Go/Gin_framework/​​    ......
  • 《金融学》笔记 第九章 货币需求、供给、均衡
    《金融学》笔记第九章货币需求、供给、均衡前言在《​​《金融学》笔记​​》中开了一个头,现在完善具体细节。金融范畴篇第一章货币的本质第二章货币制度第三章信用、......
  • 《金融学》笔记
    《金融学》笔记前言参考《​​《金融学原理》笔记​​》和《​​《金融学》摘要笔记​​》,写一下金融学的笔记。《​​《金融学》阶段性回顾笔记(上篇)​​》《​​《金融学》......
  • 20201318李兴昕第十二章学习笔记
    第十二章:块设备I/O和缓冲区管理知识点归纳总结:本章讨论了块设备I/O和缓冲区管理;解释了块设备I/O的原理和I/O缓冲的优点;论述了Unix的缓冲区管理算法,并指出了其不足之......
  • CMS垃圾收集器
    CMS是基于标记-清除算法的,收集的时候分为4个步骤:初始标记并发标记重新标记并发清除初始标记初始标记仅仅只是标记一下GCRoots能直接关联到的对象,所以速度很快。比......
  • sql笔记
    1.注释单行注释:"--"注释内容或"#"注释内容(MySQL特有)多行注释:/*注释*/1DDL操作数据库查询SHOWDATABASES;创建创建数据库CREATEDATABASE数据库......
  • Java学习笔记day3--二维数组
    packageday4_array;importjavax.swing.plaf.synth.SynthFormattedTextFieldUI;//一维数组的元素仍然是一维数组,则构成了二维数组publicclassArrayDemension2{......
  • 英语学习笔记部分汇总——wsdchong
    英语学习笔记部分汇总回顾一下以前做的笔记。​​英语单词记忆以及句式记忆​​​​四级考试技巧​​内容有三:英语四级准备、英语思维、范文英语思维是我听网易云课堂的课的......
  • 《前端技术架构与工程》之性能笔记
    《前端技术架构与工程》之性能前言:《前端技术架构与工程》这本书真的越看越有味。目前写了部分这本书的笔记,共分为三部分做笔记,已写了两篇如下。​​《前端技术架构与工程》......