从零单刷JVM网课逐集详细笔记(连载中...)

网课来自B站黑马官方:https://www.bilibili.com/video/BV1yE411Z7AP/?share_source=copy_web&vd_source=14a5b51324eb1c304bfd050117731789

在此再次感谢所有开源资源。

1-01什么是jvm

在这里插入图片描述
在这里插入图片描述

2-02学习jvm有什么用

关于第三点:排错,找内存泄漏点。

3-03常见的jvm

JVM 是一套规范。

当前我们学的是Oracle的HotSpot

4-04学习路线

ClassLoader类加载器
一个类,从java源代码,编译为二进制字节码,必须经过类加载器,才能被加载到JVM里运行。
类都放在方法区Method Area。
类创建的实例、对象放在堆区Heap。
堆里的对象调用方法时,会用到JVM Stacks、PC Register、Native Method Stacks。
方法执行时,每行代码被执行引擎中的Interpreter逐行执行,
方法里的热点代码或说频繁调用的代码会被JIT Compiler作编译(优化)后的执行,
GC(garbage collect)对堆Heap中不再被引用的对象进行回收。
另外还有Java代码不方便实现的功能,比如必须调用底层操作系统的功能,要借助本地方法接口来完成。

学习过程由简到繁。

5-05程序计数器作用

java源代码不能被直接执行,需要经过一次编译。
编译成左侧的二进制字节码,这些字节码是jvm指令,这些指令就是java跨平台的基础。
这些指令还不能直接交给cpu执行,还需要经过解释器 Interpreter,解释器把jvm指令解释成为机器码,机器码就可以直接被cpu执行。
重点是要知道java中jvm指令的执行流程。源码→jvm指令→解释器译为机器码→cpu执行。

程序计数器的作用:记录下一条jvm指令的执行地址。

在执行每一条jvm指令时,都会把下一条jvm指令的地址放入程序计数器中。当上一条jvm指令执行结束后,解释器就会去程序计数器里取得下条指令的地址,以获得指令内容。以此往复。

物理上,程序计数器是通过一个寄存器实现的。寄存器是cpu里读取速度最快的单元。由于读写程序计数器非常频繁,故jvm在设计时,取寄存器作为程序计数器。

6-06程序计数器特点

cpu会给每个线程分配时间片,当前线程可以在自己的时间片里获得cpu资源,当时间片结束后,需要程序计数器记录当前执行进度,记录下一个要执行的指令。等其它线程的时间片结束,cpu资源回来时,就可以从本线程的程序计数器里获得下一个本线程要执行的指令,继续下去。

7-07 栈

栈 - 线程运行时需要的内存空间。
如果有多个线程,就会有多个虚拟机栈。
1个栈可看成由多个栈帧组成,1个栈帧可以看作是1次方法的调用。
线程最终是为了要执行代码,而代码又由一个个方法组成。
在线程运行的时候,每个方法需要的内存称为1个栈帧。
栈帧:每个方法运行时需要的内存。
方法里有参数、局部变量、如果方法有返回值时,还需要存储返回地址。这些都是需要占用内存的。方法执行时就需要预先分配这些内存。

程序调用方法1,就先把栈帧1先压入栈内。方法1里调用了方法2,就会再把栈帧2压入栈内。
当方法2结束时,栈帧2先被弹出栈,然后方法1结束之后,再弹出栈帧 1。

8-08栈的演示

9-09栈问题辨析1

1、不需要,因为方法结束后,栈帧自动弹出,不需要回收。
2、不是,栈内存越大反而使线程数变少。因为栈内存是给每个线程分配的,物理空间确定,单个线程所获得的栈空间越大,物理空间里可同时存在的线程就越少。栈空间越大,通常只是可以进行更多次的方法递归调用。一般使用系统默认的栈空间大小即可。

10-10栈问题辨析2线程安全

以上方法结束后,x的值会不会混乱?
答案是不会。x是方法内的局部变量。1个线程对应1个栈,线程内每次方法调用都会产生1个新的栈帧。
即每个线程里都有1个私有的局部变量x,初始值都是0。

但如果是 static int x = 0; 就不一样了,此时线程1和线程2都要读取这个变量,并且各自自增后都要回写到这个static变量里,会出现线程安全的问题。

11-11栈问题辨析2线程安全

在这里插入图片描述

这3个方法会不会存在线程安全问题?

m1:安全,因为是局部变量。
m2:不安全,因为是传入的参数,其它线程也可能访问到这个变量。
m3:不安全,因为局部变量被当成方法的返回结果返回了,这意味着其它线程有可能获得这个对象的引用,就可能对它进行了修改。

结论:
在这里插入图片描述

另外如果是基本数据类型的变量,即使它是形参或者是返回值,它也是线程安全的。
在这里插入图片描述

12-12栈内存溢出1

  • 栈帧过多导致栈内存溢出。比如在方法的递归调用里不设置正确的结束条件就会容易导致栈溢出。每次调用自己都会产生新的栈帧。
    在这里插入图片描述

  • 栈帧过大导致栈内存溢出。很少发生,因为栈帧里一般都是局部变量或方法参数,它们占用内存比较小,不会轻易导致栈帧过大。

演示栈帧过多导致栈溢出
在这里插入图片描述

报的错是:java.lang.StackOverflowError

栈内存可以通过虚拟机参数 -Xss 来设置,下面是各系统的默认栈空间
在这里插入图片描述

演示idea中如何设置栈内存
在这里插入图片描述
在这里插入图片描述

应用即可。再次执行上方演示demo,就发现数变小了。
在这里插入图片描述

从24864减小到5222

13-13栈内存溢出2

第三方库也有导致栈溢出的风险。

如在使用json工具类将对象转为json字符串时。如果对象和对象的属性有循环套用,就会发生栈溢出。在这种情况中,要注意对套娃属性进行转json的忽略,在使用Jackson库时,用@JsonIgnore实现。

@Data
class Emp {
    private String name;
    private Dept dept;
}

@Data
class Dept {
    private String name;
    private List<Emp> emps;
}

public static void main(String[] args) throws JsonProcessingException {
    Dept d = new Dept();
    d.setName("Market");
    
    Emp e1 = new Emp();
    e1.setName("zhang");
    e1.setDept(d);
    Emp e2 = new Emp();
    e2.setName("zhang");
    e2.setDept(d);
    
    d.setEmps(Arrays.asList(e1, e2));
    
    ObjectMapper mapper = new ObjectMapper();
    System.out.println(mapper.writeValueAsString(d));
    // 优化前,这里会报错 java.lang.stackOverflowError
    /* 因为套娃了:
    { name : 'Market', 
      emps : [
      { name : 'zhang', 
      	dept : { 
      		name : 'Market',
             emps : [ {...}, {...} ]
             }
       },
       {...}
       ]
    }
    */
}

// 需要优化才能解决这个栈溢出的问题
@Data
class Emp {
    private String name;
    @JsonIgnore
    private Dept dept;
}
// 当前场景下,加上上方注解即可。

2023-04-29_15:32:26

15-15线程诊断迟迟得不到结果

2023-06-09_17:57:55,开始学习

案例1:cpu占用过多时的线程运行诊断

centos中的top命令可以查看到各进程的cpu占用情况

top

在这里插入图片描述

top命令只能定位到进程,无法定位到线程。

下面用ps命令可以查到每个进程中,每个线程的占用情况

ps H -eo pid,tid,%cpu
# 列出进程的PID(进程ID)、TID(线程ID)和进程/线程使用CPU的百分比信息。
# H 表示列出进程和线程
# -eo 表示格式化输出信息
# pid 进程id
# tid 线程id
# %cpu 进程或线程消耗的cpu百分比。此处是累计使用cpu时间的百分比,而不是单个cpu的占用率	

输出结果如下

PID   TID  %CPU
1234  1234 0.0
1234  1235 20.0
5678  5678 0.0
5678  5679 30.0

使用 grep 过滤,进一步看到是哪个线程的占用率最高

ps H -eo pid,tid,%cpu | grep 1234
# 会将进程号为1234的所有线程筛选出来显示

用jstack命令将某个java进程的所有线程列出来

# jstack pid
jstack 32655
# 该命令可以根据线程id找到有问题的线程,进一步定位到问题代码的源码行号

在这里插入图片描述

已知pid为32655的进程里,tid为32665的线程,cpu的占用率是99.5%。
在jstack命令里,线程号是16进制的,所以要定位到32665线程的话,先把32665从10进制转为16进制,再去jstack 32655的输出结果里找到这个线程的信息
在这里插入图片描述

在这里插入图片描述

15-15线程诊断迟迟得不到结果

案例2:程序运行长时间没有得到结果,诊断出来是线程死锁

# jstack pid
jstack 32752

拉到输出结果的最后面可以看到一个报线程死锁的提示,并且告知了发生的源码位置
在这里插入图片描述

16-16本地方法栈
17-17堆定义
18-18堆内存溢出

19-19堆内存诊断jmap

在这里插入图片描述

演示堆内存用的案例代码

# 查看当前系统中有哪些 java 进程
jps

jps直接在 idea 里的 terminal 里面敲
在这里插入图片描述

在控制台分别打印出来1… 2… 3… 之后,去 terminal 分别输入一次指令 jmap -heap 18756

# 查看堆内存占用情况
jmap -heap pid

break 2023-06-09_18:42:26,看到19-19,05:05

start 2023-06-10_18:05:54
在这里插入图片描述

每次输出jmap指令后都会有一些输出。下面是其中一次输出的内容。

Heap Configuration 是堆配置信息
MaxHeapSize 表示最大堆内存
NewSize 新生代Size
OldSize 老年代Size

Heap Usage 堆内存占用信息
在这里插入图片描述

