JVM学习笔记 03、垃圾回收

举报
长路 发表于 2022/11/27 22:49:34 2022/11/27
【摘要】 文章目录前言一、如何判断对象可以回收1.1、引用计数法(python虚拟机)1.2、可达分析算法(JVM使用)1.2.1、认识根对象1.2.2、查看根对象的存在(借助Mat工具)1.3、四种引用1.4、实际应用场景1.4.1、软引用—垃圾回收软引用引用对象1.4.2、软引用—引用队列1.4.3、弱引用-WeakReference二、垃圾回收算法2.1、标注清除算法2.2、标注整理算法2.3、复制算

@[toc]

前言

本篇博客是跟随黑马程序员JVM完整教程,全网超高评价,全程干货不拖沓的学习JVM的笔记,若文章中出现相关问题,请指出!

所有博客文件目录索引:博客目录索引(持续更新)

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

1.1、引用计数法(python虚拟机)

只要一个对象被其他变量引用,就进行计数+1;如果被引用两次,就计数+2,若是某个变量不再引用它了就计数-1,一旦计数变为0,则表示没有人再引用它了,就可以作为垃圾进行回收。

出现弊端:循环引用问题,若是两个对象进行各自引用,那么此时两个对象计数都为1,此时就不能够进行垃圾回收,造成内存上的泄漏,之前python虚拟机就采用了引用计数的算法。

image-20211118095314384



1.2、可达分析算法(JVM使用)

1.2.1、认识根对象

Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象。

扫描堆中的对象,看是否能够沿着 GC Root对象 为起点的引用链找到该对象,找不到,表示可以回收。

哪些对象可以作为 GC Root ?

  • 首先需要确定一系列根对象。
  • 根对象:肯定不能被当成垃圾回收的对象。在垃圾回收之前,首先会对堆中的所有对象进行一遍扫描,然后判断每一个对象是否是被根对象所直接或间接引用,若是则不会被垃圾回收,反之则会。


1.2.2、查看根对象的存在(借助Mat工具)

案例描述:在程序中我们通过进行一次强引用以及取消引用来构建快照,并使用Mat工具来进行监测根对象的存在。

import java.io.*;
import java.util.ArrayList;
import java.util.List;


/**
 * 演示GC Roots
 */
public class Main {
    public static void main(String[] args) throws InterruptedException, IOException {
        //list1是一个变量,存储在活动栈帧里,相应的对象存储在堆中
        List<Object> list1 = new ArrayList<>();
        list1.add("a");
        list1.add("b");
        System.out.println(1);
        System.in.read();//阶段1:构建快照

        list1 = null;//取消强引用
        System.out.println(2);
        System.in.read();
        System.out.println("end...");//阶段2:构建快照
    }

}

分别在输出1、2时创建快照,我们可以借助jmap来进行创建

# 获取当前运行程序的进程号
jps 

# 创建两次快照
jmap -dump:format=b,live,file=1.bin 10556  # -dump表示将当前堆内存的状态抓取转储成一个文件,format=b表示二进制,live指的是抓快照时只抓取存活的对象,垃圾回收掉的会进行过滤,注意设置live参数会主动触发一次垃圾回收。

jmap -dump:format=b,live,file=2.bin 10556

此时我们就会在指定目录得到两个.bin文件,接着来使用Mat工具来进行分析:

image-20211118233548807

image-20211118233632430

image-20211118233653443

查看GC roots对象

image-20211118233746131

快照1:

image-20211118234007219

快照2:由于jmap设置了live参数,所以在生成快照前进行了GC垃圾回收,并且由于已经对该集合对象设置了null,则表示不再进行强引用,那么此时垃圾回收就会将其清理掉了,自然下面也就查看不到了!

image-20211118234115491



1.3、四种引用

严格意义上为五种。

强引用、软引用、弱引用、虚引用以及终结器引用。

image-20211118234329289

强引用:一般我们引用的对象就是强引用,通过引用链能够找到它就不会被垃圾回收,只有其他所有的引用都断开时才允许被垃圾回收。

