Java核心篇之JVM探秘:对象创建与内存分配机制

系列文章目录

第一章 Java核心篇之JVM探秘:内存模型与管理初探

第二章 Java核心篇之JVM探秘:对象创建与内存分配机制

第三章 Java核心篇之JVM探秘:垃圾回收算法与垃圾收集器

第四章 Java核心篇之JVM调优实战:Arthas工具使用及GC日志分析


目录

前言

一、对象创建过程

(1)类加载检查

(2)分配内存

1.对象创建与内存分配

2.对象填充

3.执行构造函数

4.并发问题处理

(3)初始化零值

(4)设置对象头

Mark Word

Type Pointer

Array Length (如果对象是数组)

(5)执行方法

二、对象内存分配

(1)对象在栈上分配

示例:

(2)对象在Eden上分配

Eden区分配的特点:

示例:

(3)大对象直接进入老年代

(4)长期存活的对象将进入老年代

(5)对象动态年龄判断

(6)老年代空间分配担保机制

三、对象内存回收

(1)引用计数法

示例:

(2)可达性分析算法

(3)常见引用类型

(4)finalize()方法最终判定对象是否存活

(5)如何判断一个类是无用的类

总结


前言

        Java虚拟机(JVM)是Java语言的核心组件之一,负责执行Java字节码。在JVM中,对象的创建和内存管理是一个复杂而精细的过程,涉及多个阶段和多种策略。本文将深入探讨JVM中的对象创建流程、内存分配机制以及它们如何影响程序性能。


一、对象创建过程

下边两张图分别是类加载机制和对象创建过程的流程图

(1)类加载检查

当程序请求创建一个新对象时,JVM首先会检查这个类是否已经被加载、解析和初始化过。如果尚未完成这些步骤,那么JVM会先执行类加载过程。

(2)分配内存

一旦类被确认可以使用,JVM会在堆内存中为新对象分配空间。对象的内存大小由其成员变量决定,包括实例变量和继承链上的变量。

在JVM中,内存分配主要发生在堆内存中,堆内存是所有线程共享的内存区域,用来存储所有Java对象实例和数组。内存分配的过程可以分为几个关键步骤:

1.对象创建与内存分配

当Java程序请求创建一个新对象时,JVM首先检查这个类是否已经被加载、解析和初始化过。如果类已经准备好,JVM会在堆中为新对象分配内存。内存分配的方式有两种主要策略:指针碰撞(Bump-the-Pointer)和空闲列表(Free List)。

  • 指针碰撞:如果堆内存是规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间维护一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
  • 空闲列表:如果堆内存是不规整的,已使用的内存和空闲的内存相互交错,那就需要维护一个列表,记录上面那些大小不一的空闲内存区间。

2.对象填充

分配好内存之后,JVM会对新对象进行填充,包括初始化对象的成员变量为默认值(例如,int类型为0,引用类型为null)。

3.执行构造函数

最后,JVM会执行对象的构造函数,完成对象的初始化。

4.并发问题处理

在多线程环境下,内存分配和对象创建可能会引发竞态条件和内存一致性问题。JVM采用了多种机制来解决这些问题:

  • 线程同步 JVM使用锁机制来保证在多线程环境下内存分配的原子性。当多个线程试图同时创建对象时,JVM会使用锁来确保一次只有一个线程能够进行内存分配,从而避免竞态条件。

  • 内存屏障 为了保证内存访问的有序性,防止指令重排序,JVM使用内存屏障(Memory Barrier)技术。内存屏障是一种特殊的指令,它可以阻止编译器和处理器对内存操作进行重排序,确保内存操作的顺序符合程序的预期。

  • 缓存一致性 在现代多核处理器中,每个CPU都有自己的缓存,为了保持缓存之间的一致性,处理器使用了一种称为MESI(Modified, Exclusive, Shared, Invalid)协议的缓存一致性协议。JVM利用硬件提供的缓存一致性协议来维持多线程环境下的内存一致性。

  • TLAB (Thread Local Allocation Buffer) 为了减少锁的使用,提高对象创建的效率,JVM提供了一个叫做TLAB(线程本地分配缓冲区)的概念。每个线程都有一个独立的TLAB,对象优先在自己的TLAB中分配,这样可以避免在多线程环境下频繁地获取锁,从而提高了对象创建的速度。

  • CAS (Compare and Swap) CAS是一种无锁编程技术,用于原子更新变量。JVM利用CAS操作来实现一些轻量级的同步机制,比如原子变量类(如java.util.concurrent.atomic包中的类)。