新创建的对象会使用 Eden Space 这个区
在控制台打印出来1…时,new byte[] 语句还没执行,可以看到 Eden Space 里的 used = 6.4… MB
在控制台打印出来2…时,new byte[] 语句已经执行了,看到 used = 16.4… MB,使用了的空间增加了。
在控制台打印出来3…时,System.gc(); 已经执行了,指向 null 的对象就会被回收。看到 used = 1.2… MB。

20-20堆内存诊断jconsole

在这里插入图片描述

执行代码,然后 terminal 输入指令

jconsole

弹出来控制台界面

选择相应进程就可以进去动态看到堆内存占用情况了。
也还有其它可观测的信息
在这里插入图片描述

21-21堆内存诊断jvirsualvm

假设当前系统已经有正在执行的java程序,进行以下步骤

jps # 先查看进程id
jmap -heap 17748 # 查看堆内存占用情况

在这里插入图片描述新生代内存占用 33M

在这里插入图片描述
老年代占用 202MB

一共占用230MB左右。

使用 jconsole 控制台,在内存标签点击 执行GC,之后,发现内存占用也没有下降很多。用 jmap 重新看一下,发现新生代剩8M占用,老年代还剩200多M。为什么内存没有回收掉?

使用新的可视化虚拟机界面。在 terminal 界面输入下方指令。

jvisualvm

在这里插入图片描述

堆dump 功能,抓取当前堆的快照,用于分析。这个功能 jmap 和 jconsole 没有。
在这里插入图片描述

这是当前时刻的堆内存占用的快照。点击右边检查卡中的查找按钮可以列出当前占用内存最大的20个对象,降序排列。
在这里插入图片描述

点进去这个占用量第一的类看一看。
在这里插入图片描述

有很多student对象。
在这里插入图片描述

每个 student 对象都占用了1m多空间,一共200个student对象。就定位到了这个 arraylist 有问题,长时间使用,无法回收。

看到源代码是这样的。
在这里插入图片描述

数组里add了200个对象,sleep了,所以一直没有被回收。

22-22方法区定义

Java 虚拟机有一个在所有 Java 虚拟机线程之间共享的方法区域。方法区域类似于用于传统语言的编译代码的存储区域,或者类似于操作系统进程中的“文本”段。它存储每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括特殊方法,用于类和实例初始化以及接口初始化方法区域是在虚拟机启动时创建的。尽管方法区域在逻辑上是堆的一部分,但简单的实现可能不会选择垃圾收集或压缩它。此规范不强制指定方法区的位置或用于管理已编译代码的策略。方法区域可以具有固定的大小,或者可以根据计算的需要进行扩展,并且如果不需要更大的方法区域,则可以收缩。方法区域的内存不需要是连续的!
在这里插入图片描述

JVM 1.6 版本里,方法区的实现叫做 PermGen 永久代,使用的是堆内存的空间。
1.8之后,永久代这个实现被废弃了,方法区的实现变为了 元空间 Metaspace,不再占用堆内存,不再由JVM来管理内存结构。也注意到 StringTable 也不再被放到元空间里了,或者说 StringTable 被单独拿出来移到了堆当中。(见上图下半部分)

23-23方法区内存溢出

演示元空间内存溢出

/**
 * 演示元空间内存溢出
 * -XX:MaxMetaspaceSize=8m
 */
@Slf4j
public class Test extends ClassLoader { // ClassLoader 是类加载器,继承它之后可以使用加载类的二进制字节码的方法
    public static void main(String[] args) {
        int j = 0;
        try {
            Test test = new Test();
            for (int i = 0; i < 10000; i++, j++) {
                // ClassWriter 作用是生成类的二进制字节码。参数0表示关闭自动计算和验证栈帧映射表,即不对字节码进行额外的验证和优化。
                ClassWriter cw = new ClassWriter(0);
                // visit()执行生成。形参意义:版本, 权限修饰符public, 类名, 包名, 父类, 这个类要实现的接口
                cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                // 生成类,并返回这个类的字节码的byte数组。二进制字节码都是用byte表示的
                byte[] code = cw.toByteArray();
                // 用 ClassLoader 的加载类的方法。加载类分加载、链接、初始化等等好几个阶段。defineClass 方法只会触发类的加载,不触发其它。
                test.defineClass("Class" + i, code, 0, code.length); // 类名, byte数组, 读取的数组的起始位置, 结束位置
            }
        } finally {
            // 看看加载了多少个类
            System.out.println(j);
        }
    }
}

首先,jdk1.8版本的方法区的实现是元空间,元空间默认情况使用系统内存,并且默认没有设置上限,所以上面main方法直接运行的时候并不会触发元空间内存溢出。
在这里插入图片描述

当我们手动设置方法区,也就是元空间的上线后,再次运行,就能看到元空间内存溢出的报错了。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在 VM Options 里输入下方指令后点击 Apply 和 OK

-XX:MaxMetaspaceSize=8m

然后再次运行main方法,就能看到控制台报错内存溢出了。
注意到 OutOfMemory : Metaspace, 冒号后面跟的是Metaspace
在这里插入图片描述

如果是1.8之前的版本,可能就会报
Exception in thread “main” java.lang.OutOfMemoryError: PermGen space
(PermGen space 表示永久代)
或者报
Exception in thread “main” java.lang.OutOfMemoryError: Compressed class space

24-24方法区内存溢出的场景

代理技术里广泛应用了字节码的动态生成技术。所以在使用 Spring 框架、Mybatis 框架经常会在运行期间产生大量的类,也是存在导致永久代内存溢出的风险的,因为永久代是在堆内存中,受到JVM的垃圾回收机制管理,效率不算太高。到了1.8之后使用元空间,系统内存相对更加充裕,垃圾回收机制是由内存空间自行管理的,效率更加高一些。

25-25方法区常量池

二进制字节码包括三部分

  • 类的基本信息
  • 常量池
  • 类方法定义(包含了虚拟机指令)

准备一个类

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

执行它使生成class文件。

到 terminal 里执行命令对 class 文件进行反编译

javap -v HelloWorld.class
# javap 是 Java 自带的反汇编工具,可以将 Java 字节码文件反汇编成可读的指令列表。
# -c:输出反汇编后的字节码指令。这个选项会显示每个方法中的字节码指令,以及它们的操作数和操作数栈的状态。这个选项可以让我们了解代码内部的实现细节。
# -v:输出更详细的反汇编信息。在 -c 输出的基础上,还会显示类、字段、方法等的访问标志、常量池、异常处理表、局部变量表等信息。这个选项可以让我们了解更多关于类的结构和组成的信息。

类的基本信息包括上次修改时间、包名、类名、内部版本号、访问修饰符、父类、接口信息等等。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

类的方法定义里,看到第一个是类的无参构造器。
在这里插入图片描述
在这里插入图片描述

常量池的作用就是提供常量符号给虚拟机去索引,去找到要执行的内容。

stop 2023-06-10_20:58:32 看到26-26 0:00

26-26方法区运行时常量池

start 2023-06-11_12:37:53

  • 常量池:一张表,虚拟机指令根据此表找到要执行的类名、方法名、参数类型、字面量等信息。
  • 运行时常量池:常量池在*.class文件中,当该类被加载,它的常量池信息就会被放入运行时常量池,并把里面的符号地址变为真实地址。

27-27_StringTable面试题

在这里插入图片描述

28-28_StringTable常量池与串池的关系

public class Demo1_22 {
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
    }
}

编译上面这个class,然后反编译它获得一些信息。反编译指令 javap -v Demo1_22.class,查看其常量池。
在这里插入图片描述

package com.ruiyuai.ruiyu.server.collect.utils;

/**
 * TODO
 *
 * @ClassName Demo1_22
 * @Date 2023/6/11 13:07
 * @Created by chenwei
 */
public class Demo1_22 {
    // 常量池中的信息,都会被加载到运行时常量池中,此时 a b ab 都是常量池中的符号,尚未变成 java 字符串对象。
    // 【执行】0: ldc    #2    // String a ====> 会把 a 符号变为 "a" 字符串对象。类懒汉行为
    // 然后还会做一件事情,会把 "a" 作为key,去看看 StringTable[] 里找有没有取值相同的 key,
    //     如果在 StringTable[] 里没有找到 "a",就会把 "a" 放入,此时得到 StringTable["a"]。
    // 【执行】3: ldc    #3    // String b ====> 会把 b 符号变为 "b" 字符串对象。类懒汉行为
    // 去找 StringTable["a"] 里有无 "b" ,没有就放入,得到 StringTable["a","b"]
    // 【执行】6: ldc    #4    // String ab ====> 会把 ab 符号变为 "ab" 字符串对象。类懒汉行为
    // 去找 StringTable["a","b"] 里有无 "ab" ,没有就放入,得到 StringTable["a","b","ab"]
    // StringTable["a","b","ab"] 是 hashtable 结构,不能扩容

    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        // 总结,字符串对象的创建是懒汉式的,尚未执行到时,不会创建对象。
        // 创建之前会先到串池 StringTable 找有没有这个符号,有就直接用,没有的话再创建然后放入。
        // 串池中的字符串对象不重复。相同字符串只会存在一个。
    }
}

29-29_StringTable字符串变量拼接

源码

public class Demo1_22 {
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2;
    }
}