软引用:若是一个对象没有被直接强引用引用,那么当垃圾回收发生时可能被回收掉。当内存不够时才会被回收掉

  • 若是对象A有一个软引用和一个强引用,不会被垃圾回收。
  • 若是对象A只有一个软引用(无强引用引用),满足一个条件(当进行了垃圾回收后内存依旧不够)才会将软引用引用的对象释放掉。

弱引用:与软引用类似,在无强引用情况下,只要进行垃圾回收就会将弱引用引用的对象回收掉。

软弱引用可以配合一个引用队列来进行工作,当软引用、弱引用的对象回收了之后,那么此时软、弱引用都会进入到队列中,因为这两个对象也会占用空间,若是想要对这两种对象处理就要借助该队列来进行(对队列进行遍历,进行一次释放掉)。


虚引用、终结器引用与前面软、弱引用区别:前两个可以不配合队列来进行使用,而这两个是一定要配合引用队列来使用。

注意一旦虚引用、终结器引用创建时就会直接关联一个队列。

虚引用:若是虚引用对象引用的对象被垃圾回收时,该虚引用对象就会被放入到队列,从而间接的有一个线程来调用虚引用对象的方法,来调用Unsafe.clearMemery方法来将对应的直接内存给清理。

终结器引用:所有的java对象都继承了Object类,都会有一个finallize()方法(终结方法)。若是某个对象重写了终结方法,并且没有强引用引用它时,它就可以被当成垃圾被回收。

  • 终结方法finallize()调用时机:需要靠终结器引用对象来达到目的,若是没有强引用引用对象时,JVM会为我们的对象来创建对应的终结器引用,一旦这个对象被垃圾回收时,这个终结器引用对象也会被加入引用队列,再由一个优先级很低的线程finallize线程在某些时机去查看这个引用队列中是否有终结器引用,若是有就会根据这个终结器引用找到被垃圾回收的对象并且调用其finallize方法,调用完了最终也会将这个对象给进行垃圾回收掉了。
  • 弊端:工作效率很低,第一次回收时还不能够真正把它回收掉而是先要入队,而且由于处理这个入队的终结器引用线程优先级很低处理的机会也会很少就可能会造成这个对象的finallize()迟迟不被调用,那么这个对象占用的内存也迟迟不会被释放,这也是为什么不推荐使用finallize()方法释放推荐的理由。


1.4、实际应用场景

1.4.1、软引用—垃圾回收软引用引用对象

软应用应用场景:对于存储图片资源这些并不是很重要的内容时并且内存资源十分紧张,我们可以使用软引用来进行。

普通java代码编写

案例描述:设置jvm中堆内存大小为20MB,打印GC垃圾回收信息。在该案例中由于仅仅设置20MB,循环5次,每次创建4MB大小的数组,那么过程中一会触发GC垃圾回收,但由于List对数组是强引用就会导致垃圾回收不了集合中的对象,最终就会导致堆内存溢出。

  • vm参数:-Xmx20m -XX:+PrintGCDetails -verbose:gc
import java.io.*;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;


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

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

image-20211118235434529

出现问题:使用普通的java代码编写的话,不考虑进行有效合理垃圾回收的话,此时就会抛出异常!

解决方案:由于这些集合添加的资源并不是十分重要的,很有可能使用了一次就不再使用了,那么我们其实可以将其添加到软引用中,一旦内存空间不足再经历了垃圾回收仍然不足的情况下,会对软引用引用的对象进行垃圾回收!从而阻止最终抛出堆内存溢出的问题!

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

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


    public static void main(String[] args) throws IOException {
        soft();
    }

    public static void soft() {
        // list --> SoftReference --> byte[]    引用关系
        List<SoftReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
            System.out.println(ref.get());
            list.add(ref);
            System.out.println(list.size());
        }
        System.out.println("循环结束:" + list.size());
        for (SoftReference<byte[]> ref : list) {
            System.out.println(ref.get());
        }
    }
}

image-20211119000413779



1.4.2、软引用—引用队列

目的:当内存不够时会将软引用引用的对象给清除,不过软引用自己本身依旧存在原始的集合中,本案例将软引用本身也作清理。