(3)初始化零值

分配完内存后,JVM会将对象的成员变量初始化为默认值,如整型为0,浮点型为0.0,引用类型为null等。这一步是为了确保对象在构造函数执行前处于一致状态。

(4)设置对象头

当JVM为新对象分配完内存后,在初始化零值之前,它会先设置对象头。对象头通常包含以下信息:

Mark Word

Mark Word是对象头中的一部分,主要用于存储对象的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁等。Mark Word的布局会根据对象的锁定状态动态改变,以便支持轻量级锁、偏向锁、重量级锁等不同的锁级别。

Type Pointer

这是指向对象所属类的元数据的指针,JVM通过这个指针确定对象所属的类。在某些JVM实现中,类型指针可能不在对象头中,而是通过其他方式(如类指针压缩)来存储。

Array Length (如果对象是数组)

如果创建的对象是一个数组,对象头还会额外包含一个长度字段,用于存储数组的长度。

(5)执行<init>方法

执行<init>方法,也就是执行对象的构造函数,这是程序员定义的用于初始化对象状态的方法。构造函数可以调用其他方法或访问静态变量,但不能直接访问或修改非初始化的实例变量。

二、对象内存分配

(1)对象在栈上分配

通常情况下,对象是在堆内存中分配的,这是因为对象的生命周期不可预测,可能需要长期存在。然而,在某些特殊情况下,对象可以分配在栈上,这种情况被称为栈上分配(Stack Allocation)或标量替换(Scalar Replacement)。栈上分配主要应用于局部变量,尤其是那些在方法体内创建并很快就会被销毁的小型对象,这样可以显著减少垃圾收集的压力。 

示例:

假设有一个简单的Java方法,其中创建了一个局部变量对象,这个对象不会被方法之外的代码引用,而且它的生命周期仅限于该方法的执行期间。在这种情况下,如果JVM启用了栈上分配并且经过逃逸分析确定该对象不会逃逸,那么该对象就有可能在栈上分配。

public class StackAllocationExample {
    public void method() {
        User user = new User();
        // 使用user...
        // user仅在method方法的栈帧中存在
    }
}

class User {
    String name;
    int age;
}

在这个例子中,User对象只在method方法内部创建和使用,如果它满足栈上分配的条件,那么它将会在栈上分配,而不是在堆上。 

(2)对象在Eden上分配

JVM的堆内存被划分为新生代和老年代。新生代又进一步分为一个Eden区和两个Survivor区(S0和S1)。Eden区是对象首次创建的地方,大部分对象在Eden区分配。

Eden区分配的特点

  • 对象创建时,首先尝试在Eden区内分配。
  • 如果Eden区没有足够的空间,或者对象太大,可能直接进入老年代。
  • 经过若干次垃圾回收(Minor GC),存活下来的对象会从Eden区晋升到Survivor区,或者直接晋升到老年代。

示例:

当一个对象创建时,默认情况下它会在Eden区内分配。Eden区是新生代的一部分,专门用于存放新创建的对象。如果对象在一次或多次垃圾回收后仍然存活,它将被移动到Survivor区,或者直接晋升到老年代。

public class EdenAllocationExample {
    public static void main(String[] args) {
        User user = new User();
        // 使用user...
    }
}

class User {
    String name;
    int age;
}

在这个例子中,User对象创建在Eden区,如果它在接下来的垃圾回收过程中存活,那么它可能被晋升到Survivor区或老年代。 

(3)大对象直接进入老年代