对以上class进行反编译 javap -v Demo1_22.class,查看结果中的main方法

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=5, args_size=1
         0: ldc           #2                  // String a
         2: astore_1
         3: ldc           #3                  // String b
         5: astore_2
         6: ldc           #4                  // String ab
         8: astore_3
         /* 创建了一个 StringBuilder 对象。new StringBuilder() */
         9: new           #5                  // class java/lang/StringBuilder
        12: dup
        /* 调用了特殊方法,StringBuilder."<init>":()V ,init就表示构造方法,无参圆括号就是表示该构造方法是无参构造。V表示void,没有返回值 */
        13: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
        /* 表示把 s1 加载进来了。可以看到上面有个 astore_1 ,这里 aload_1 取得就是它 store 进来的值。
           astore_1 就是把 s1 存入了局部变量表中的 Slot 为 1 的位置。可以看到下面 LocalVariableTable 有披露。 */
        16: aload_1
        /* aload_1 拿到的值就作为参数被传到了下面的这个 append 方法里 。new StringBuilder().append("a") 。
           方法:(参数列表)返回值类型 */
        17: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        /* 把 s2 加载进来了,用于传给下一行指令的 append 方法 */
        20: aload_2
        /* new StringBuilder().append("a").append("b") */
        21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        /* 最后调用了 StringBuilder 的 toString 方法。 new StringBuilder().append("a").append("b").toString();
           StringBuilder 的 toString() 方法就是 new 了一个 String。
           @Override
           public String toString() {
               return new String(value, 0, count);
           }
            */
        24: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        /* 把最后的到的结果存入 Slot 为 4 的局部变量中,即存到了 s4 中。 */
        27: astore        4
        29: return
      LineNumberTable:
        line 22: 0
        line 23: 3
        line 24: 6
        line 25: 9
        line 29: 29
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      30     0  args   [Ljava/lang/String;
            3      27     1    s1   Ljava/lang/String;
            6      24     2    s2   Ljava/lang/String;
            9      21     3    s3   Ljava/lang/String;
           29       1     4    s4   Ljava/lang/String;
    MethodParameters:
      Name                           Flags
      args

根据以上的分析我们就知道实际上代码背后做的事情是这样的

public class Demo1_22 {
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString(); StringBuilder 的 toString() 就是 new String()
        String s5 = "ab";

        // == 号比较的是引用地址,s3 的引用地址是串池的地址,而 s4 就是在堆空间里 new 出来的对象然后在堆空间里进行的一系列操作,引用的地址就是堆空间中的地址,所以 s3 和 s4 的引用地址必然不相等。
        System.out.println(s3 == s4); // false
        
        System.out.println(s3 == s5); // true 。直接字符串常量赋值的s5,其引用地址就是引用的串池,而串池里每个不同的字符串都是唯一的,所以引用地址和s3是相等的。
        
        System.out.println(s3.equals(s4)); // true。比较的是字符串内容,是相等的。
    }
}

30-30_StringTable编译期优化

当字符串常量拼接时,又发生了什么?

public class Demo1_22 {
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString(); StringBuilder 的 toString() 就是 new String()
        String s5 = "a" + "b"; // javac 在编译时进行了优化,"a" 和 "b" 都是常量,拼接的结果同样也是常量,所以在编译期间被优化成一个字符串常量 "ab"。这种优化只适用于字符串常量,如果有变量参与到字符串的拼接中,优化就不会发生。而上一行是两个变量的拼接,变量引用的值后续可能还会被修改,结果是不能确定的,所以必须在运行期间使用 StringBuilder 来动态地创建。
        
        System.out.println(s5 == s3); // true。
    }
}

看看反编译class文件的字节码内容
在这里插入图片描述

31-31_StringTable字符串延迟加载

字符串对象的加载是延迟的,需要用到的时候,比如要打印,或者要赋值时,才会去串池中找有没有这个字符串对象。当串池中没有才会新建一个字符串对象并加入串池。串池中已有,就直接用了,而不会新建对象加入串池。

32-32_StringTable_intern_1.8

在这里插入图片描述

intern方法可以主动将串池中还没有的字符串对象放入串池。不管串池中是否已存在该字符串对象,都会返回这个字符串对象。

当串池中还不存在即将被放入的字符串:

public class Demo1_22 {
   public static void main(String[] args) {
       String s = new String("a") + new String("b");
       // 此时串池已经放入了 "a" 和 "b"。StringTable["a","b"]
       // 堆中有3个对象,new String("a"), new String("b"), new String("ab")。其中 new String("ab") 就是 s 变量
       // 此时串池中是还没有 "ab" 的,它只存在于堆中

       String s2 = s.intern();
       // 将这个字符串对象 s 尝试放入串池,串池如果已经存在 "ab" 对象,则 s 不会被放入。
       // 当串池中还没有 "ab" 时,将把 s 放入串池。注意这是因为 JDK7 以上的版本,字符串常量池是在堆内存中的,所以直接就把 s 对象放入了串池,这也是为什么 s == "ab" 返回了 true,两者是同一个对象,引用地址也是相同的。
       // 当串池中已有 "ab" 时,intern() 返回的就是这个 "ab" 对象,此时这个对象和 s 是两个不同的对象。

       System.out.println(s2 == "ab"); // true
       System.out.println(s == "ab"); // true
    }
}

当串池中已经存在即将被放入的字符串

public class Demo1_22 {
    public static void main(String[] args) {
        String x = "ab";
        String s = new String("a") + new String("b");

        String s2 = s.intern();
        // 将这个字符串对象 s 尝试放入串池,串池如果已经存在 "ab" 对象,则 s 不会被放入。
        // 当串池中已有 "ab" 时,intern() 返回的就是这个 "ab" 对象,此时这个对象和 s 是两个不同的对象。

        System.out.println(s2 == "ab"); // true
        System.out.println(s == "ab"); // false

        System.out.println(s2 == x); // true
        System.out.println(s == x); // false
    }
}

33-33_StringTable_intern_1.6

在这里插入图片描述

在JDK6或者JDK1.6版本或者它们之前的版本里,intern方法将字符串放入串池的时候,如果串池中没有这个字符串对象的话,会复制一个副本放进串池,而不是把对象本身放进串池。这个特性就会导致下方的输出现象。

public class Demo1_22 {
   public static void main(String[] args) {
       String s = new String("a") + new String("b");
       String s2 = s.intern();
       // 此时串池中还没有 "ab" 时,在jdk1.6或者jdk6及之前的版本下,会将 s 对象拷贝一份,将拷贝放入串池。然后返回这个拷贝出来的对象。这个对象和 s 并不是同一个对象。

       String x = "ab";
       
       System.out.println(s2 == "ab"); // true
       System.out.println(s == "ab"); // false
       
       System.out.println(s2 == x); // true
       System.out.println(s == x); // false
    }
}

34-34_StringTable面试题

public class Demo1_22 {
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "a" + "b";
        String s4 = s1 + s2;
        String s5 = "ab";
        String s6 = s4.intern();

        // 问
        System.out.println(s3 == s4); // false。一个在堆里,一个是串池对象
        System.out.println(s3 == s5); // true
        System.out.println(s3 == s6); // true。s6 是返回的串池对象

        String x2 = new String("c") + new String("d");
        String x1 = "cd";
        x2.intern();
        // 问
        System.out.println(x1 == x2); // false。x2 仍然是堆里的对象。

        // jdk 1.8
        String x4 = new String("e") + new String("f");
        x4.intern();
        String x3 = "ef";
        // 问
        System.out.println(x3 == x4); // true。x4调用intern方法把自己放进了常量池。可以理解为 x3 引用的就是 x4

        // jdk1.6
        String x6 = new String("g") + new String("h");
        x6.intern();
        String x5 = "gh";
        // 问
        System.out.println(x5 == x6); // false。jdk1.6中,x6调用intern方法把自己的一份拷贝放进了常量池。可以理解为 x5 引用的是这份拷贝,而不是 x6 本身
    }
}

35-35_StringTable位置

在这里插入图片描述

从JDK1.7开始,StringTable 从永久代转移到了堆里。原因是永久代内存回收效率很低,Full GC 时才会触发永久代的垃圾回收,而 Full GC 得等到老年代的空间不足才会触发,触发时机属于比较晚。导致 StringTable 的回收效率很低。而 Java 项目里字符串对象的数量是很庞大的,需要高效的垃圾回收效率来配合。

堆里的 StringTable 的垃圾回收只需要 Minor GC 就可以触发。

=====
Full GC(老年代垃圾回收)是指对整个堆进行垃圾回收的一种GC方式。在Java虚拟机中,当新生代和老年代都需要进行垃圾回收时,JVM会先进行Minor GC,然后再进行Full GC。Full GC的回收效率相对较低,涉及到更多的内存空间,因此开销比较大,对应用程序的性能影响也比较明显。Full GC的频率较低,一般情况下,应尽量避免触发Full GC。

=====
Minor GC(新生代垃圾回收)是JVM对年轻代进行垃圾回收的一种GC方式。在Java虚拟机中,对象被分为三代,其中新生代中存放的是大量的短命对象。在Minor GC中,JVM只会对新生代进行垃圾回收,而老年代则不会受到影响。因为新生代中的对象很容易就被创建出来,也很容易就死亡,所以Minor GC会比较频繁地触发。相对于Full GC,Minor GC的回收效率更高,回收速度也更快。

=====
Mixed GC(混合垃圾回收)是指同时对新生代和老年代进行垃圾回收的一种GC方式。与Minor GC相比,Mixed GC需要回收更多内存,因此回收时间更长。但Mixed GC也比Full GC更加高效,因为Full GC需要扫描整个堆,而Mixed GC只需要扫描部分堆空间即可完成回收。

=====
需要注意的是,具体使用哪种GC方式,取决于JVM本身的实现以及应用程序的特点。未来的JVM版本中也可能会引入新的GC方式或优化现有方式,以进一步提升性能和效率。

36-36_StringTable位置

