AI摘要
北海のAI

https://www.bilibili.com/video/BV1dt411u7wi/?spm_id_from=333.337.search-card.all.click&vd_source=bed2588951fb9fd0821dd2ef0191e48b

GC垃圾回收机制

GCRoot

被下列这些直接或间接引用的就不能删除

  1. 被栈所引用的
  2. 本地方法栈(C\C++)
  3. 方法区
删除方法
  1. 标记-清理

    像这种的标记之后需要删除,但是会导致-内存碎片的问题:比如现在这两个1KB的内存被清理之后,又来了一个2KB的是不能放到这两个地方去

    image-20250426102435688

  2. 标记-整理

    还是这张图,可以删除后进行往前进位这种整理后就不会导致内存碎片了,但是缺点是代价太大

  3. 复制

    将原始堆内存一分为二,每次只复制没有被标记的对象,但是缺点是需要2倍内存

    image-20250426103139746

GC回收

image-20250426104518728

理解这张图:

在Java的垃圾回收(GC)机制中,堆内存通常被划分为不同的区域,最常见的划分方式是将堆内存分为Young Generation(年轻代)Old Generation(老年代)。这种划分方式是基于对象的生命周期和内存分配的特点来设计的。

1. 堆内存的划分

Java堆内存通常被划分为以下几个主要区域:

  • Young Generation(年轻代)
    • Eden Space(伊甸区):新创建的对象首先被分配到Eden区。
    • Survivor Space(幸存者区):通常有两个幸存者区(S0和S1),用于存放经过一次GC后仍然存活的对象。
  • Old Generation(老年代)
    • 存放生命周期较长的对象,通常是经过多次GC后仍然存活的对象。
  • Metaspace(元空间)(JDK 1.8及以后):
    • 存放类的元数据,替代了之前的永久代(Permanent Generation)。

2. 不是开辟两个独立的内存空间

这种划分并不是简单地开辟两个独立的内存空间,而是将整个堆内存逻辑上划分为不同的区域。这些区域在物理上是连续的内存空间,但在逻辑上被分成了不同的用途。例如:

  • 年轻代和老年代:它们共享整个堆内存空间,只是逻辑上被划分为不同的区域。
  • 幸存者区和Eden区:它们也是在年轻代内部的逻辑划分。

3. 垃圾回收过程

  • Minor GC(年轻代GC)
    • 主要针对年轻代进行垃圾回收。
    • 当Eden区满了之后,触发Minor GC。
    • 在Minor GC过程中,存活的对象会被移动到幸存者区,而死亡的对象会被回收。
    • 如果幸存者区满了,对象会被晋升到老年代。
  • Major GC(老年代GC)
    • 主要针对老年代进行垃圾回收。
    • 通常触发条件是老年代空间不足。
  • Full GC(全堆GC)
    • 同时清理年轻代和老年代。
    • 通常在元空间不足或手动调用System.gc()时触发。

4. 为什么这样划分

这种划分方式基于以下两个主要原则:

  • 对象的生命周期
    • 大多数对象是短暂存在的,因此将它们分配到年轻代可以快速回收。
    • 少部分对象生命周期较长,最终会被晋升到老年代。
  • 内存分配效率
    • 年轻代使用复制算法(Copying Algorithm),效率较高。
    • 老年代使用标记-清除(Mark-Sweep)或标记-压缩(Mark-Compact)算法,适合处理生命周期长的对象。

总结

  • Java堆内存是逻辑上划分为年轻代和老年代,而不是开辟两个完全独立的内存空间。
  • 这种划分方式是为了优化垃圾回收的效率和内存分配的性能。

:star: JVM

在 JVM 中,将编译好的字节码 .class 文件加载到内存并在执行时转换为机器码的过程涉及多个阶段和组件。以下是逐步说明:


1. 类加载(Class Loading)