大对象指的是需要大量连续内存空间的对象,例如大的数组或长字符串。JVM为了减少新生代的碎片化,避免频繁的Minor GC,采取了以下策略:

  • 如果对象的大小超过了一定的阈值(通常是JVM的一个参数设置),这个对象会被直接分配到老年代中。
  • 直接进入老年代可以避免新生代的频繁垃圾回收,因为大对象通常生命周期较长,不易被回收。
  • 这种策略有助于提高大对象密集型应用的性能,减少垃圾收集的次数。

(4)长期存活的对象将进入老年代

既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。

如果对象在Eden出生并经过第一次Minor GC后仍然能够存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,CMS收集器默认6岁,不同的垃圾收集器会略微有点不同),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数–XX:MaxTenuringThreshold来设置。

(5)对象动态年龄判断

对象的年龄是指对象从创建开始到被垃圾收集器回收之间所经历的Minor GC次数。JVM给每个对象分配一个年龄计数器(Age Counter)。对象在Eden区创建后,如果在第一次Minor GC后依然存活,它会被移动到一个Survivor区,并且年龄计数器会加1。此后,每次经历Minor GC并且没有被回收,年龄计数器都会递增,直到达到一定的阈值(默认为15)。当对象的年龄达到这个阈值时,它将被提升到老年代。

对象年龄的判断机制有助于将长期存活的对象及时移动到老年代,避免新生代的频繁垃圾回收,同时也减少了老年代的垃圾回收压力,因为老年代的垃圾回收(Full GC或Major GC)成本更高。

(6)老年代空间分配担保机制

在进行Minor GC之前,JVM会检查老年代是否有足够的空间来接收可能从新生代提升过来的对象。如果老年代的空间不足以担保这次Minor GC后所有存活对象的迁移,JVM将触发一次Full GC,以腾出足够的空间。这种机制被称为老年代空间分配担保(Space Allocation Guarantee),其目的是避免在新生代进行垃圾回收时出现内存不足的情况,确保Minor GC的顺利进行。

如果担保机制检测到老年代空间不足,JVM会进行如下操作:

  1. 尝试压缩老年代,回收部分空间。
  2. 如果压缩后仍然不足,将触发Full GC,清理整个堆内存,包括老年代和永久代(在JDK 8中是方法区)。
  3. 如果Full GC后仍然不足,将抛出OutOfMemoryError异常。

三、对象内存回收

堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断那些对象是没有被任何地方使用的。

(1)引用计数法

引用计数法是一种简单的内存管理策略,它通过跟踪指向一个对象的引用数量来确定对象是否可被回收。每当有一个地方引用一个对象时,它的引用计数器就会加1;当引用失效时,计数器减1。当一个对象的引用计数变为0时,表明没有任何引用指向它,此时它就可以被回收了。

优点:实现简单,运行时不需要进行额外的计算或全局性的搜索,即时性好。

缺点:无法处理循环引用的问题。如果有两个对象相互引用对方,即使它们不再被外部引用,引用计数法也无法正确识别它们为垃圾,从而导致内存泄露。

示例:

尽管Java的垃圾收集器不采用引用计数法,但我们可以通过一个类似场景的示例来说明,如果在引用计数法下,两个对象互相引用时可能导致的内存泄漏情况。下面是一个简化版的伪代码示例,用于说明这个问题:

// 注意:以下代码仅用于演示,实际上Java的垃圾收集器不使用引用计数法
class Node {
    private Node reference;

    public Node(Node ref) {
        this.reference = ref;
    }

    public void setReference(Node ref) {
        this.reference = ref;
    }

    public Node getReference() {
        return reference;
    }
}

public class ReferenceCountingDemo {
    public static void main(String[] args) {
        Node nodeA = new Node(null);
        Node nodeB = new Node(nodeA);
        nodeA.setReference(nodeB);

        // 现在nodeA和nodeB互相引用,如果没有垃圾收集器,将导致它们都无法被回收
    }
}

在引用计数法中,nodeAnodeB互相引用对方,即使它们不再被任何外部引用持有,但由于它们相互之间的引用,它们的引用计数都不会降到0,因此按照引用计数法,这两个对象将永远不会被回收,导致内存泄漏。 