设计案例证明 jdk1.6 和 jdk1.8 的 StringTable 的存放位置不同。
1、不断向 StringTable 里放入大量的字符串对象。
2、用一个长时间存活的对象来引用 StringTable 中的这些对象。
3、StringTable 空间急剧上升,将触发空间不足。
4、jdk1.6 环境下就会触发永久代空间不足。(PermGen)
5、jdk1.7/jdk1.8 环境下就会触发堆空间内存不足。

jdk1.6 案例
在这里插入图片描述

jdk1.8 案例
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

以上两个案例就证明了,在 jdk1.7 之后,StringTable 确实是被放在了堆空间内的。
而在 jdk1.6 及之前,StringTable 是被放在 PermGen 老年代之中的。

37-37_StringTable垃圾回收

下面通过案例证明 StringTable 是可以被垃圾回收的。

/**
 * 演示 StringTable 垃圾回收
 * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
 * 堆空间分配 10m
 * 打印字符串表的统计信息。可以看到字符串常量池中,字符串实例的个数、占用空间大小等等的信息
 * 打印垃圾回收的一些详细信息
 */
public class Demo1_6 {
    public static void main(String[] args) {
        int i = 0;
        try {

        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }
    }
}

执行 main 方法,看控制台输出结果
在这里插入图片描述

改源码后执行一下,再看看控制台输出

/**
 * 演示 StringTable 垃圾回收
 * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
 * 堆空间分配 10m
 * 打印字符串表的统计信息。可以看到字符串常量池中,字符串实例的个数、占用空间大小等等的信息
 * 打印垃圾回收的一些详细信息
 */
public class Demo1_6 {
    public static void main(String[] args) {
        int i = 0;
        try {
            // 添加 100 个字符串对象到 StringTable 里
            for (int j = 0; j < 100; j++) {
                String.valueOf(j).intern();
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }
    }
}

在这里插入图片描述

增加添加到 StringTable 里的字符串对象的数量到十分大的数量,看看是否能触发垃圾回收。

/**
 * 演示 StringTable 垃圾回收
 * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
 * 堆空间分配 10m
 * 打印字符串表的统计信息。可以看到字符串常量池中,字符串实例的个数、占用空间大小等等的信息
 * 打印垃圾回收的一些详细信息
 */
public class Demo1_6 {
    public static void main(String[] args) {
        int i = 0;
        try {
            // 添加 40000 个字符串对象到 StringTable 里
            for (int j = 0; j < 40000; j++) {
                String.valueOf(j).intern();
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }
    }
}

在这里插入图片描述

38-38_StringTable 调优

StringTable 底层是个 HashTable 。一个 HashTable 如果桶的个数比较多,那么表里面值的分布集中度就会相对分散,哈希碰撞的几率就会降低,链表长度相对较短,查链表的次数也会减少,查找的速度相对就更快。
如果桶的个数比较少,哈希碰撞的几率就会提高,链表长度相对较长,且查链表的次数就会增加,查找速度就会下降。

StringTable 调优主要就是调整 StringTable 的 size,也就是调整桶的个数。
使用的 VM Option 指令:

-XX:StringTableSize=100000
# 默认 StringTableSize 是 60013

向串池中放入字符串常量对象时,要首先查询池中是否已经有相同的字符串常量。这个查询操作就是查哈希表。当哈希表的数组的 size 越大,值分布的集中度就越分散,发生的哈希碰撞次数就越少,查询速度就越快。所以加大 StringTable 的 size 就会提高查询串池的速度。

所以如果项目里字符串常量个数非常多,则可以适当调高 StringTable 的 size 大小。

在Java 9及之后的版本中,-XX:StringTableSize这个JVM参数已经被废弃了。这是因为Java 9对StringTable进行了优化,采用了哈希桶的方式存储字符串对象,使得其大小可以根据实际需要动态增长。这样可以更好地适应不同规模的应用程序和字符串使用场景,提高性能和效率。

===
在Java 8以及之前的版本中,-XX:StringTableSize参数仍然有效,可以用于设置StringTable的大小。默认情况下,StringTable的大小为1009。如果想要优化字符串的使用和驻留,可以通过适当调整此参数来提高性能。

===
总之,从Java 9开始,-XX:StringTableSize这个JVM参数已经不再有实际作用,可以省略。

39-39_StringTable调优

考虑是否将字符串对象入池。入不入池对内存占用的对比。

40-40_StringTable调优

设计案例证明上面的猜想。

List<String> list = new ArrayList<>();
List<String> listOfWords = 48万个单词;
String s = null;

// 方式 1
for(String s : listOfWords){
    list.add(s);
}
// 方式 2
for(String s : listOfWords){
    list.add(s.intern());
}

实践证明 方式2 的内存占用是明显小于 方式1 的。
因为 方式1 里的 list 里的所有对象都是堆空间中的,即使重复的字符串对象,也都实在地各自占用了一份内存。
而 方式2 里的 list 里 add 的元素是来自于串池,如果有字符串内容相同的对象,就会引用到同一个串池里的对象,不会额外占用到堆里的更多内存。

stop 2023-06-11_17:55:37

41-41直接内存

start 2023-06-12_18:56:15

直接内存是操作系统的内存。
在这里插入图片描述

42-42直接内存基本使用

直接内存经常用在NIO操作中充当数据的缓冲区。
在这里插入图片描述
如上图,在不使用直接内存时,系统内存读到磁盘文件之后,还需要进行一次内容的拷贝,拷贝到 java 的缓冲区,才可以被java操作。

如下图,在使用直接内存之后,系统内存和java堆内存之间有一篇空间是可以被双方直接访问的,所以系统内存读到磁盘文件之后,并不需要再进行一次拷贝数据的操作,整体的运行效率就提升了。
在这里插入图片描述

43-43直接内存内存溢出

演示直接内存溢出

/**
 * 演示直接内存溢出
 */
public class Demo1_10 {
    static int _100M = 1024 * 1024 * 100;

    public static void main(String[] args) {
        List<ByteBuffer> list = new ArrayList<>();
        int i = 0;
        try {
            while (true) {
                ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100M);
                list.add(byteBuffer);
                i++;
            }
        } finally {
            System.out.println(i);
        }
    }
}

输出结果,报OOM了,提示 Direct buffer memory,直接内存溢出。
在这里插入图片描述

44-44直接内存释放原理

设计代码观察直接内存释放。在任务管理器的进程标签进行观察。
在这里插入图片描述
观察到确实在 byteBuffer = nullSystem.gc() 执行之后,最开始 java 占用的 1g 内存被释放了。

45-45直接内存释放原理

直接内存的分配和释放是通过 unsafe 对象来管理的。如下代码可以验证。

public class Demo1_27 {
    static int _1GB = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        Unsafe unsafe = getUnsafe();
        // 分配内存
        long base = unsafe.allocateMemory(_1GB); // 返回的是分配到的内存的地址
        unsafe.setMemory(base,_1GB, (byte) 0);
        System.in.read();

        // 释放内存
        unsafe.freeMemory(base);
        System.in.read();
    }

    private static Unsafe getUnsafe() {...}
}

直接内存不会自动释放,只有手动调用 unsafe 的 freeMemory() 方法才能释放

46-46直接内存释放原理

直接内存的释放借助了Java中的一个虚引用机制。

47-47直接内存禁用显式回收对直接内存的影响

在这里插入图片描述
JVM调优时,一般会加入一条虚拟机参数。这条参数会使显示声明的 System.gc() 失效。

-XX:+DisableExplicitGC # 禁用显示垃圾回收

在这里插入图片描述
原因是 System.gc() 是一种 Full GC,执行时间很长,会十分影响性能。

Full GC(老年代垃圾回收)是指对整个堆进行垃圾回收的一种GC方式。在Java虚拟机中,当新生代和老年代都需要进行垃圾回收时,JVM会先进行Minor GC,然后再进行Full GC。Full GC的回收效率相对较低,涉及到更多的内存空间,因此开销比较大,对应用程序的性能影响也比较明显。Full GC的频率较低,一般情况下,应尽量避免触发Full GC。

但是在禁用显示垃圾回收之后,上图代码里分配的直接内存就必须等到jvm真正执行垃圾回收之后才能被释放,因此这条 -XX:+DisableExplicitGC # 禁用显示垃圾回收 的指令确实会对直接内存的释放有影响的。

因此,对于较多使用直接内存的情况下,在使用结束之后强烈建议手动调用 unsafe.freeMemory() 来对直接内存进行释放,以避免可能出现的系统内存 OOM。

48-01垃圾回收概述

在这里插入图片描述

49-02判断垃圾引用计数

引用计数法的简单介绍:
某个对象被引用1次,就给它+1,被引用 n 次,就给它+n。如果某次引用结束了,不再引用这个对象了,就-1…等等,以此类推,当某个对象的被引用次数变为 0 之后,就表明可以被回收了。
但是也存在一个问题,如果两个对象互相引用,循环引用等等,就会导致这两个对象永远都不会被回收,导致OOM,所以也有弊端。

50-03判断垃圾可达分析

可达性分析算法简单介绍:
根对象,肯定不能被垃圾回收的对象。
在垃圾回收之前,会对堆中的所有对象进行一次扫描,检查每一个对象是否被根对象直接或间接地引用。如果是,这个对象就不能被垃圾回收。如果一个对象并没有被根对象直接或简介地引用,那么这个对象就可以被垃圾回收。

51-04判断垃圾可达分析根对象

在这里插入图片描述

# -dump 获得堆内存情况的快照转储成一个文件输出
# format 转储文件的格式
# b 表示二进制格式
# live 表示抓快照时只关注存活的,过滤掉已经被GC的。并且live会使抓快照之前进行一次GC
# file 快照存为哪个文件。file=1.bin 表示存在当前目录下的1.bin里
# pid 进程id。这里举例 pid 是 21384
jmap -dump:format=b,live,file=1.bin 21384