在1.4.1中,我们可以看到最后打印结果有四个null说明软引用引用的对象已经被回收,但是软引用自己本身并没有被回收,此时我们需要通过一个引用队列来进行手动将这些软引用对象给清除。

  • jvm参数:-Xmx20m -XX:+PrintGCDetails -verbose:gc

原理分析:创建一个软引用队列,在每次创建一个软引用对象时,将队列传入,之后若是垃圾回收回收了某个软引用对象,那么该软引用会被添加到队列中。之后我们遍历队列,来进行手动移除集合中的指定软引用!

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

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


    public static void main(String[] args) throws IOException {
        //1、定义一个引用队列
        ReferenceQueue<byte[]> queue = new ReferenceQueue<>();

        List<SoftReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            list.add(new SoftReference<>(new byte[_4MB], queue));//2、在调用构造方法时,传入队列。默认就会将该软引用对象添加到队列中
            System.out.println(list.get(i).get());
            System.out.println( i + 1 );
        }

        //3、出队软引用,从集合中移除指定的软饮用(GC进行垃圾回收若是将软引用引用的对象回收掉,那么该软引用也会被添加到队列中)
        Reference<? extends byte[]> ele = queue.poll();
        while (ele != null) {
            list.remove(ele);//从原始集合中移除该元素
            ele = queue.poll();
        }
        System.out.println("=====================");
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i).get());
            System.out.println( i + 1 );
        }

    }
}

image-20211119093928651



1.4.3、弱引用-WeakReference

弱引用:在垃圾回收时就会被清理。使用方式与软引用相同,只不过换一个对象而已。

import java.io.*;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;


/**
 * 演示软引用
 * -Xmx20m -XX:+PrintGCDetails -verbose:gc
 */
public class Main {
    private static final int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        //  list --> WeakReference --> byte[]
        List<WeakReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 5; 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());
    }
}

image-20211119102310677

疑问:就是黑马教程中在进行普通GC时就会将某个软引用引用的对象直接清除,而这里直接就触发了Full GC不太明白。

image-20211119102452036



二、垃圾回收算法

2.1、标注清除算法

过程

  • 标记:将被引用的对象与根节点进行关联。
  • 清除:并非将指定的区域进行清零擦除,而是将指定的对象所占用内存的起始结束地址记录下来放在一个空闲的地址链表中,下次进行分配内存的时候可以在对应链表中找即可,不需要做更多额外的处理,其垃圾回收的速度很快。

优缺点

  • 优点:速度快。
  • 缺点:容易产生内存碎片,此时若是要分配一块连续的大空间区域,此时多个碎片合起来的空间是够得,但是不连续此时这些空间就被称为碎片。

image-20211119105726170



2.2、标注整理算法

过程

  • 标记:将被引用的对象与根节点进行关联。
  • 整理:避免了之前垃圾清除时有碎片的问题。在清除垃圾的过程中会将占据空间的区域向前移动,有一个整理的过程。此时内存会变得更加紧凑,连续的空间也变得多了。
    • 注意:涉及整理内容有很多,不仅仅只是内存地址会向前移,对应的变量引用地址也要进行改变,牵扯到内存区块的拷贝移动,所有引用的地址加以改变。

优缺点

  • 优点:不会产生内存碎片。
  • 缺点: 速度慢,每次整理都涉及很多内容。

image-20211119110148797



2.3、复制算法

过程

  • 标记:将被引用的对象与根节点进行关联。
  • 复制:准备两块相同大小的内存区域,在进行复制操作时会将对应引用的对象复制到另一个区域中,之后将两个区域进行反转。这一过程与整理类似,只不过这里的话避免了向前大量前移的操作。

优缺点

  • 优点:不会产生内存碎片。
  • 缺点: 需要占用双倍的内存空间。

image-20211119110618107

image-20211119110634883

image-20211119110651554



总结

三种算法在jvm中都会有所应用,只不过会根据不同的情况来使用!



三、分代垃圾回收

3.1、新生代与老年代

针对不同区域采取不同的垃圾回收算法就可以更有效的对我们垃圾回收进行管理。

新生代:主要都是新创建的,可能需要频繁清理的对象。