(2)可达性分析算法

这是JVM中常用的垃圾回收算法。它基于一个基本思想:通过一系列称为“GC Roots”的根对象作为起始点,从这些根对象向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,即可判定此对象是不可达的,即不可能再被使用。

GC Roots通常包括:

  • 正在执行的方法中声明的局部变量对象。
  • 方法调用栈中引用的对象。
  • 本地方法栈中JNI(Native方法)引用的对象。
  • Java虚拟机内部引用的对象,如基本类型的Class对象,或者常量池中的引用。

(3)常见引用类型

Java中定义了四种强度不同的引用类型,它们分别是强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)。

  • 强引用是最常用的引用类型,只要强引用存在,垃圾回收器就不会回收掉对象。
  • 软引用用于描述还有用但非必需的对象。当系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收后还没有足够的内存,才会抛出内存溢出异常。
  • 弱引用比软引用的强度更弱一些,被弱引用关联的对象只能生存到下一次垃圾回收发生之前。
  • 虚引用也称为幽灵引用或幻影引用,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

(4)finalize()方法最终判定对象是否存活

在Java中,finalize()方法是Object类的一个保护方法,允许在对象被垃圾回收前做一些必要的清理工作。当垃圾回收器准备回收一个对象时,如果发现这个对象中定义了finalize()方法,就会自动调用这个方法。但是,finalize()方法的调用并不是强制的,也不保证一定会被调用,且其执行时机不确定,不应依赖它进行资源释放,应使用try-finally或try-with-resources语句进行资源的显式释放。

示例:

在Java中 finalize()方法可以用来执行一些清理工作,如关闭文件、网络连接等资源。然而,finalize()方法的调用不是强制的,且其实现细节和调用时机由JVM决定,因此不应该依赖它来确保资源的释放。下面是一个使用finalize()方法的示例:

class ResourceManagedObject {
    private boolean isClosed = false;

    protected void finalize() throws Throwable {
        if (!isClosed) {
            // 执行清理工作,例如关闭文件或网络连接
            System.out.println("Resource cleaned up by finalize()");
            isClosed = true;
        }
        super.finalize();
    }

    public void close() {
        // 显式关闭资源
        System.out.println("Resource closed explicitly");
        isClosed = true;
    }
}

public class FinalizeDemo {
    public static void main(String[] args) throws Exception {
        ResourceManagedObject obj = new ResourceManagedObject();
        // 使用obj...

        // 显式关闭资源
        obj.close();

        // 删除引用,使obj成为垃圾收集的目标
        obj = null;
        System.gc(); // 请求垃圾收集

        // 等待垃圾收集器运行
        Thread.sleep(1000); // 假设垃圾收集器会在1秒内运行
    }
}

在这个示例中,ResourceManagedObject类实现了finalize()方法,用于在对象被垃圾收集前执行资源清理工作。然而,最佳实践是不要依赖finalize()方法,而应该在不再需要对象时显式地调用close()方法来释放资源,这是因为finalize()方法的执行是不确定的,且其执行可能带来性能上的开销。此外,从Java 9开始,finalize()方法的使用已被弃用,建议使用其他资源管理技术,如try-with-resources语句或显式的资源关闭逻辑。 

(5)如何判断一个类是无用的类

类的卸载(Class Unloading)在Java中并不常见,但在某些特定情况下,如Web容器中,可能需要卸载不再使用的类。判断一个类是否无用,通常考虑以下几点:

  • 类的所有实例都已经回收。
  • 没有任何引用指向该类的Class对象。
  • 类的加载器已经回收或可被回收。

总结

        JVM中对象的创建和内存分配是一个多步骤、多策略的过程,涉及类加载、内存布局、垃圾回收等多个层面。理解和优化这一机制对于提升Java应用程序的性能至关重要。通过合理的设计和编码实践,我们可以最大限度地发挥JVM的优势,构建高效稳定的应用系统。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/798097.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

窗口比较器的TINA仿真

