JVM学习笔记(三)垃圾回收

奋斗吧
奋斗吧
擅长邻域:未填写

标签: JVM学习笔记(三)垃圾回收

2023-07-15 18:23:54 103浏览

相关文章:笔记参考文章:JVM 学习笔记(二)垃圾回收_CodeAli的博客-CSDN博客当一个对象被引用时,就当引用对象的值加一,当值为 0 时,就表示该对象不被引用,可以被垃圾收集器回收。这个引用计数法听起来不错,但是有一个弊端,如下图所示,循环引用时,两个对象的计数都为1,导致两个对象都无法被释放。(java虚拟机垃圾回收没有采用它)堆分析工具:MAT,eclipse官网下载地址:Eclipse Memory Analyzer Open Source Project | The Eclipse Fou

相关文章:

jvm虚拟机运行时数据区

虚拟机栈:存储基本数据类型、引用对象的变量、局部变量表等,这是线程私有的,每个线上线程的大小默认为1Mb。

程序计数器:存储字节指令的地地址,如果是本地方法栈,则存储undefined。

本地方法栈:由于java是表层语言,无法直接访问硬件,需要调用第三方语言,比如C、C++来操作硬件,比如创建内核线程,操作文件等。

方法区:存储jvm编译后地字节码文件,静态变量,常量,类信息等。

堆:

  • 这是一块很重要的存储区域,也是我们性能调优重要依据,其用来存储java对象,gc回收地也是这一块数据。其分为老年代和新生代,默认数据大小为2 :1。
  • 新生代又分为Eden区,s0区,s1区,默认数据大小为8 : 1 : 1。
  • 新创建一个对象,首先判断能否放到Eden区,如果Eden区满了,会触发mirrorGc「所有的 Minor GC 都会触发“全世界的暂停(stop-the-world)”,停止应用程序的线程,但这段时间可以忽略不计」。此时Eden区和s0区中存活的对象移至s1区,并标志对象的分代年龄,eden区和s0区清空,如果此时对象还无法放置eden区,则直接放置老年代。反之亦然。
  • 分代年龄存储到java对象头中。如果old区满了,会触发fullGc,我们尽量避免fullGc「fullGc暂停所有正在执行的线程(Stop The World),来回收内存空间,这个时间需要考虑地」。

因而,我们所说的性能调优,那么就是堆中的性能调优。
 

笔记参考文章:

JVM学习笔记——垃圾回收篇-腾讯云开发者社区-腾讯云

JVM 学习笔记(二)垃圾回收_CodeAli的博客-CSDN博客

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

1. 引用计数法

当一个对象被引用时,就当引用对象的值加一,当值为 0 时,就表示该对象不被引用,可以被垃圾收集器回收。这个引用计数法听起来不错,但是有一个弊端,如下图所示,循环引用时,两个对象的计数都为1,导致两个对象都无法被释放。(java虚拟机垃圾回收没有采用它)

 2. 可达性分析算法

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

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

2.1 哪些对象可以作为 GC Root ?

堆分析工具:MAT,eclipse官网下载地址:Eclipse Memory Analyzer Open Source Project | The Eclipse Foundation

 2.2 案例

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

        ArrayList<Object> list = new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add(1);
        System.out.println(1);
        System.in.read();

        list = null;
        System.out.println(2);
        System.in.read();
        System.out.println("end");
    }

第一步:运行上面的代码后,找到.class文件的位置,选中->右键->Open In->Terminal,然后使用命令jps查看当前系统中有哪些 java 进程;

第二步:执行如下命令,生成转储文件1.bin,然后在Run窗口回车让程序继续执行下面的代码,最后再回到Terminal窗口执行命令 jmap -dump:format=b,live,file=2.bin 10396 这时便生成了两个转储文件;

jmap -dump:format=b,live,file=1.bin 10396

参数解释:

  • 参数"-dump:":表示不是查看堆内存的使用情况,而是要把当前堆内存的状态转储到文件;
  • format=b:表示指定转储文件的格式为二进制格式;
  • live:抓快照时只关心存活的对象,过滤掉那些被垃圾回收掉的;且这个参数表示抓快照前会进行一次垃圾回收,因为进行垃圾回收后才知道哪些是存活对象;
  • file=1.bin:指定转储文件的文件名;(当前目录下)
  • 10396:进程id

第三步:用刚刚下载的堆内存分析工具MAT打开上面生成的文件进行分析;对比两个文件发现,ArrayList对象被回收掉了。

在这里插入图片描述

延伸:区分引用变量和对象

MAT工具使用遇到的问题

双击运行程序时,提示下面的错误。此时,需要在官网下载一个JDK11的压缩包,解压后放在磁盘中某个位置,如D:\JavaTools\jdk-11.0.19;然后打开MAT安装目录下的MemoryAnalyzer.ini文件,添加下方两行代码,表示指定以JDK11方式启动。这样,再次双击运行MAT工具时,便可正常启动。

-vm
D:/JavaTools/jdk-11.0.19/bin/javaw.exe

MAT工具使用的文章

3. 四种引用

强引用

  • 只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收。

软引用(SoftReference)

  • 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用对象;
  • 可以配合引用队列来释放软引用自身。

弱引用(WeakReference)

  • 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象;
  • 可以配合引用队列来释放弱引用自身。

虚引用(PhantomReference)

  • 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存