老年代:存储的都是更有价值的一些对象,能够长时间存活的对象。

# 比喻
新生代:每个居民楼下都有一个公共的垃圾桶,其就是可以看做是居民楼,都是存放生命周期很短的垃圾,都是一些回收比较频繁的垃圾信息。
老年代:每家每户的垃圾可以看做是老年代,例如不使用的椅子啊不想扔可以直接先暂存。直到之后屋子实在放不下了再进行垃圾清理,将无用的垃圾清理掉。这个耗时较长,频率较低。

image-20211119112245199

  • 伊甸园:初始对象诞生的地方。
大致过程
首先伊甸园中的内存被占满时,就会触发一次垃圾回收(Minor GC),此时就会采用复制算法将存活的对象复制到幸存区To中,此时在幸存区中的对象寿命会进行+1,之后对伊甸园进行垃圾清理,紧接着To与From分区进行交换位置。

紧接着又重新创建对象分配空间在伊甸园中,若此时内存又要占满了,紧接着会触发第二次垃圾回收,此时会对伊甸园以及幸存区From中对象进行存活判断,接着将存活的对象再次重新放置到幸存区To中,此时他们的寿命都会依次+1,原本就在幸存区的此时就是2,在伊甸园中的对象则为1,此时对From与伊甸园进行垃圾回收,再次将To与From进行交换位置。

若是在幸存区中的对象寿命超过阈值(例如15),此时会将其对象径直复制到老年代中!

情况:若是新生代、老年代区域都快放满了,此时来了一个对象新生代放不下、老年代放不下,此时会先触发新生代的GC(Minor GC)若是还是空间不足就会触发Full GC(连带老年代垃圾回收),对整个堆进行清理。

重点说明

1.、垃圾回收的动作都是在内存不足时才会触发。

2、对象首先分配在伊甸园区域。

3、新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to。

4、minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。

  • 暂停其他线程的原因是:复制过程中牵扯到对象内存地址的改变,若是此时有多个线程同时运行就会造成线程的混乱

5、当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)。

6、当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长。

  • 老年代采用的回收算法与新生代不一样,由于存活对象比较多清除起来比较慢,采用的算法是标记+清除标记+整理,前者速度会快一些,后者会慢一些。


3.2、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(默认开启) -XX:+ScavengeBeforeFullGC


3.3、案例演示新生代与老年代的晋升过程

示例1:程序无任何代码时运行效果

/**
 *  演示内存的分配策略
 *  -XX:+UseSerialGC => 幸存区比例不会动态调整
 *  堆初始大小为20M,最大大小为20MB,新生代大小为10MB,后面的则是比例设置8:1:1
 *  -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
 */
public class Main {
    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 {
    }
}

打印结果:

Heap # 堆
 # 新生代   total 9216K(总共为9M)原因是并没有将to空间的算进去,to区间是不能用的;used 2990K(新生代总共使用量)
 # 后面跟着的是地址,高级的一些用户可以根据地址来使用一些工具进行排查
 def new generation   total 9216K, used 2990K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  # 伊甸园:8MB (初始有东西的原因是任何一个java程序初始都会加载对象)
  eden space 8192K,  36% used [0x00000000fec00000, 0x00000000feeebbb0, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 # 老年代(晋升区) :10MB
 tenured generation   total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,   0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
 # 元空间(方法区,并不属于堆,只是这里-XX:+PrintGCDetails -verbose:gc会进行打印该信息)
 Metaspace       used 3156K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 343K, capacity 388K, committed 512K, reserved 1048576K

示例2:分配7MB空间

/**
 *  演示内存的分配策略
 *  -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
 */
public class Main {
    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 {
        List<byte[]> list = new ArrayList<>();
        list.add(new byte[_7MB]);
    }
}

运行结果:

# 由于伊甸园空间不足发生了一次GC垃圾回收 
# 第一组:xx->xx,DefNew指的是新生代,后面指的是回收前->回收后,花费的时间。
# 第二组:指的是整个堆回收前与回收后的占用以及花费时间。
[GC (Allocation Failure) [DefNew: 2826K->909K(9216K), 0.0024737 secs] 2826K->909K(19456K), 0.0025432 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 8651K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  # 可以看到伊甸园的现在占用94空间,之后的from与to是交换位置了,之前的对象先会放入to空间,接着进行交换
  eden space 8192K,  94% used [0x00000000fec00000, 0x00000000ff38f7b8, 0x00000000ff400000)
  from space 1024K,  88% used [0x00000000ff500000, 0x00000000ff5e3500, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 tenured generation   total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,   0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
 Metaspace       used 3228K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 348K, capacity 388K, committed 512K, reserved 1048576K

示例3:新生代晋升老年代情景

/**
 *  演示内存的分配策略
 *  -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
 */
public class Main {
    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 -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC
    public static void main(String[] args) throws InterruptedException {
        List<byte[]> list = new ArrayList<>();
        list.add(new byte[_7MB]);
        list.add(new byte[_512KB]);
        list.add(new byte[_512KB]);
    }
}

运行结果:

# 可以看到此时进行了两次GC垃圾回收,第二次GC垃圾回收的原因是新生代的容量已经要被占满此时就会将新生代里的一些对象放置到老年区!
[GC (Allocation Failure) [DefNew: 2659K->901K(9216K), 0.0022758 secs] 2659K->901K(19456K), 0.0023360 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 8909K->519K(9216K), 0.0054093 secs] 8909K->8477K(19456K), 0.0054343 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 1196K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,   8% used [0x00000000fec00000, 0x00000000feca9578, 0x00000000ff400000)
  from space 1024K,  50% used [0x00000000ff400000, 0x00000000ff481e70, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 # 可以看到老年区现在已经占用了7MB
 tenured generation   total 10240K, used 7958K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  77% used [0x00000000ff600000, 0x00000000ffdc58f0, 0x00000000ffdc5a00, 0x0000000100000000)
 Metaspace       used 3153K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 343K, capacity 388K, committed 512K, reserved 1048576K


3.4、特殊情况(不进行GC直接晋升老年代)

大对象直接晋升老年代场景:若是当前一次创建的对象在新生代里肯定不够,则会直接添加到老年代里,不会触发垃圾回收。

/**
 *  演示内存的分配策略
 *  -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
 */
public class Main {
    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 {
        List<byte[]> list = new ArrayList<>();
        list.add(new byte[_8MB]);
    }
}

运行结果:

Heap
 def new generation   total 9216K, used 2990K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  36% used [0x00000000fec00000, 0x00000000feeebbb0, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 # 由于要直接一次开辟8MB空间,此时新生代容量不够会直接晋升到老年代,不会触发垃圾回收!
 tenured generation   total 10240K, used 8192K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  80% used [0x00000000ff600000, 0x00000000ffe00010, 0x00000000ffe00200, 0x0000000100000000)
 Metaspace       used 3220K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 346K, capacity 388K, committed 512K, reserved 1048576K


3.5、探究开辟线程出现堆内存溢出是否影响主线程程序

结论:一个线程内的out of memory不会导致主线程程序的结束!

主线程内存溢出,程序直接结束

/**
 *  演示内存的分配策略
 *  -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
 */
public class Main {
    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 {
        List<byte[]> list = new ArrayList<>();
        list.add(new byte[_8MB]);
        list.add(new byte[_8MB]);
    }
}

image-20211120135841231


新创建的线程中出现内存溢出

import java.util.ArrayList;
import java.util.List;

/**
 *  演示内存的分配策略
 */
public class Main {
    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(()->{
            List<byte[]> list = new ArrayList<>();
            list.add(new byte[_8MB]);
            list.add(new byte[_8MB]);
        }).start();

        System.out.println("Main Thread sleep !");
        Thread.sleep(2000);
    }
}

image-20211120135958987

image-20211120140045108

  • 可以看到在线程中第一次添加的8MB空间实际在堆中存在的,只是第二次创建内存空间不足导致该线程会直接停止运行并报异常。


四、垃圾回收器

STW:Stop the world

1、串行

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

2、吞吐量优先(总时间更优)

  • 多线程
  • 堆内存较大,多核 cpu
  • 让单位时间内(指的是一段时间内),STW 的时间最短 0.2 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高

3、响应时间优先

  • 多线程
  • 堆内存较大,多核 cpu
  • 尽可能让单次 STW 的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5


4.1、串行回收器

虚拟机参数(开启串行):XX:+UseSerialGC = Serial + SerialOld

  • serial工作在新生代,采用复制算法;serialold工作在老年代,采用标记+整理算法。

特点:单线程,对于新生、老年代都使用该种算法进行,一旦要进行垃圾回收,除GC线程都要进入阻塞,知道GC线程完成结束才唤醒其他线程!

image-20211127143736089

过程:内存不够时,触发垃圾回收此时多个线程都会在安全点停下来,因为在垃圾回收时可能对象的地址发送改变,为了保证安全的使用对象地址,需要所有的用户到达安全点,这时候垃圾回收线程开始工作,其他线程进入阻塞状态不去干扰GC线程工作!



4.2、吞吐量优先(并行垃圾回收器)

虚拟机参数:

# 开启并行垃圾回收期,在JDK8中默认是开启的。前者是应用于新生代垃圾回收期,采用复制算法,后者也是标记+整理。
-XX:+UseParallelGC ~ -XX:+UseParallelOldGC   
-XX:+UseAdaptiveSizePolicy  # 采用自适应大小调整策略,针对于新生代,动态调整伊甸园与surver区大小
-XX:GCTimeRatio=ratio  # 目标1:调整吞吐量目标 默认是99,一般设置为19。 例如ratio=99 1/(1+ratio),记过为0.01,也就是说100分钟内只有1分钟可以进行垃圾回收,若是达不到这个目标,垃圾回收期会去调整堆的大小一般是增大(吞吐量提升)。
-XX:MaxGCPauseMillis=ms  # 目标2:最大暂停毫秒数,默认值是200ms,这与目标1是有冲突的,要与应用情况进行取合理值。
-XX:ParallelGCThreads=n    # 线程数控制参数,根据目标来进行设定

特点:开启多个垃圾回收线程来进行回收,默认线程数与你的CPU核数相关,回收结束之后继续让其他线程运行。

image-20211127145215659



4.3、响应时间优先(CMS垃圾回收器,一款老年代)

# 开启并发清除垃圾回收。基于标注清除算法的垃圾回收器
# -XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC:后者是一款工作在基于新生代的垃圾回收器,但是CMS垃圾回收期有时候会发生一个并发失败的问题,这时候采取补救的措施,让老年代的回收器从CMS退化成serial old垃圾回收器(并发->单线程)
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads   # 前者并行线程数、后者并发线程数,一般设置成并行线程数1/4
-XX:CMSInitiatingOccupancyFraction=percent  # 早期默认值为65%。控制何时进行CMS垃圾回收的时间,执行CMS垃圾回收时间的内存占比。percent表示时间占比,例如设值80,若是老年代的内存占用到80%时就执行一次垃圾回收,这样的话是预留一些空间给浮动垃圾。越小触发时机越早。
-XX:+CMSScavengeBeforeRemark  # 问题:有可能新生代的对象引用老年代的对象,在重新标记时可能会扫描整个堆,之后在扫描新生代时可能会扫描老年代对象作可达性的分析,这样的话对我们的性能影响较大,在回收之前多做了一些无用的查找工作。
# 解决方案:为了避免整个现象可在重新标记前先对新生代做一次垃圾回收,通过这种方式之后的扫描时间也就更少,减轻重新标记时的压力。

特点:对于CPU占用并没有之前并行GC占用线程高,但是由于有大量并发处理,对于我们的吞吐量是有一定影响的。

  • 内存碎片特别多的时候,造成产生比较多的内存碎片现象,这样的话就会造成我将来分配内存对象,由于新生代、老年代内存空间都不足最终造成并发失败,此时CMS老年回收器就不能正常工作了这时候垃圾回收期就会退化为serial old,也就是来做一次单线程串行的垃圾回收,做一次整理,将内存清理完成之后其他线程重新运行。
  • 现象:遇到并发失败之后而坐串行垃圾回收时,就会出现

进行垃圾回收的同时,用户线程与垃圾回收线程可以同时进行并发执行,都要去抢占CPU,进一步减少了STW时间,在某些阶段还是需要STW的,但是在垃圾回收的其他阶段是不需要STW的,用户线程可与GC线程并发执行。

内存已满,此时要进行垃圾回收。首先多个线程到达了安全点,此时CMS垃圾回收期开始工作了:

  1. 首先会进行初始标记的动作(该动作会出现STW情况)。
  2. 等到初始标记完成以后此时用户线程可以进行运行了。此时垃圾回收线程开始进行并发标记,注意该过程是与用户线程并行的,该过程不需要STW,所以响应时间是很短的,几乎不影响用户线程工作。
  3. 并发标记以后要做一步重新标记,该步骤需要STW。
  4. 一旦重新标记完了,用户线程又可以进行运行,这时候垃圾回收线程做一次并发清理,该过程我也可以与用户线程进行并发处理。

image-20211127150212825

在进行并发清理的过程中产生出来的垃圾叫做浮动垃圾,这些垃圾需要在我们下次进行清理时才能够被清理掉!



4.4、G1(JDK9默认使用)

定义:Garbage First,并发垃圾回收器。

2004 论文发布
2009 JDK 6u14 体验
2012 JDK 7u4 官方支持  逐渐成熟起来了
2017 JDK 9 默认  此时废弃了之前的CMS垃圾回收器

适用场景

  1. 同时注重吞吐量(Throughput)和低延迟(Low latency),也是属于并发的。默认的暂停目标是 200 ms。
  2. 超大堆内存管理思想,会将堆划分为多个大小相等的 Region,例如每个区域1,2,4,8,每个区域都可以独立作为伊甸区、Survivor区、老年区。(G1与CMS在内存较小的场景下,暂停时间是不相上下的,若是随着堆内存的容量越来越大,那么G1的优势更加明显,比CMS暂停时间更领先)
  3. 整体上是 标记+整理 算法(不会出下内存碎片问题),两个区域之间是 复制 算法。
-XX:+UseG1GC  # 开启G1垃圾回收器(JDK9默认)
-XX:G1HeapRegionSize=size  # 设置区域大小,例如1、2、4、8、16
-XX:MaxGCPauseMillis=time  # 默认的暂停目标

新生代调优:-Xmn,并非越大越好。越大在进行full GC时间越长。主要耗费时间在复制上。

新生代-幸存区:存活时间短放在幸存区,利于垃圾GC。



整理者:长路 时间:2021.11.19
过程我也可以与用户线程进行并发处理。

[外链图片转存中…(img-hppnaOjC-1651711271433)]

在进行并发清理的过程中产生出来的垃圾叫做浮动垃圾,这些垃圾需要在我们下次进行清理时才能够被清理掉!



4.4、G1(JDK9默认使用)

定义:Garbage First,并发垃圾回收器。

2004 论文发布
2009 JDK 6u14 体验
2012 JDK 7u4 官方支持  逐渐成熟起来了
2017 JDK 9 默认  此时废弃了之前的CMS垃圾回收器

适用场景

  1. 同时注重吞吐量(Throughput)和低延迟(Low latency),也是属于并发的。默认的暂停目标是 200 ms。
  2. 超大堆内存管理思想,会将堆划分为多个大小相等的 Region,例如每个区域1,2,4,8,每个区域都可以独立作为伊甸区、Survivor区、老年区。(G1与CMS在内存较小的场景下,暂停时间是不相上下的,若是随着堆内存的容量越来越大,那么G1的优势更加明显,比CMS暂停时间更领先)
  3. 整体上是 标记+整理 算法(不会出下内存碎片问题),两个区域之间是 复制 算法。
-XX:+UseG1GC  # 开启G1垃圾回收器(JDK9默认)
-XX:G1HeapRegionSize=size  # 设置区域大小,例如1、2、4、8、16
-XX:MaxGCPauseMillis=time  # 默认的暂停目标

新生代调优:-Xmn,并非越大越好。越大在进行full GC时间越长。主要耗费时间在复制上。

新生代-幸存区:存活时间短放在幸存区,利于垃圾GC。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。