JVM 通过 类加载器(ClassLoader).class 文件的字节码加载到内存中。类加载过程包括:

  • 加载(Loading):找到 .class 文件,读取其字节码,并创建一个 Class 对象。
  • 验证(Verification):确保字节码符合 JVM 规范,防止恶意代码。
  • 准备(Preparation):为静态变量分配内存并设置默认初始值。
  • 解析(Resolution):将符号引用(如类名、方法名)转换为直接引用(内存地址)。

2. 字节码验证与存储

加载后的字节码存储在 方法区(Method Area)(Java 8 后称为 元空间(Metaspace))。此时字节码仍是平台无关的中间代码,尚未转换为机器码。


3. 执行引擎(Execution Engine)

JVM 通过 执行引擎 将字节码转换为机器码,有两种主要方式:

(1)解释执行(Interpreter)

  • 即时解释:JVM 的解释器逐条读取字节码指令,将其动态解释为对应平台的机器码并立即执行。
  • 特点:启动快,但执行效率低(重复解释相同代码)。

(2)即时编译(JIT Compilation)

  • 热点代码检测:JVM 通过 热点探测(Hot Spot Detection) 识别频繁执行的代码(如循环、高频方法)。
  • JIT 编译器(如 HotSpot 的 C1/C2):将热点字节码 整体编译为本地机器码,并缓存到 代码缓存区(Code Cache)
  • 后续执行:直接调用已编译的机器码,无需重复解释,性能接近原生代码。

4. 混合模式(Mixed Mode)

现代 JVM(如 HotSpot)默认采用 混合模式

  • 冷路径(Cold Path):初次执行时通过解释器快速启动。
  • 热路径(Hot Path):热点代码被 JIT 编译后,以机器码高速执行。

5. 分层编译(Tiered Compilation)

HotSpot JVM 的分层编译优化了编译策略:

  • 第 0 层:纯解释执行。
  • 第 1 层:C1 编译器(Client Compiler)快速生成轻量级机器码,带基本优化。
  • 第 2 层:C2 编译器(Server Compiler)对高度热点代码进行深度优化(如内联、循环展开)。

6. 内存与机器码存储

  • 代码缓存(Code Cache):JIT 编译后的机器码存储在 JVM 的 代码缓存区(非堆内存),直接供 CPU 执行。
  • 元空间(Metaspace):存储类的元数据(如字节码、常量池),但 不存储机器码

总结流程图

1
2
3
.class文件 → 类加载器 → 方法区(字节码) → 解释器(即时解释) → 机器码(单次执行)

JIT编译器(热点代码) → 代码缓存(机器码) → 重复执行

关键区别

  • 解释执行:逐条转换,无缓存,适合低频代码。
  • JIT编译:批量转换并缓存,适合高频代码,性能更高。

通过这种方式,JVM 平衡了启动速度和运行效率,实现了“一次编译,到处运行”的跨平台特性。

1、初识

JVM负责可跨平台的运行Java字节码文件,其功能包括内存管理、解释执行虚拟指令、即时编译三大功能。

jvm会根据字节码文件去查找方法区中的数据,然后根据方法区的信息定位到堆中真实的数据

image-20251103142356735

image-20251109180337956


2、字节码文件

2.1、i++和++i

在字节码中++操作会变成 incre by 这样的字节码,这里字节码不直接改变操作数栈中的数据,而是直接操作本地变量表,但是i++会在load之前,而++i会在load字节码之后


3、类加载器

3.1、类的生命周期

加载、连接(验证/准备/解析)、初始化、使用、卸载

image-20251102220708145

3.2、类加载器

负责在类加载过程中的字节码文件获取并加载到内存中。通过加载字节码数据放入到内存转成byte[],接下来调用虚拟机底层方法将byte[]转换成方法区和堆中的数据

JDK8及以前

  • 启动类加载器

    使用ClassLoader去加载类的时候,返回为null的可能是被启动类加载器加载了,这是java的核心类库,不允许被获取修改等,这样保证了核心类库的安全。

    如果想要自己些的一个类被启动类加载器加载,可以打好jar包放到jre/lib下进行扩展,或者使用参数进行启动扩展+Xbootclasspath/a:jar

  • 扩展类加载器

    默认加载jre/lib/ext下的jar包,通用但不重要

  • 应用程序类加载器

    会加载pom.xml中的类、以及自己所写的类。

    但这里也会加载启动类和扩展类中加载的类!!!这是由于双亲委派机制