在当前活动线程执行过程中,局部变量所引用的对象,是可以作为根对象的。
在这里插入图片描述
方法的参数引用的字符串数组对象 String[] 也是根对象。

堆分析工具 MAT,可以用来查看根节点。要传入上面 dump 下来的堆快照
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • System Class 系统类,由启动类加载器加载的类。
  • Native Stack ,Java 有时候需要调用操作系统方法,操作系统方法在执行时所引用的 Java 对象也是作为根对象。
  • Thread ,一些活动线程,也是作为根对象。线程正在运行,回收对象了线程就无法运行了。
  • Busy Monitor ,被加锁的对象这一类,也作为根对象。如果加锁的对象被回收了,锁就再也不能被释放了。

52-05五种引用强软弱

start 2023-06-13_17:57:09
在这里插入图片描述
先来说说强引用

下图中实线箭头表示强引用
在这里插入图片描述
如上图,A1 对象是被 C对象 和 B对象 这两个根对象强引用的,这种情况下,A1对象就不会被GC回收。
如下图,C对象和B对象对A1对象的强引用断掉以后,此时没有根对象直接或间接引用A1对象,A1对象就可以被回收了。
在这里插入图片描述
软引用
当某个对象没有被强引用,只被软引用时,那么它满足一定条件就会被GC垃圾回收。这个条件是当垃圾回收时,内存不够了,那么软引用的对象就会被回收。

弱引用
只要发生垃圾回收,不管内存够不够,弱引用都会被回收。

软引用和弱引用还可以配合引用队列来进行进一步的垃圾回收。
首先"软引用"和"弱引用"这两个东西自身也是一个对象,自身也占用一定的内存。
C对象通过"软引用"对象软引用了A2,通过"弱引用"对象弱引用了A3对象。当A2和A3都被垃圾回收之后,如果"软引用"对象和"弱引用"对象在被创建时,同时分配了一个引用队列,分别是软引用队列和弱引用队列。
那么它们所引用的对象被回收时,"软引用"和"弱引用"就会进入各自的队列。
如果想对"软引用"和"弱引用"的内存做释放,就要通过引用队列来找到它们。
在这里插入图片描述

53-06五种引用虚终

在这里插入图片描述
注意,软引用和弱引用可以不配合引用队列来使用,但是虚引用和终结器引用必须配合引用队列使用。

虚引用引用的对象被垃圾回收时,虚引用对象自己就会被放入虚引用队列,随后 referenceHandler 线程会定时检查虚引用队列里有没有新入队的对象,有的话就会调用虚引用对象的方法来释放内存。(例如之前的 unsafe.freeMemory() 释放直接内存)。

终结器引用
所有Java对象都继承自Object,Object里有个 finallize() 方法。当某个对象重写了 finallize() 方法,并且这个对象没有被强引用引用它的时候,这个对象就可以被垃圾回收。
当没有强引用引用这个对象时,JVM会创建对应的终结器引用,当这个对象即将被垃圾回收时,这个终结器引用就会被加入终结器引用队列,此时这个对象还没被垃圾回收,由一个优先级很低的 finallizeHandler 线程定时检查终结器引用队列中是否有终结器引用,有的话根据这个引用找到即将被垃圾回收的对象,然后调用这个对象的 finallize() 方法。调用结束之后,这个对象就能被真正地垃圾回收了。
finallize() 方法的工作效率较低,第一次回收时不能真正回收,终结器引用要先入队,再等待优先级很低的线程调用 finallize() 方法,最后才能被回收,所以不推荐使用 finallize() 来释放资源,效率太低。

54-07软引用应用

下图总结上一集的内容
在这里插入图片描述
首先演示不使用软引用的情况,会报错OOM

/**
 * 演示软引用
 */
public class Demo2_3 {
    // 先把配置虚拟机参数限制堆空间为20M。【-Xmx20m】

    private static final int _4MB = 4 * 1024 * 1024;
    public static void main(String[] args) throws IOException {
        List<byte[]> list = new ArrayList<>();
        // 尝试往 list 里放入 20mb 的byte数组
        for (int i = 0; i < 5; i++) {
            list.add(new byte[_4MB]);
        }
        // 阻塞进程,等待控制台输入回车
        System.in.read();
    }
}

控制台输出结果,堆空间溢出
在这里插入图片描述
下面使用软引用,然后配置虚拟机参数观察GC过程。

/**
 * 演示软引用
 */
public class Demo2_3 {
    // 先把配置虚拟机参数限制堆空间为20M。【-Xmx20m】

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

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

    public static void soft() {
        // list 强引用了一个软引用对象,软引用对象引用了 byte数组
        // 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() : " + list.size());
        // 输出 list 看看内容
        for (SoftReference<byte[]> ref : list) {
            System.out.println("ref.get() : " + ref.get());
        }
    }
}

先不开GC过程,直接看看控制台输出结果
在这里插入图片描述
配置 JVM 参数,然后再次运行程序看看控制台输出结果

-Xmx20m -XX:+PrintGCDetails -verbose:gc

在这里插入图片描述
第3次循环时,内存已经紧张,触发了一次MinorGC,把新生代从2397K的占用回收到了剩余500K占用。
第4次循环时,内存还是不够,又触发了一次MinorGC,回收效果不明显,然后触发了一次Full GC。发现回收效果还是不好。最后触发了软引用的内存回收,可以看到最后一次垃圾回收之后,新生代的占用就从 4506K -> 0K 了,老年代的占用也从12691K -> 798K。代价就是把前4个软引用的byte数组对象都回收了。
可以看到下面 list 的输出,前4个元素都是null了。

55-08软引用引用队列

上一集组后的控制台输出中,list 里的前4个软引用对象已经是null了,这些null也没有必要再存在了,毕竟软引用本身也是要占用内存的,怎么把它们也回收了呢?使用引用队列。

看下面源码然后看输出

public class Demo2_3 {
    // 先把配置虚拟机参数限制堆空间为20M。【-Xmx20m】

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

    public static void main(String[] args) throws IOException {
        // list 强引用了一个软引用对象,软引用对象引用了 byte数组
        // list ---> SoftReference ---> byte[]
        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());
        }

        // 从引用队列中获取无用的软引用对象,并将其移除。poll 方法会获得队列中最先入队的对象
        Reference<? extends byte[]> poll = queue.poll();
        while (poll != null) {
            list.remove(poll);
            poll = queue.poll();
        }

        System.out.println("======================");
        // 输出 list 看看内容
        for (SoftReference<byte[]> ref : list) {
            System.out.println("ref.get() : " + ref.get());
        }
    }
}

在这里插入图片描述

56-09弱引用

弱引用所引用的对象在每次GC时都会被回收,无论空间够不够。

/**
 * 演示弱引用
 */
public class Demo2_3 {
    // 先把配置虚拟机参数限制堆空间为20M。【-Xmx20m】

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

    public static void main(String[] args) throws IOException {
        // list 强引用一个弱引用对象,弱引用对象再引用 byte[]
        // list ---> WeakReference ---> byte[]
        List<WeakReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 6; 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() : " + list.size());
    }
}

在这里插入图片描述
能看到弱引用的引用对象只需要一次 Minor GC 就被回收掉了。

看看把for循环次数从6改成10会发生什么?
在这里插入图片描述
最后 list 里有 9 个都是 null 了,这是因为弱引用本身也是个对象,也要占用空间,而它是 list 的强引用,不能被回收,所以因为空间不够就直接把更多的 byte[] 回收了。

要把弱引用对象本身也回收掉,也要结束弱引用队列,具体做法和上一集的软引用队列相关的操作是一样的,这里不做赘述。

stop 2023-06-13_19:40:51

57-10回收算法标记清除

start 2023-06-14_19:32:49

常见的垃圾回收算法有3种

  1. 标记清除
  2. 标记整理
  3. 复制

标记清除:
分2个阶段,第1个阶段先标记,把没有被 GC root 直接或间接引用的对象标记出来。
第2个阶段就是清除。把这个垃圾对象所占用的空间释放掉。这个释放并不会把这部分空间的信息擦除,而是记录起始地址和结束地址或者所占大小的信息,放在空闲地址列表里。下次再给新对象分配空间时,就可以直接从这个列表里找有没有足够的空间来分配给这个新的对象,有的话就进行分配。

标记清除算法的优缺点

  • 优点:速度快
  • 缺点:易产生内存碎片。因为不会擦除内存里垃圾对象的信息,空闲地址列表里的空闲空间分布不连续不集中。
    在这里插入图片描述

58-11回收算法标记整理

标记整理算法:

分2个阶段,第1个阶段先标记,把没有被 GC root 直接或间接引用的对象标记出来,表示它们是垃圾对象。
第2个阶段,整理。将存活下来的对象在内存中依次向前整理其占用地址,使得对内存空间的占用更加连续和紧凑。

标记整理算法的优缺点:

  • 优点:避免产生更多内存碎片,存活对象占用内存比较连续和集中,对内存空间的利用率高。
  • 缺点:效率较低。因为整理过程涉及对象的迁移,地址发生变化,牵连引用这些对象的众多变量的引用地址也要相应变化,所以速度较慢。
    在这里插入图片描述

59-12回收算法复制

复制算法:

会有两个固定大小的空间,一个是FROM空间,另一个是TO空间。TO空间初始时是全部空闲的。
在FROM空间里标记好要清除的垃圾对象之后,将所有剩余未标记要清除的对象全部复制到TO空间里,复制的过程就完成了碎片整理,不会产生内存碎片。
然后删除FROM空间里留下的全部对象(留下的都是垃圾对象)。
最后再交换FROM和TO的位置。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
复制算法的优缺点

  • 也不会产生内存碎片
  • 需要占用双倍空间