终结器引用(FinalReference)

  • 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize方法,第二次 GC 时才能回收被引用对象。

3.1 演示软引用

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

    public static int _4MB = 4 * 1024 * 1024;

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

    // 设置 -Xmx20m , 演示堆内存不足,
    public static void method1() throws IOException {
        ArrayList<byte[]> list = new ArrayList<>();

        for(int i = 0; i < 5; i++) {
            list.add(new byte[_4MB]);
        }
        System.in.read();
    }

    // 演示 软引用
    public static void method2() throws IOException {
        ArrayList<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());
        }
    }
}

method1 方法解析:
首先会设置一个堆内存的大小为 20m,然后运行 mehtod1 方法,会抛异常,堆内存不足,因为 mehtod1 中的 list 都是强引用。 

在这里插入图片描述

method2 方法解析:
在 list 集合中存放了 软引用对象,当内存不足时,会触发 full gc,将软引用的对象回收。细节如图:

在这里插入图片描述上面的代码中,当软引用引用的对象被回收了,但是软引用还存在,所以,一般软引用需要搭配一个引用队列一起使用。
修改 method2 如下: 

    // 演示 软引用 搭配引用队列
    public static void method3() throws IOException {
        ArrayList<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[]> ref : list) {
            System.out.println(ref.get());
        }
    }

在这里插入图片描述

3.2 弱引用演示

public class Code_09_WeakReferenceTest {

    public static void main(String[] args) {
//        method1();
        method2();
    }

    public static int _4MB = 4 * 1024 *1024;

    // 演示 弱引用
    public static void method1() {
        List<WeakReference<byte[]>> list = new ArrayList<>();
        for(int i = 0; i < 10; i++) {
            WeakReference<byte[]> weakReference = new WeakReference<>(new byte[_4MB]);
            list.add(weakReference);

            for(WeakReference<byte[]> wake : list) {
                System.out.print(wake.get() + ",");
            }
            System.out.println();
        }
    }

    // 演示 弱引用搭配 引用队列
    public static void method2() {
        List<WeakReference<byte[]>> list = new ArrayList<>();
        ReferenceQueue<byte[]> queue = new ReferenceQueue<>();

        for(int i = 0; i < 9; i++) {
            WeakReference<byte[]> weakReference = new WeakReference<>(new byte[_4MB], queue);
            list.add(weakReference);
            for(WeakReference<byte[]> wake : list) {
                System.out.print(wake.get() + ",");
            }
            System.out.println();
        }
        System.out.println("===========================================");
        Reference<? extends byte[]> poll = queue.poll();
        while (poll != null) {
            list.remove(poll);
            poll = queue.poll();
        }
        for(WeakReference<byte[]> wake : list) {
            System.out.print(wake.get() + ",");
        }
    }

}

首先学习老师视频52~56,若想进一步学习java的四种引用,可参考以下资料:

二、垃圾回收算法

1. 标记清除

        定义: Mark Sweep

        特点:①速度较快;②会造成内存碎片

2. 标记整理

        定义:Mark Compact

        特点:①速度慢;②没有内存碎片

3. 复制

        定义:Copy
        特点:①不会有内存碎片;②需要占用双倍内存空间

 
三、分代垃圾回收

  • 对象首先分配在伊甸园区域
  • 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to
  • minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
  • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
  • 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长 

3.1 相关 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

更多参数的理解,请移步: JAVA命令行工具(一)--JAVA垃圾回收选项 - 简书

3.2 GC分析

(1)main中任何代码时

问题一:上图中,新生代总大小total为什么只有9M多?

答:JVM参数新生代大小参数【-Xmn10M】明明划分了10M,可是为什么只有9M多呢?因为幸存区比例参数【-XX:SurvivorRatio=ratio】添加后,8M会分给伊甸园,幸存区from和幸存区to各占1M。这里就认为幸存区to那一兆的空间要始终空着,是不能用的,所以计算空间时,那1M就没有计算在内。

扩展:-XX:SurvivorRatio=ratio参数详解

  设置Survivor和Eden的相对大小,该选项作用于新生代内部。为了明白该选项的含义,需要了解新生代的空间划分,一个简单的示意如下:

    |  Eden  | From(S0) | To(S1) | // Trom和To都是Survivor且大小相同

该选项表示Eden区和两个Surivior区的比率,默认值为8,即各区的比例为Eden:S0:S1=8:1:1。如果Eden区过大,在一次GC后幸存的对象过多以至于Survivor区不能容纳,那么大部分对象在一次GC后就会转移到老年代;如果Eden区过小,意味着新对象的产生将很快耗尽Eden区,从而增加新生代GC次数。

问题二:上面代码中只有main方法,内部什么都没有,为什么eden伊甸园空间已经使用了28%?

因为哪怕是再简单的一个java程序运行时,都要加载一些类,创建一些对象;这些对象刚开始也是使用的伊甸园区域。

(2)main中创建list集合并分配7M

 注意:如何区分老年代GC还是新生代GC呢?

  • 上图中如果是GC开头时,表示新生代GC;如果为full GC时,表示老年代GC;

四、垃圾回收器


五、垃圾回收调优

好博客就要一起分享哦!分享海报

此处可发布评论

评论(0展开评论

暂无评论,快来写一下吧

展开评论

您可能感兴趣的博客

客服QQ 1913284695