TINA仿真 LM339比较器是大多数情况下应用的&#xff0c;仿真用的TLV3491只是为了仿真说明问题。 窗口比较器。当被比较的信号电压Uin位于门限电压之间时&#xff0c;输出为高电位。当Uin不在门限电位范围之间时, 输出为低电位&#xff0c;窗口电压△UVS1-VS2。它可用来判断输入…

1.5.1抽象java入门

前言&#xff1a; 1.5.0版本中&#xff0c;我们熟练使用Git三个可视化操作&#xff08;签出&#xff0c;提交&#xff0c;对比&#xff09;&#xff0c;再加上1.4.0版本的新建&#xff0c;总计使用四个Git可视化操作&#xff1b;对java编程的学习&#xff0c;总结&#xff0c;…

部署一台本机的ai智能模型

部署ai模型 1.打开地址搜 https://ollama.com/&#xff08;开源ai模型网址&#xff09;下载软件 2.点击安装具有羊驼图标的文件 3.在右下角查看 羊驼图标 4。打开脚本&#xff0c;执行可以切换羊驼安装的位置与环境变量 5.winR 输入cmd,在命令窗口输入ollama命令 如果安装成功…

算法之工程化内容(2)—— Git常用命令

目录 1. git初始化配置 2. 新建仓库 3. 工作区——>暂存区——>本地仓库 4. git reset回退版本 5. 查看差异 git diff 6. 删除文件git rm 7. .gitignore 8. vscode操作git 9. git分支、合并和删除 10. 解决合并冲突 11. 回退和rebase 12. 添加远程仓库 参考链接&#xff…

Web 性能入门指南-1.5 创建 Web 性能优化文化的最佳实践

最成功的网站都有什么共同点&#xff1f;那就是他们都有很强的网站性能和可用性文化。以下是一些经过验证的有效技巧和最佳实践&#xff0c;可帮助您建立健康、快乐、值得庆祝的性能文化。 创建强大的性能优化文化意味着在你的公司或团队中创建一个如下所示的反馈循环&#xff…

C语言:指针详解(5)

目录 一、sizeof()函数和strlen()函数的对比 1.sizeof()函数 2.strlen()函数 3.sizeof()函数和strlen()函数的对比 二、数组和指针笔试试题解析 1.一维数组 2.字符数组 &#xff08;1&#xff09;代码1 &#xff08;2&#xff09;代码2 &#xff08;3&#xff09;代码…

pytorch训练的时候 shm共享内存不足,导致训练停止

1.查看shm情况 df -h /dev/shm内存已经满了&#xff0c;因为之前训练多次训练意外停止到shm中的缓存不能及时被清理 2、手动清理shm 依然没被释放 3、查看关联的进程&#xff0c;一个一个kill lsof |grep deletedkill -9 46619 44618 44617 。。。。。4、搞定

vue中父子传递属性值

1、父传子属性值 自定义图库组件 在add.vue中应用tuku组件并给默认值 效果 2、 子传父&#xff0c;逆向赋值 add.vue和第一问中一样 修改tuku组件&#xff0c;传值给add.vue 3、多个传递 效果&#xff1a; 点击两个修改按钮后 4、使用defineModel简化父子传值 其他代码跟…

python作业三

1.使用requests模块获取这个json文件http://java-api.super-yx.com/html/hello.json 2.将获取到的json转为dict 3.将dict保存为hello.json文件 4.用io流写一个copy(src,dst)函数,复制hello.json到C:\hello.json import json import shutilimport requests #使用requests模块获…

OceanBase:引领下一代分布式数据库技术的前沿

OceanBase的基本概念 定义和特点 OceanBase是一款由蚂蚁金服开发的分布式关系数据库系统&#xff0c;旨在提供高性能、高可用性和强一致性的数据库服务。它结合了关系数据库和分布式系统的优势&#xff0c;适用于大规模数据处理和高并发业务场景。其核心特点包括&#xff1a; …

【C/C++积累技巧】实现 连续播放文件图片+逐帧文本显示, 同时 可以按任意键退出(基于easyx小游戏编程)