60-13回收算法小结

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
实际中,JVM会根据不同的情况来使用不同的垃圾回收算法,并且不会只使用单一的算法,而是多种算法组合使用来进行垃圾回收。

61-14分代回收

分代垃圾回收机制
在这里插入图片描述
为什么要分新生代和老年代?
因为 Java 中有的对象需要长时间使用,这些对象就放在老年代里。而其余的用完就可以回收的对象,就放在新生代。这样就可以针对对象声明周期的不同特点进行不同的垃圾回收策略。老年代的垃圾回收不频繁,而新生代的垃圾回收则比较频繁。

62-15分代回收

当一个新的对象被创建之后,默认会被放入伊甸园空间(Eden)。
当Eden空间逐渐减少,直到剩余的空间放不下新创建的一个对象的时候,就会触发一次垃圾回收。新生代的垃圾回收我们称之为 Minor GC。
此时Eden里会执行一次复制算法进行垃圾回收,去除碎片和交换FROM和TO之后,存活的对象最终被放在了FROM空间里,并且这些对象的寿命被加上了1。Eden里剩余的垃圾对象被回收掉了。
在这里插入图片描述
第1次垃圾回收结束。

第1次垃圾回收结束后,Eden里的空间又充足了,可以继续放入新创建的对象。
当Eden空间又紧张之后,开始触发第2次垃圾回收 Minor GC。此时,会标记Eden里需要存活的对象,并将其放入幸存区TO空间(TO空间在每次复制算法结束之后都是空的),然后对FROM空间里上一轮GC留下来的对象再进行一次筛选,标记需要存活的对象,清除不再需要的对象,将FROM区剩余的存活对象复制到TO区,然后给TO区里所有存活的对象寿命+1。最后交换FROM区和TO区。

幸存区中的对象不会永远留在幸存区,这些对象的寿命超过指定的阈值之后(默认15次),证明该对象价值比较高,可以晋升到老年代区。
在这里插入图片描述
那如果新生代老年代的空间都快满了呢?这时候就会触发 Full GC。
在这里插入图片描述
stop 2023-06-14_20:33:35

63-16分代回收

start 2023-06-14_22:00:55

总结,以及新增3条说明,重点是 STW 的引发

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

老年代空间不足时,会先尝试触发 Minor GC,如果空间仍然不足,那么触发 Full GC,整个过程 stop the world 时间更长。
在这里插入图片描述

64-17_GC相关参数

在这里插入图片描述

65-18_GC分析

通过案例演示垃圾回收的过程。

解释虚拟机参数

-Xms20M # 堆内存最小值
-Xmx20M # 堆内存最大值
-Xmn10M # 新生代大小
-XX:+UseSerialGC # 手动指定垃圾回收器。这个垃圾回收器不会动态调整幸存区比例,而 JDK8 下的会。指定为这个垃圾回收器是为了方便演示。
-XX:+PrintGCDetails # 打印 GC 详情
-verbose:gc # 打印 GC 详情

读一下空跑出来的控制台的一些信息。

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;

    //-Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
    public static void main(String[] args) throws IOException {
    }
}

在这里插入图片描述
Heap 下分了三部分,分别是 def new generation 新生代tenured generation 老年代Metaspace 元空间(事实上元空间不属于堆的一部分,只不过这里由于配置了 +PrintGCDetails 才被打印了出来)。

def new generation 新生代 :观察到新生代空间大小虽然由虚拟机参数指定为了10M,但是显示最大总大小是9M多,为什么?
因为当前幸存区比例是 8:1:1 ,Eden : FROM : TO 分别获得了 8M : 1M : 1M,而 TO 空间默认一直都是空的不能用,所以新生代的最大总大小就去掉了 TO 空间,显示出来一共是 9M。used 空间是 2.6M 左右。后面的是内存地址。
观察到 eden space 8M,from space 1M,to space 1M,以及各自的使用情况,内存地址。

tenured generation 老年代:总大小10M,used 空间是0,目前还没使用。

Metaspace 元空间:使用量情况。

stop 2023-06-14_22:40:09

66-19_GC分析

start 2023-06-15_18:06:50

执行下方代码触发一次新生代回收 Minor GC

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;

    //-Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
    public static void main(String[] args) throws IOException {
        ArrayList<byte[]> list = new ArrayList<>();
        list.add(new byte[_7MB]);
    }
}

观察控制台输出
在这里插入图片描述
解释一下输出内容