image-20251101223324385

jdk9及其之后

jdk9引入了模块化module的概念,类加载器在设计上发生了很多变化。从jar中获取转成了从module中获取

  • 启动类加载器从原来的C++编写改成了Java编写,并且BootClassLoader继承自BuiltinClassLoader实现从模块中找到要加载的字节码资源文件。注意启动类加载器无法通过java代码获取,仍然为null,为了安全及其保证一致性
  • 扩展类加载器变成了平台类加载器(Platform Class Loader),依然继承自BuiltinClassLoader

4、双亲委派机制

4.1、理解双亲委派

  1. 双亲委派机制就是解决一个类是被哪个类加载器去加载的,会自底向上查找,再由顶向下加载
  2. 当一个类加载器去加载一个类的时候,会自底向上查找是否加载过,如果加载过就直接返回,如果一直到启动类加载器也没有被加载,则从启动类加载器开始自顶向下进行加载
  3. 应用程序类加载器的父类是扩展类加载器、扩展类加载器的父类是启动类加载器
  4. 双亲委派机制有两个好处:第一点避免恶意代码替换JDK中核心类库,确保核心类库的完整性和安全性;第二点避免了一个类被重复加载

image-20251102203036188

4.2、打破双亲委派机制

  1. 创建一个自定义的类加载器继承ClassLoader重写其中的loadclass方法,然后把双亲委派机制的代码删掉,使其不走双亲委派机制,这里会创建一个BreakClassLoader自定义类加载器

    注意:star:: 打破双亲委派是重写loadclass方法,如果要自定义类加载器需要重写findClass方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    public class BreakClassLoader1 extends ClassLoader {

    private String basePath;
    private final static String FILE_EXT = ".class";

    public void setBasePath(String basePath) {
    this.basePath = basePath;
    }

    private byte[] loadClassData(String name) {
    // 这里应该是从文件系统中读取类文件的代码,返回类的字节数组
    // 由于具体实现依赖于文件系统操作,这里仅提供一个示例框架
    try {
    String path = basePath + name.replace('.', '/') + FILE_EXT;
    File file = new File(path);
    FileInputStream fis = new FileInputStream(file);
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    byte[] buffer = new byte[1024];
    int length;
    while ((length = fis.read(buffer)) > 0) {
    baos.write(buffer, 0, length);
    }
    fis.close();
    return baos.toByteArray();
    } catch (IOException e) {
    e.printStackTrace();
    return null;
    }
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
    byte[] data = loadClassData(name);
    if (data == null) {
    throw new ClassNotFoundException("Cannot load class " + name);
    }
    return defineClass(name, data, 0, data.length);
    }

    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
    BreakClassLoader1 classLoader1 = new BreakClassLoader1();
    classLoader1.setBasePath("D:\\lib\\");
    Class<?> clazz1 = classLoader1.loadClass("java.lang.String");
    Object instance = clazz1.newInstance(); // 使用 Object 接收实例,避免类型转换错误
    System.out.println(instance.getClass().getName());
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    import java.io.*;

    public class MyClassLoader extends ClassLoader {
    private String classDir;

    public MyClassLoader(String classDir) {
    this.classDir = classDir;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
    // 将类名转换为文件路径
    String fileName = name.replace('.', File.separatorChar) + ".class";
    File classFile = new File(classDir, fileName);

    try {
    // 从文件中读取类的字节码
    FileInputStream fis = new FileInputStream(classFile);
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    byte[] buffer = new byte[1024];
    int length;
    while ((length = fis.read(buffer)) != -1) {
    baos.write(buffer, 0, length);
    }
    fis.close();

    // 将字节码转换为Class对象
    byte[] classData = baos.toByteArray();
    return defineClass(name, classData, 0, classData.length);
    } catch (IOException e) {
    throw new ClassNotFoundException("Class " + name + " not found", e);
    }
    }
    }
  2. JDBC中SPI(服务发现)机制-----线程上下文类加载器

    image-20251103084313234

    image-20251103084635851


5、Athas

一款可以实现排查java代码、字节码文件、热部署的软件

5.1、热部署

如果项目上线后代码部分错误,又不想重新打包上传,那么这里采用热部署的方式

  1. 在出问题的服务器上部署一个arthas,并启动。

  2. jad命令反编译,然后可以用其它编译器,比如vim来修改源码

    1
    jad --source-only 类全限定名 > 目录/文件名java
  3. mc命令用来编译修改过的代码

    1
    mc -c 类加载器的hashcode 目录/文件名.java -d 输出目录
  4. 用retransform命令加载新的字节码到内存中

    1
    retransform class文件所在目录/xxx.class

6、运行时数据区

由于类加载器将字节码文件加载到了JVM的内存中,所以有了字节码的内存地址

image-20251103093353117

6.1、线程不共享

6.1.1、程序计数器

也叫PC寄存器,每个线程会通过程序计数器记录当前要执行的字节码指令的地址,即下一个要执行的字节码指令

image-20251103094609607

6.1.2、Java虚拟机栈
  • 存放方法栈帧到栈内存中,采用先进后出原则。

  • 在抛出异常的时候其实是将栈中的各个栈帧依次弹出,所以在报错信息中才会有那么多方法

    1
    2
    3
    4
    5
    Exception in thread“main"java.lang.RuntimeException Createbreakpoint:测试
    at com.itheima.jvm.chapter03.FrameDemo.C(FrameDemo.java:20)
    at com.itheima.jvm.chapter03.FrameDemo.B(FrameDemo.java:15)
    at com.itheima.jvm.chapter03.FrameDemo.A(FrameDemo.java:10)
    at com.itheima.jvm.chapter03.FrameDemo.main(FrameDemo.java:5)
  • 每个栈帧的构成:局部变量表、操作数栈、帧数据

    • 局部变量表:保存了每个栈帧的所使用的变量信息大小等
    • 操作数栈:用于iconst放到操作数栈之后才可以istore保存到局部变量表中
    • 帧数据:用于动态链接、保存上一个栈帧中下一个指令的地址、异常表
  • 虚拟机栈的深度:默认是1024KB,如果超出了可能是由于递归结束条件没有调整好,这里会爆 StackOverFlow的错误提示

  • 调整虚拟机栈的大小:

    1
    -Xss1g、-Xss1m、-Xss1024K

    在HotSpot虚拟机中最小值和最大值是有限制的:180k - 1024m。如果局部变量表过多、操作数栈深度过大也会影响栈内存的大小

6.1.3、本地方法栈

存在native等修饰的由C++语言所编写的方法

6.2、线程共享

6.2.1、堆

栈上的局部变量表可以存储堆对象的地址

  • 在jdk8以前静态变量是存储在方法区中,jdk8以后是存储在堆中

  • 堆内存溢出会爆 OutOfMemoryError 的错误信息,即常见的 OOM

  • 堆内存包含used、total、max

    默认max是系统内存的1/4、total是系统内存的1/64

    1
    2
    3
    设置total:-Xms
    设置max: -Xmx
    Xmx必须大于2G、Xms必须大于1MB
  • 堆内存优化:建议将Xms和Xmx设置为统一大小,避免分配内存时候开销

6.2.2、方法区

存放基础信息的位置,线程共享,主要包含三个部分的内容:

  • 类的元信息

    这里常量池和方法其实是引用真正的数据是放到堆中

    image-20251103113318071

  • 运行时常量池

    在原空间中

  • 字符串常量池

    1
    String.intern() 可以直接将字符串放到串池中

    image-20251103115143845

方法区调优

image-20251103113525240

  1. 针对JDK7

    1
    -XX:MaxPermSize=?
  2. 针对JDK8

    1
    -XX:MaxMetaspaceSize=?

6.3、直接内存

使用ByteBuffer可以直接申请内存,不需要再从内存中拷贝一份到堆空间中,可以直接在堆空间中引用内存中的地址,也需要使用这个命令进行限制,不能无限的去申请直接内存

1
-XX:MaxDirectMemorySize=?

7、垃圾回收机制

在C/C++语言中对于创建的对象需要程序员手动的去释放这样比较麻烦,所以在Java中对堆上采用了GC。

7.1、 线程不共享区域回收

这里考虑为什么不对线程不共享区域也进行垃圾回收呢?这里其实是因为:线程不共享区域是只对每个线程可见,那么自然会随着线程的创建而创建,随着线程的销毁而销毁

7.2、方法区回收

方法区中能回收的内容主要就是不再使用的类。判定一个类可以被卸载。需要同时满足下面三个条件:

  1. 此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象。
  2. 加载该类的类加载器已经被回收。
  3. 该类对应的java.lang.Class对象没有在任何地方被引l用。

7.3、堆回收

7.3.1、引用计数法和可达性分析法

❌引用计数法:

  • 在创建的每个对象的时候在其堆空间中给其计数+1,取消引用的时候-1,为0的时候就回收
  • 缺点:当A引用B,B引用A的时候会出现循环引用的问题,此时会导致对象无法回收

image-20251103150253770

可达性分析法

如果一个对象的引用链上没有GCroot对象,那么这个对象可以被垃圾回收

GC ROOT对象:

  • 线程Thread对象,引用线程栈帧中的方法参数、局部变量等。
  • 系统类加载器加载的java.lang.Class对象。
  • 监视器对象,用来保存同步锁synchronized关键字持有的对象。
  • 本地方法调用时使用的全局对象。

image-20251103151122379

7.3.2、引用方式
  1. 强引用:不允许被回收

  2. 软引用:如果一个对象只有软引用关联,那么当内存不足的时候,会将软引用中的数据进行回收 SoftReference,常用于缓存中。只有 SoftReference 自身作为 Root 对象存在;其所引用的目标对象在 Root 不可达且仅存在软引用路径时,可在内存不足时被回收

    image-20251104090254978

  3. 弱引用:弱引用中包含的对象,GC时候不管内存够不够都回收

    image-20251104092342746

  4. 虚引用、终结器引用:

    虚引用:对象已经被回收后才收到通知,用于事后清理

    image-20251104092719160

7.3.3、垃圾回收算法
  • 标记清除

  • 复制

  • 标记整理

  • 分代GC

    image-20251104142728873


8、垃圾回收器

image-20251104151044636

image-20251104151027334


8.1、年轻代垃圾回收器

8.1.1、Serial

Serial和SerialOld组合在实际工作中不常用,主要是一些服务器资源不足的时候可以使用,比如单核CPU

image-20251104151407084


8.1.2、ParNew

image-20251104151908679


8.1.3、Parallel Scavenge

image-20251104152935427


8.2、老年代垃圾回收器

8.2.1、SerialOld

image-20251104151529407

image-20251104153614102


8.2.2、CMS

image-20251104152127748

image-20251104152405150


8.2.3、Parallel Old

image-20251104153223052

8.3、G1垃圾回收器

在传统的垃圾回收器(如Serial、Parallel、CMS等)中,年轻代和老年代通常位于连续的内存区域。具体来说:

  • 年轻代:包含Eden区和两个Survivor区(通常称为S0和S1)。这些区域是连续的。
  • 老年代:是一个单独的连续内存区域。
  • 永久代(或元空间):用于存储类的元数据,通常位于堆外内存。

G1垃圾回收器

G1(Garbage-First)垃圾回收器的设计与传统的垃圾回收器有所不同:

  • 区域划分:G1将整个堆内存划分为多个大小相等的区域(Region)。这些区域可以是年轻代的一部分,也可以是老年代的一部分。
  • 年轻代和老年代的分布:年轻代和老年代的区域是分散的,而不是连续的。G1通过这些区域来管理内存,而不是传统的连续内存块。

G1的特点

  1. 并行和并发:G1在垃圾回收过程中可以并行和并发执行,减少停顿时间。
  2. 预测性:G1可以预测垃圾回收的时间和效率,从而更好地控制停顿时间。
  3. 灵活的内存管理:G1可以根据应用的需求动态调整年轻代和老年代的大小。
  • 传统垃圾回收器:年轻代和老年代通常位于连续的内存区域。
  • G1垃圾回收器:将堆内存划分为多个区域,年轻代和老年代的区域是分散的,而不是连续的。

这种设计使得G1在处理大堆内存时更加灵活和高效,尤其是在需要低延迟的应用场景中。

image-20251104160734871

image-20251104160829507

image-20251104170950479

好的,让我们详细了解一下传统垃圾回收器和G1垃圾回收器在年轻代和老年代回收方法上的具体实现。

传统垃圾回收器

年轻代回收(Minor GC)

  • Eden区和Survivor区
    • 年轻代通常分为三个部分:一个Eden区和两个Survivor区(S0和S1)。
    • 新创建的对象首先分配到Eden区。
    • 当Eden区满时,触发Minor GC。
  • 复制算法
    • Minor GC使用复制算法。在Minor GC过程中,Eden区和一个Survivor区(假设为S0)中的存活对象被复制到另一个Survivor区(S1)。
    • 每次Minor GC后,S0和S1的角色会互换,即S0变为空区,S1变为存活对象的存储区。
    • 如果一个对象在Survivor区中经过多次Minor GC(默认15次,可以通过-XX:MaxTenuringThreshold参数调整)仍然存活,它会被晋升到老年代。
  • 特点
    • 复制算法可以有效避免内存碎片问题。
    • Minor GC的频率较高,但每次回收的速度通常较快,因为年轻代的内存相对较小。

老年代回收(Major GC / Full GC)

  • 标记-清除算法
    • 老年代通常使用标记-清除算法。在Major GC过程中,垃圾回收器会遍历整个老年代,标记所有存活的对象,然后清除未标记的对象。
    • 标记-清除算法会导致内存碎片问题,因为对象的内存空间可能会变得不连续。
  • 标记-压缩算法
    • 为了减少内存碎片问题,一些垃圾回收器(如CMS)在标记-清除算法的基础上,还会进行压缩操作,将存活的对象移动到内存的一端,从而整理出一块连续的空闲内存。
  • 特点
    • Major GC的频率较低,但每次回收的时间通常较长,因为老年代的内存相对较大。
    • Full GC通常会暂停所有应用程序线程,导致较长的停顿时间。

G1垃圾回收器

年轻代回收(Young GC)

  • 区域划分
    • G1将堆内存划分为多个大小相等的区域(Region),其中一部分区域被分配为年轻代,另一部分被分配为老年代。
    • 年轻代由多个Eden区和Survivor区组成,这些区域在堆内存中是分散的。
  • 复制算法
    • G1在年轻代回收中仍然使用复制算法。在Young GC过程中,Eden区和一个Survivor区中的存活对象被复制到另一个Survivor区。
    • 每次Young GC后,Survivor区的角色会互换,类似于传统垃圾回收器。
    • 如果一个对象在Survivor区中经过多次Young GC仍然存活,它会被晋升到老年代。
  • 特点
    • G1的Young GC仍然使用复制算法,有效避免内存碎片问题。
    • 由于区域是分散的,G1可以并行和并发地进行Young GC,减少停顿时间。

老年代回收(Mixed GC)

  • 增量式回收
    • G1的老年代回收称为Mixed GC。G1不会一次性回收整个老年代,而是将老年代划分为多个区域,每次回收一部分区域。
    • G1会根据区域的垃圾回收成本和收益来选择回收哪些区域,从而实现增量式回收,减少停顿时间。
  • 标记-复制算法
    • G1在老年代回收中使用标记-复制算法。在Mixed GC过程中,G1会标记老年代中的存活对象,然后将这些对象复制到其他区域。
    • 这种方法可以有效避免内存碎片问题,同时减少停顿时间。
  • 特点
    • G1通过增量式回收和并行、并发机制,能够有效减少老年代回收的停顿时间。
    • G1可以动态调整回收的区域数量,根据应用程序的实际需求优化垃圾回收性能。

总结

  • 传统垃圾回收器
    • 年轻代回收:使用复制算法,将Eden区和一个Survivor区中的存活对象复制到另一个Survivor区。
    • 老年代回收:使用标记-清除算法或标记-压缩算法,标记存活对象并清除未标记的对象,可能需要进行压缩操作以减少内存碎片。
  • G1垃圾回收器
    • 年轻代回收:使用复制算法,将Eden区和一个Survivor区中的存活对象复制到另一个Survivor区,区域分散但仍然有效避免内存碎片。
    • 老年代回收:使用增量式标记-复制算法,每次回收一部分区域,减少停顿时间,同时避免内存碎片问题。

G1垃圾回收器通过区域划分和增量式回收机制,在减少停顿时间方面表现得更加出色,适合对停顿时间要求较高的应用场景。

image-20251104171015979

8.4、Shenandoah 和 ZGC

ZGC和Shenandoah设计的目标都是追求较短的停顿时间,他们具体的使用场景如下:
两种垃圾回收器在并行回收时都会使用垃圾回收线程占用CPU资源

  1. 在内存足够的情况下,ZGC垃圾回收表现的效果会更好,停顿时间更短。
  2. 在内存不是特别充足的情况下,ShenandoahGC表现更好,并行垃圾回收的时间较短,用户请求的执行效率比较高。

9、运维工具

Promethus + Grafana(安装复杂)

image-20251106112721591

MAT

image-20251106110510631


10、栈、堆存储方式

栈:在栈中存储基本数据类型时候每个栈帧包含操作数栈以及局部变量表,其中局部变量表中每个槽位slot在64位虚拟机占8字节,32位虚拟机占4字节。比如long类型占8字节,那么就占两个槽位。

堆:在堆中存储的对象

image-20251110103010823

image-20251110094418664

在堆中的内存布局的时候由于需要内存对齐即内存对齐填充,让CPU可以直接读取一个整数的内存,达到空间换时间的概念。但是会导致字段重排序,即源代码中写的顺序和实际内存中的顺序不一致。从而达到了CPU缓存读取的效率

静态绑定和动态绑定


11、JIT即时编译器

正常对于字节码来说是一行一行的进行翻译成机器码,这样是同步的速度快,但是做不到异步,所以JIT即时编译器会将热点数据事先做个缓存加载到内存中。已达到复用方法的机器码

对于JIT即时编译器来说最重要的两个组件:C1、C2

image-20251110160919375

11.1、方法内联

image-20251110162845430

其中G2编译器会优化代码,比如一个循环,可以优化为一个乘法

image-20251113084504588

11.2、逃逸分析

image-20251113085315062

逃逸分析里面有个重要的就是标量替换,可以将发生内联的代码直接在栈上进行操作,而不需要放在堆上进行操作了


12、垃圾回收器原理

12.1、G1垃圾回收器

image-20251113090651432

image-20251113090920612

image-20251113091030182

12.1.1、年轻代回收

image-20251113092442834

image-20251113092803591

image-20251113092835370

image-20251113093157594

12.1.2、老年代回收

image-20251113093409992

image-20251113093748594

image-20251113093822318

image-20251113094234702

image-20251113094435100

image-20251113095703327

12.2 ZGC垃圾回收器

STW的时间最短,这里和G1垃圾回收器最大的区别是转移阶段,G1中需要用户线程暂停

image-20251113095242110

在转移阶段,ZGC和Shenandoah采用了读屏障,防止用户线程在被某个线程修改后读到的是老数据

image-20251113100320932

image-20251113100339636

image-20251113100448620

image-20251113101252646

image-20251113101854851

JDK21之前的ZGC是没有进行分代处理的

image-20251113102205197

12.3、ShenandoahGC垃圾回收器

image-20251113102814185