技巧一、使用 IMAGE数组循环&#xff1a;实现【连续播放图片】 &#xff08;1&#xff09;一张图片如何放映在 图形化窗口上&#xff1a;借用两个函数 #include<graphics.h> // 函数的头文件IMAGE imgMy; // 图形变量 loadimage(&imgMy, "写入你想显示的图片路…

软件开发面试题(C#语言,.NET框架)

1. 解释什么是委托&#xff08;Delegate&#xff09;&#xff0c;并举例说明它在C#中的用法。 委托是一种引用类型&#xff0c;它可以用于封装一个或多个方法。委托对象可以像方法一样调用&#xff0c;甚至可以用于创建事件处理程序。委托是C#中实现事件和回调函数的重要机制。…

Hive表【汇总】

提前必备 1、内部表和外部表的区别 概念讲解&#xff1a; 外部表&#xff1a;1、存放他人给予自己的数据2、当我们删除表操作时&#xff0c;会将表的元数据删除&#xff0c;保留数据文件 内部表&#xff1a;1、存放已有的数据2、当我们删除表操作时&#xff0c;会将表的元数据…

Unity 优化合集

1️⃣ 贴图优化 1. Read/Write Enable 这个属性勾选后允许你在运行时读取和写入纹理数据&#xff0c;这对于需要实时生成内容或者需要动态修改纹理的场合非常有用但在大部分情况下这是不必要的。如果打开这个属性&#xff0c;会使运行时贴图大小翻倍&#xff0c;内存中会额外…

缓存与分布式锁

一、缓存 1、缓存使用 为了系统性能的提升&#xff0c;我们一般都会将部分数据放入缓存中&#xff0c;加速访问。 适合放入缓存的数据有&#xff1a; 即时性、数据一致性要求不高的&#xff1b;访问量大且更新频率不高的数据。 在开发中&#xff0c;凡是放入缓存中的数据我们都…

Git 命令行快速入门

前言 &#xff08;1&#xff09;新手个人建议使用TortoiseGit这类图形化界面来上手学习。 &#xff08;2&#xff09;如果一定需要用命令行进行操作&#xff0c;可以按照B站&#xff1a;程式与网页开发者必备技能&#xff01;Git 和 GitHub 零基础快速上手&#xff0c;轻松掌握…

构造与操作链栈

归纳编程学习的感悟, 记录奋斗路上的点滴, 希望能帮到一样刻苦的你! 如有不足欢迎指正! 共同学习交流! 🌎欢迎各位→点赞 👍+ 收藏⭐ + 留言​📝心态决定高度,细节决定成败! 链栈是数据结构中栈的一种实现方式,它利用链表(通常是单链表)来存储栈中的元…

【触摸屏】【红十字会学习系统】功能模块:视频 + AI拍照合成

项目背景 提升公众急救能力&#xff1a;确保每个人都能在紧急情况下采取正确的急救措施&#xff0c;减少伤害&#xff0c;挽救生命。培养人道主义价值观&#xff1a;通过教育和培训&#xff0c;传播红十字精神&#xff0c;促进社会对弱势群体的关注与支持。建立社区响应网络&a…

1. InternLM - 入门岛

第1关 Linux 基础知识 1. 完成SSH连接与端口映射并运行hello_world.py SSH连接配置 # wsl2中生成密钥对&#xff08;~/.ssh/id_rsa, ~/.ssh/id_rsa.pub&#xff09; ssh-keygen -t rsa# 将id_rsa.pub在internStudio作为公钥导入SSH登录 $ ssh -p 38871 rootssh.intern-ai.o…

5.SpringBoot核心源码-启动类源码分析

目录 概述技巧spring boot 如何启动应用程序run方法里面核心逻辑 SpringApplicaiton.run(xxx.class,args)结束 概述 SpringBoot核心源码-启动类源码分析 技巧 如何给外部源码加注释&#xff0c;想要在源码中添加自己的注释&#xff0c;会弹出 file is read only&#xff0c;代…