[GC (Allocation Failure) [DefNew: 2491K->813K(9216K), 0.0015243 secs] 2491K->813K(19456K), 0.0016315 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
# GC 表示新生代回收,Minor GC
	# 如果是 Full GC 则表示老年代回收。
# [DefNew: 2491K -> 813K(9216K), 0.0015243 secs] 表示发生在新生代,回收前的空间是 2491K ,回收之后的空间是 813K,新生代区域总大小是 9216K,垃圾回收耗费 0.0015243 秒。
# [DefNew: ...] 结束之后,后面的信息是整个堆的信息,包括回收前、回收后的内存占用,回收耗费时间。

另外观察到控制台 GC 信息的下面同样打印出来的 Heap 的内存信息。
在这里插入图片描述
再向 list 里放入 512K,再执行

//-Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
public static void main(String[] args) throws IOException {
    ArrayList<byte[]> list = new ArrayList<>();
    list.add(new byte[_7MB]);
    list.add(new byte[_512KB]);
}

观察控制台输出
在这里插入图片描述
仍然只触发了 Minor GC,eden几乎被放满。

再放入512K

//-Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
public static void main(String[] args) throws IOException {
    ArrayList<byte[]> list = new ArrayList<>();
    list.add(new byte[_7MB]);
    list.add(new byte[_512KB]);
    list.add(new byte[_512KB]);
}

观察控制台
在这里插入图片描述
发生了 2 次 Minor GC,可以看到第二条 [GC .... ] 里打印出来的堆空间信息,8657K->8490K(19456K),回收前和回收后的空间占用几乎不变,并且堆空间已经快满了。这两次 GC 之后,eden 的空间占用就下来了,而新生代空间快不够用了,有部分对象已经晋升到了老年代(内存实在紧张,忽略了阈值直接晋升)。

在这里插入图片描述

67-20_GC分析大对象oom

当新创建的对象的大小已经超过了整个 eden 的总空间,新生代计算发现,即使垃圾回收之后整个新生代清空,空间也不足够容纳这个对象,那么如果老年代空间足够,就会将这个大的对象直接晋升到老年代。这种情况,不会触发垃圾回收。

//-Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
public static void main(String[] args) throws IOException {
    ArrayList<byte[]> list = new ArrayList<>();
    list.add(new byte[_8MB]);
}

观察控制台输出
在这里插入图片描述
确实在不触发 GC 的情况下,直接晋升老年代。

放进去两个 8MB 的对象,就触发 OOM 了。

//-Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
public static void main(String[] args) throws IOException {
    ArrayList<byte[]> list = new ArrayList<>();
    list.add(new byte[_8MB]);
    list.add(new byte[_8MB]);
}

在这里插入图片描述
发生了两次 GC,一次 Minor GC , 一次 Full GC 。注意这里的 Minor GC 其实是触发 Full GC 之前间接触发的。
即使触发了 Full GC 还是空间不够,就直接报 OOM 了。

另外,如果是 new 一个线程去跑这段会触发 OOM 的代码呢?主线程会受到影响吗?


//-Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
public static void main(String[] args) throws IOException, InterruptedException {
    new Thread(() -> {
        ArrayList<byte[]> list = new ArrayList<>();
        list.add(new byte[_8MB]);
        list.add(new byte[_8MB]);
    }).start();

    System.out.println("sleep for 2s...");
    Thread.sleep(2000L);
}

在这里插入图片描述
看到控制台,还是打印出来 sleep for 2s... 了,先执行的触发OOM的线程并没有阻止主线程的控制台输出语句的执行,所以,一个线程内的 OOM 并不会导致整个 Java 进程的意外结束。

68-21垃圾回收器

垃圾回收器

  1. 串行
    • 单线程
    • 堆内存较小,适合个人电脑
  2. 吞吐量优先
    • 多线程
    • 堆内存较大,多核 cpu
    • 让单位时间内,STW 的时间最短。GC 的总时间越少,系统吞吐量越大。
  3. 响应时间优先
    • 多线程
    • 堆内存较大,多核 cpu
    • 尽可能让单次 STW 时间更短。GC 的总时间可以长,但是每次 GC 的时间必须短。

69-22垃圾回收器串行

开启串行垃圾回收期的 JVM 指令

-XX:+UseSerialGC = Serial + SerialOld
# Serial 工作在新生代,采用的回收算法是复制
# SerialOld 工作在老年代,采用的回收算法是标记+整理

新生代和老年代的垃圾回收器是分别运行的。
在这里插入图片描述
触发垃圾回收时,首先要让所有线程在安全点停下来。因为在垃圾回收的过程中,会涉及到对象的移动,对象的地址就会变化,如果其它线程还在运行,并且在用旧的地址找某个对象时,这个对象已经移动到新的地址了,就会发生错误。
另外由于 SerialSerialOld 都是单线程的垃圾回收器,只有一个垃圾回收线程在运行,此时其它的用户线程必须阻塞,等垃圾回收线程结束,其它的用户线程再恢复运行。

70-23垃圾回收器吞吐量优先

# 开启吞吐量垃圾回收器。这个在 JDK1.8 里是默认开启的。
# UseParallelGC 工作在新生代,使用复制算法
# UseParallelOldGC 工作在老年代,使用标记+整理算法
# 注意,开启其中一个,另一个就会跟随着开启。
-XX:+UseParallelGC ~ -XX:+UseParallelOldGC

# 自适应的大小调整策略。调整的是新生代的大小,会动态调整 eden 和 survivor 区的占比。整个堆的大小也会进行调整。包括晋升阈值。
-XX:+UseAdaptiveSizePolicy

# 垃圾回收时间占用总运行时间的比例。公式 1/(1+ratio) ,ratio 默认值是 99。
# 所以默认情况下,垃圾回收的时间占用总时间的比例不能超过 1%
# 假如垃圾回收时间比例达不到 1%,垃圾回收器就会动态扩大堆空间,从而使得垃圾回收发生的频率降低,使得满足垃圾回收时间占用比例。
# 习惯上设置为 19。也就是最终 1/20=0.05
-XX:GCTimeRatio=ratio

# 最大暂停毫秒数。默认值是 200。如果要让每次 GC 的暂停时间更短或保持很短,需要通过降低堆空间大小来满足。这个目的和减少垃圾回收时间占比正好是相反的。
-XX:MaxGCPauseMillis=ms

# 设置垃圾回收线程的数量
-XX:ParallelGCThreads=n

在这里插入图片描述
垃圾回收的线程数和CPU核数是相关的。
在这个垃圾回收器工作时,CPU的工作曲线是下面这样的,N核CPU的每个核心都去进行垃圾回收了,在垃圾回收的过程中CPU的占用率会上升到100%。
在这里插入图片描述
stop 2023-06-15_19:55:06

71-24垃圾回收器响应时间优先

start 2023-06-16_18:03:26
在这里插入图片描述

# UseConcMarkSweepGC即CMS,并发的垃圾回收器,标记清除算法。工作在老年代,与其配合的是 UseParNewGC,工作在新生代。另外 CMS 有时候会发生并发失败,此时就由 SerialOld 做替补来负责老年代的垃圾回收。
-XX:+UseConcMarkSweepGC ~ -XX:UseParNewGC ~ SerialOld

# 并行的GC线程数(默认为CPU核数) ~ 并发的GC线程数,一般设置为并行的1/4
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads

# 执行 CMS 垃圾回收的内存占比
-XX:CMSInitiatingOccupancyFraction=percent

# 在重新标记之前进行一次新生代的GC
-XX:+CMSScavengeBeforeRemark

CMS 对 CPU 的占用并不像吞吐量优先的垃圾回收器那样会100%占用CPU,而是受 -XX:ConcGCThreads=threads 这个配置影响的,如果CPU核心数是4,并且 -XX:ParallelGCThreads=4 ,则 -XX:ConcGCThreads=1 ,那么同一时间CMS最多只会占用 1 个 CPU 核心。但是这个核心会被 GC 一直占用,而留给用户工作线程的 CPU 核心就剩 3 个了,所以这个对整个系统的吞吐量就造成影响了。
在这里插入图片描述
而又因为GC线程工作的同时,其它用户线程仍然在工作,也会同时产生新的垃圾,这些垃圾被称为浮动垃圾。浮动垃圾需要等到下一次垃圾回收时,才能进行清理。如此会带来一个问题,GC时,会有浮动垃圾生成,所以GC不能等到堆内存不足的时候才进行垃圾回收,因为还需要预留空间给其它用户线程产生浮动垃圾。

下面这个命令就用来控制何时进行 CMS 回收的时机

# 执行 CMS 垃圾回收的内存占比
-XX:CMSInitiatingOccupancyFraction=percent
# 如果你想将 -XX:CMSInitiatingOccupancyFraction 设置为 70%,可以将参数设置为
# -XX:CMSInitiatingOccupancyFraction=70,也就是不需要在 percent 后面添加百分号符号 %

假设 -XX:CMSInitiatingOccupancyFraction=80 ,那只要老年代的内存占用达到80%时,执行一次垃圾回收。目的就是为了预留空间给浮动垃圾。早期JVM的这个值默认是65。

在重新标记的阶段有一个特殊场景,有可能新生代的对象会引用老年代的对象,但是这些新生代对象里会有相当部分是要作为垃圾被回收的,也就是重新标记阶段要扫描的很多新生代对象很快就被回收了,扫描工作相当于白做,还影响效率。所以在重新标记阶段可以先先进行一次新生代的GC,回收掉一部分对象,减少扫描对象的数量,减轻重新标记时的压力,提高效率。

# 在重新标记之前进行一次新生代的GC
-XX:+CMSScavengeBeforeRemark

另外,由于 CMS 是一种标记+清除的算法,会产生内存碎片,当产生的内存碎片比较多的时候,就容易发生空间不足导致并发失败。此时 CMS 垃圾回收器就不能正常工作了。此时老年代的垃圾回收器就会从 CMS 退化为 SerialOld 回收器,进行一次单线程的串行的垃圾回收,进行整理内存碎片。这也是 CMS 垃圾回收器的最大问题,内存碎片过多,导致并发失败,垃圾回收器退化为 SerialOld,GC时间会突然变得很久,失去了响应时间优先的优点。

72-25_G1简介

在这里插入图片描述

73-26_G1新生代回收

在这里插入图片描述
在这里插入图片描述
每个 region 都可以被作为 eden 或者 survivor 或者 tenured generation 。
随着系统运行,逐渐的 eden 越来越多,就开始触发一次 Young Collection,会触发一次 STW 。
在这里插入图片描述
Young Collection 时,会以复制算法将幸存对象放入 survivor 区域。
在这里插入图片描述
再过了一段时间,survivor 越来越多,或者 survivor 里的某些对象超过一定存活时间,幸存区里的这部分对象就会晋升到老年代,年龄还不够的就会再次拷贝到一个 survivor 里继续存放。

74-27_G1新生代回收+CM

新生代的回收和并发标记阶段。CM 表示 ConcurrentMarking
在这里插入图片描述
图里 O 表示 Old Generation。

初始标记表示要找到根对象。
并发标记表示从根对象出发,顺着引用链找到所有要被标记的对象并标记。

75-28_G1混合回收

混合收集阶段。
在这里插入图片描述
此时并不是所有 O 区域都会进行复制算法回收,
而是被 G1 回收器根据最大暂停时间 -XX:MaxGCPauseMillis=ms 来有选择的对回收价值最高的部分 O 区域进行复制回收。

76-29_G1_FullGC概念辨析

在这里插入图片描述
G1垃圾回收器对老年代回收时的阈值默认是45%(即老年代内存占比整个堆内存达到45%时),触发并发标记和混合收集阶段。这两个阶段进行的过程中,如果回收速度高于新的用户线程产生的垃圾时,此时还不是 Full GC,还处于并发垃圾收集的阶段。
那 G1 什么时候会触发 Full GC?当垃圾回收的速度跟不上垃圾产生的速度,这时并发收集失败,退化为串行收集,进入 Full GC。

77-30_G1新生代跨代引用

新生代垃圾回收时的跨代引用问题。

回忆新生代垃圾回收的过程:
找到根对象,可达性分析,找到存活对象,复制存活对象到幸存区。
找根对象时,有一部分来自老年代,通常老年代存活对象非常多,通过遍历老年代找根对象效率十分低,因此采用卡表技术。

卡表技术CardTable把老年代区域进行细分,分成一个个Card,每个Card大约512K。
如果老年代其中有一个对象,引用了新生代的对象,那么对应的Card就被标记为脏Card。
将来做GC Root 遍历的时候就不需要再遍历整个老年代了,只需要关注脏Card即可,减少搜索范围。
在这里插入图片描述
图里粉红色表示脏Card区。
新生代这边会有一个 Remembered Set,里面记录 Incoming Reference,外部对自身的一些引用(记录有哪些脏Card)。将来去 eden 做垃圾回收时,先通过 Remembered Set 得到脏Card,再到脏Card去遍历 GC Root,非脏Card就暂时可以不遍历,所以这样就可以减少 GC Root 的遍历时间了。

标记脏Card是通过 post-write barrier ,在每次对象的引用发生变更时,都要去更新脏Card。这是异步操作,不会马上执行,把更新的指令放在队列 dirty card queue 中,由未来的线程去完成脏Card的更新操作。

78-31_G1_remark

重新标记阶段。

在这里插入图片描述
图里表示并发标记阶段时,对象的处理状态。
黑色表示已经处理完成,并且有在被引用着的对象,这些对象最后会被存活下来的对象。
灰色表示正在处理中,白色表示尚未处理。图里的灰色和下面被箭头指向的白色最终也还是都会变成黑色。而独立的白色没有被引用的,最终就会变成垃圾被回收。

如果初始标记阶段结束之后,有对象的引用发生了变化,那么就会触发执行 pre-write barrier 指令,pre-write barrier 会把这个对象加入 satb_mark_queue 队列中,等到并发阶段结束,触发STW,进入 remark 阶段,来对队列进行重新检查和标记,避免回收掉了引用状态发生变化使得本来是垃圾对象缺重新变回强引用的对象。

# 写屏障
pre-write barrier
# SATB 是 Snapshot-At-The-Beginning 的缩写,表示当前快照是在开始时创建的。
satb_mark_queue

在这里插入图片描述
stop 2023-06-16_20:26:50

79-32_G1字符串去重

start 2023-06-19_10:04:35
在这里插入图片描述
JDK8 中,String 底层使用 char 数组存储每个字符。

80-33_G1类卸载

在这里插入图片描述

# 类卸载的JVM指令,默认是开启的
-XX:+ClassUnloadingWithConcurrentMark

Java中的类加载器可以分为以下四种:

  1. 启动类加载器(Bootstrap ClassLoader):也称为根加载器,它是Java虚拟机的一部分,并负责加载Java运行时环境所需的基础类,如java.lang等。
  2. 扩展类加载器(Extension ClassLoader):也称为系统加载器,它是用来加载Java运行时环境扩展库的类,如javax等。
  3. 应用程序类加载器(Application ClassLoader):也称为用户自定义类加载器,它负责加载应用程序中自己编写的类。
  4. 自定义类加载器(Custom ClassLoader):开发者可以通过继承 java.lang.ClassLoader 类,来自定义加载器,实现特定功能。

这些类加载器按照顺序构成一个层次结构,每个加载器都有一个父类加载器。当类加载器需要加载一个类时,它首先将这个任务委托给父加载器进行处理。如果父加载器无法完成加载任务,子加载器才会尝试加载该类。使用这种委派模型,可以保证Java类的全局唯一性和安全性。

一般情况下,启动类加载器、扩展类加载器、应用程序类加载器都不会被卸载,只有自定义类加载器是有被卸载的需求。

81-34_G1巨型对象

在这里插入图片描述
JDK 8u60 版本增强了一项功能,可令 G1 回收巨型对象。

G1(Garbage-First)垃圾收集器的区域划分可以分为以下几种:

  1. Humongous 区:一个对象占用了连续的大块空间(>1MB),那么这个对象被放入 Humongous 区。当一个 Humongous 区没有足够的连续空间去分配一个大对象时,就会触发 Full GC。
  2. Eden 区:新生成的对象首先都是分配在 Eden 区,它是堆的一部分,当 Eden 区的空间满了之后,就会触发 Minor GC。
  3. Survivor 区:当 Eden 区进行一次Minor GC 后,Eden 中还存活的对象,会被复制到 Survivor 区。Survivor 区一般有2个,每次只有其中一个是被使用的,另外一个是空闲的。
  4. Old 区:当 Survivor 区中对象经历了多次垃圾回收仍然存活时,就会晋升到 Old 区。Old 区主要存储应用程序生命周期较长的对象(比如静态变量、大数组等)。当 Old 区空间不足或者有大对象需要分配时,就会触发 Full GC 。

G1 通过将 Heap 等分成多个大小相同的Region,可以实现上述不同区域的划分。其中,Humongous 区是跨多个 Region 的大对象区,Eden、Survivor 和 Old 区则是只涉及同一个 Region 的标准区域。G1 将这些 Region 组成了一棵树,Tree 的根节点是Heap,叶子节点是Region,它们通过指针相连接。每个 Region 的大小通常为1MB到32MB不等,它们的空间布局会影响着 G1 的收集性能和效率。

成为巨型对象的条件是默认是其大小大于Region的一半。
巨型对象在内存中的分布如下
在这里插入图片描述

解释一下下面这句话
在这里插入图片描述
老年代有CardTable,引用了巨型对象的CardTable会被标记脏卡。
当某个巨型对象从老年代的引用为0时,这个巨型对象就可以在新生代的垃圾回收时被回收掉。

82-35_G1动态调整阈值

在这里插入图片描述

83-36_G1小结

start 2023-06-25_10:55:15
在这里插入图片描述

84-37_GC调优

在这里插入图片描述
查看虚拟机运行参数,在cmd里执行。

"D:\DevelopTools\java\jdk_8u341\bin\java"  -XX:+PrintFlagsFinal -version | findstr "GC"

85-38_GC调优

在这里插入图片描述
在这里插入图片描述
高吞吐量的垃圾回收器:ParallelGC。
低延迟的垃圾回收器:CMS、G1、ZGC。(JDK9中不再推荐使用CMS)。
G1是集ParallelGC和CMS优点的垃圾回收器,适合管理超大堆内存。既可以实现低延迟,也可以设置吞吐量为目标或者设置响应时间为目标。将来可能会取代CMS。
ZGC是JAVA12推出的处于体验阶段的垃圾回收器,目标是低延迟,实现极低的延迟。
以上是Oracle的Hotshot虚拟机的选择。另外还有一个比较出名的虚拟机叫做Zing,它的垃圾回收效率对外宣称是0停顿,几乎没有STW,并且也可以管理超大内存。

86-39_GC调优

在这里插入图片描述
Java中的Object至少占用16个字节。

87-40_GC调优新生代

start 2023-06-25_20:08:39

新生代垃圾回收的复制算法,其耗费时间多的地方就是复制的过程。需要将对象复制到幸存区,并且在幸存区里还要进行FROM和TO空间之间的复制,还要修改对这些对象的引用。
在这里插入图片描述
部分内容解释

  • 新生代的特点
    • 所有的 new 操作的内存分配非常廉价。
      TLAB thread-local allocation buffer,每个线程都有自己局部的私有的分配缓冲区,每当有new操作时,会先去TLAB检查还有没有空间,有的话就直接分配了。

88-41_GC调优新生代

在这里插入图片描述
新生代的初始空间是否越大越好?
不是。如果新生代空间很小,那么将发生频繁的 Minor GC。如果新生代空间很大,那么相应的老年代空间就被压缩了,老年代空间不足就会发生频繁的 Full GC,STW 时间更久。Oracle 建议新生代空间的大小在 堆空间大小的 25% 到 50% 之间。

下图是新生代空间大小和系统吞吐量的关系图,
吞吐量表示单位时间能相应的请求数。
在这里插入图片描述

89-42_GC调优新生代

在这里插入图片描述
理想情况下,新生代能容纳所有【并发量*(请求->响应)】的数据。
解释:一次请求和响应所产生的所有数据,乘以并发量,得到的总数据量,理想情况下应该能被新生代所容纳。

因为新生代的大部分对象存活时间都很短,很快就会被回收了。所以这个空间基本上就足够使用了。

90-43_GC调优新生代幸存区

在这里插入图片描述
幸存区中有两类对象,第一类对象生命周期较短,可能下个周期就被回收了,但当前还在使用该对象,所以就留在幸存区了。第二类对象将来是要晋升到老年代的,但当前寿命还没达到阈值。

幸存区的大小就需要大到足以容纳这两类对象。
如果幸存区空间太小,那么JVM就不得不动态调整阈值,这样就使得幸存区里部分存活时间较短的对象错误地晋升到了老年代中,从而挤压了老年代的空间,让部分真正需要长时间存活的对象迟迟无法进入老年代。

91-44_GC调优新生代幸存区

在这里插入图片描述

# 最大晋升阈值
-XX:MaxTenuringThreshold=threshold

# 显示晋升的详细信息,便于调整最大晋升阈值
-XX:+PrintTenuringDistribution

stop 2023-06-25_21:18:13

92-45_GC调优老年代

93-46_GC调优案例1

94-47_GC调优案例2

95-48_GC调优案例3

96-01-类加载-概述

97-02-类文件结构

98-03-类文件结构-常量池1

99-04-类文件结构-常量池2

100-05-类文件结构-常量池3

101-06-类文件结构-访问标识和继承信息

102-07-类文件结构-field

103-08-类文件结构-method-init

104-09-类文件结构-method-main

105-10-类文件结构-附加属性

106-11-字节码指令-init

107-12-字节码指令-main

108-13-javap

109-14-图解运行流程-准备

110-15-图解运行流程-a赋值

111-16-图解运行流程-剩余

112-17-练习-分析a++

113-18-字节码指令-条件判断

114-19-字节码指令-循环控制

115-20-练习-分析x=0

116-21-字节码指令-cinit

117-22-字节码指令-init

118-23-方法调用

119-24-多态原理-HSDB

120-25-多态原理-查找类

121-26-多态原理-vtable

122-27-多态原理-小结

123-28-异常-catch

124-29-异常-多个catch

125-30-异常-multicatch

126-31-异常-finally

127-32-finally-面试题1

128-33-finally-面试题2

129-34-synchronized

130-35-语法糖-默认构造

131-36-语法糖-自动拆装箱

132-37-语法糖-泛型擦除

133-38-语法糖-泛型反射

134-39-语法糖-可变参数

135-40-语法糖-foreach

136-41-语法糖-switch-string

137-42-语法糖-switch-enum

138-43-语法糖-枚举

139-44-语法糖-twr1

140-45-语法糖-twr2

141-46-语法糖-重写桥接

142-47-语法糖-匿名内部类

143-48-类加载-加载

144-49-类加载-连接-验证

145-50-类加载-连接-准备

146-51-类加载-连接-解析

147-52-类加载-初始化

148-53-类加载-练习1

149-54-类加载-练习2

150-55-类加载器-概述

151-56-类加载器-启动类加载器

152-57-类加载器-扩展加载器

153-58-类加载器-双亲委派-源码分析1

154-59-类加载器-双亲委派-源码分析2

155-60-类加载器-线程上下文1

156-61-类加载器-线程上下文2

157-62-类加载器-自定义

158-63-类加载器-自定义-实现

159-64-运行期优化-逃逸分析

160-65-运行期优化-方法内联

161-66-运行期优化-字段优化

162-67-运行期优化-字段优化

163-68-反射优化-1

164-69-反射优化-2

165-01-JMM-概述

166-02-JMM-原子性-synchronized

167-03-JMM-原子性-synchronized

168-04-JMM-原子性-问题

169-05-JMM-可见性-问题

170-06-JMM-可见性-解决

171-07-JMM-有序性-问题

172-08-JMM-有序性-解决

173-09-JMM-有序性-理解

174-10-JMM-happens-before

175-11-CAS-概述

176-12-CAS-底层

177-13-CAS-原子类

178-14-synchronized-优化

179-15-synchronized-轻量级锁

180-16-synchronized-轻量级锁-无竞争

181-17-synchronized-轻量级锁-锁膨胀

182-18-synchronized-重量级锁-自旋

183-19-synchronized-偏向锁

184-20-synchronized-其它优化

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值