2025 届秋招常见八股文汇总——JVM、Spring

JVM 内存区域有哪些部分

  1. 程序计数器:程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。在多线程环境下,每个线程都有自己独立的程序计数器。当线程执行 Java 方法时,程序计数器记录的是正在执行的虚拟机字节码指令的地址。
  2. Java 虚拟机栈:每个 Java 线程都有一个私有的 Java 虚拟机栈,与线程同时创建。每个方法在执行时都会创建一个栈帧,用于存储局部变量、操作数栈、动态链接、方法出口等信息。栈帧在方法调用时入栈,方法返回时出栈。
  3. 本地方法栈: 本地方法栈与 Java 虚拟机栈类似,但它为本地方法服务。本地方法是用其他编程语言(如 C/C++)编写的,通过 JNI 与 Java 代码进行交互。
  4. :Java 堆是 Java 虚拟机中最大的一块内存区域,用于存储对象实例。所有的对象实例和数组都在堆上分配内存。堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。堆可以分为新生代和老年代等不同的区域,其中新生代又包括 Eden 空间、Survivor 空间(From 和 To)。
  5. 方法区: 方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在 HotSpot 虚拟机中,方法区也被称为永久代,但在较新的 JVM 版本中,永久代被元空间所取代。
  6. 运行时常量池:是方法区的一部分,用于存储编译期生成的类、方法和常量等信息。
  7. 字符串常量池:字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
  8. 直接内存:不是 Java 虚拟机运行时数据区的一部分,但 Java 可以通过 NIO 操作直接内存,提高 I/O 性能。

什么是强引用、软引用、弱引用、虚引用

这四种引用决定了对象的生命周期以及垃圾收集器如何收集垃圾。

  1. 强引用:最常见的引用类型。如果一个对象具有强引用,那么垃圾收集器绝不会回收它。
  2. 软引用:软引用用于描述一些还有用但非必需的对象。如果一个对象只有软引用指向它,那么在系统内存不足时,垃圾回收器会尝试回收这些对象。软引用通常用于实现内存敏感的缓存,可以在内存不足时释放缓存中的对象。
  3. 弱引用:弱引用比软引用的生命周期更短暂。如果一个对象只有弱引用指向它,在进行下一次垃圾回收时,不论系统内存是否充足,这些对象都会被回收。弱引用通常用于实现对象缓存,但不希望缓存的对象影响垃圾回收的情况。
  4. 虚引用:虚引用是 Java 中最弱的引用类型。如果一个对象只有虚引用指向它,那么无论何时都可能被垃圾回收器回收,但在对象被回收之前,虚引用会被放入一个队列中,供程序员进行处理。虚引用主要用于跟踪对象被垃圾回收的时机,进行一些必要的清理或记录。

哪些对象可以作为 GC Roots

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象
  • 本地方法栈(Native 方法)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 所有被同步锁持有的对象
  • JNI(Java Native Interface)引用的对象
  1. 标记-清除算法

标记清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。在标记阶段首先通过根节点(GC Roots),标记所有从根节点开始的对象,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。适用场合:

  • 存活对象较多的情况下比较高效。
  • 适用于年老代(即旧生代)。
  1. 标记-复制算法

从根集合节点进行扫描,标记出所有的存活对象,并将这些存活的对象复制到一块儿新的内存上去,之后将原来的那一块儿内存全部回收掉 现在的商业虚拟机都采用这种收集算法来回收新生代。 适用场合:

  • 存活对象较少的情况下比较高效。
  • 扫描了整个空间一次(标记存活对象并复制移动)。
  • 适用于年轻代(即新生代):基本上 98% 的对象是 “朝生夕死” 的,存活下来的会很少。

缺点:

  • 需要一块空的内存空间。
  • 需要复制移动对象。
  • 复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活的对象较多,复制的成本也将很高。
  1. 标记-整理算法

标记-压缩算法是一种老年代的回收算法,它在标记-清除算法的基础上做了一些优化。 首先也需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。

  1. 分代收集算法

分代收集算法就是目前虚拟机使用的回收算法,它解决了标记整理不适用于新年代的问题,将内存分 为各个年代。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。 在不同年代使用不同的算法,从而使用最合适的算法,新生代存活率低,可以使用复制算法。而老年代对象存活率高,没有额外空间对它进行分配担保,所以只能使用标记清除或者标记整理算法。

有哪些垃圾回收器

  1. 新生代垃圾收集器
  • Serial 收集器(复制算法)是新生代单线程收集器,优点是简单高效,算是最基本、发展历史最悠久的收集器。它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集完成。
  • ParNew 收集器(复制算法)是新生代并行收集器,其实就是 Serial 收集器的多线程版本。
  • Parallel Scavenge 收集器是新生代并行收集器,追求高吞吐量,高效利用 CPU。
  1. 老年代垃圾收集器
  • Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程(串行)收集器,使用标记整理算法。这个收 集器的主要意义也是在于给 Client 模式下的虚拟机使用
  • Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器在 JDK 1.6 中才开始提供。
  • CMS 收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的 Java 应用集中在互联网站或者 B/S 系统的服务端上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS 收集器就非常符合这类应用的需求。

CMS 收集器是基于 “标记-清除” 算法实现的,它的运作过程相对前面几种收集器来说更复杂一些,整个过程分为 4 个步骤:

    • 初始标记
    • 并发标记
    • 重新标记
    • 并发清除
  1. 新生代和老年代垃圾收集器

G1 收集器-标记整理算法 :JDK1.7 后全新的回收器, 用于取代 CMS 收集器。G1 收集器的优势:

  • 独特的分代垃圾回收器,分代 GC:分代收集器, 同时兼顾年轻代和老年代。
  • 使用分区算法, 不要求 eden,年轻代或老年代的空间都连续。
  • 并行性: 回收期间, 可由多个线程同时工作, 有效利用多核 CPU 资源。
  • 空间整理: 回收过程中, 会进行适当对象移动, 减少空间碎片。
  • 可预见性:G1 可选取部分区域进行回收,可以缩小回收范围,减少全局停顿。

G1收集器的阶段分以下几个步骤:

  • 初始标记(它标记了从 GC Root 开始直接可达的对象)
  • 并发标记(从 GC Roots 开始对堆中对象进行可达性分析,找出存活对象)
  • 最终标记(标记那些在并发标记阶段发生变化的对象,将被回收)
  • 筛选回收(首先对各个 Regin 的回收价值和成本进行排序,根据用户所期待的 GC 停顿时间指定回收计划,回收一部分 Region)

类加载机制介绍一下

类加载机制是Java虚拟机运行Java程序时负责将类加载到内存中的过程。它包括以下几个步骤。

加载:在此阶段,类加载器负责查找类的字节码文件,并将其加载到内存中。字节码可以来自文件系统、网络等位置。加载阶段不会执行类中的静态初始化代码。

连接:连接阶段包括三个子阶段:

  • 验证:确保加载的类文件格式正确,并且不包含不安全的构造。
  • 准备:在内存中为类的静态变量分配内存空间,并设置默认初始值。这些变量在此阶段被初始化为默认值,比如数值类型为0,引用类型为null。
  • 解析:将类、接口、字段和方法的符号引用解析为直接引用,即内存地址。这一步骤可能包括将常量池中的符号引用解析为直接引用。

初始化:在此阶段,执行类的静态初始化代码,包括静态字段的赋值和静态代码块的执行。静态初始化在类的首次使用时进行,可以是创建实例、访问静态字段或调用静态方法。

类加载器有哪些

  • BootstrapClassLoader(启动类加载器):最顶层的加载类,由 C++实现,通常表示为 null,并且没有父级,主要用来加载 JDK 内部的核心类库(%JAVA_HOME%/lib 目录下的 rt.jar、resources.jar、charsets.jar 等 jar 包和类)以及被 -Xbootclasspath 参数指定的路径下的所有类。
  • ExtensionClassLoader(扩展类加载器):主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类。
  • AppClassLoader(应用程序类加载器):面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。

介绍一下双亲委派机制

双亲委派机制是Java类加载器中的一种设计模式,用于确定类的加载方式和顺序。这个机制确保了 Java 核心库的安全性和一致性。该机制的核心思想是:如果一个类加载器收到了类加载请求,默认先将该请求委托给其父类加载器处理。只有当父级加载器无法加载该类时,才会尝试自行加载。

双亲委派机制能够提高安全性,防止核心库的类被篡改。因为所有的类最终都会通过顶层的启动类加载器进行加载。另外由于类加载器直接从父类加载器那里加载类,也避免了类的重复加载。

说一说对 Spring AOP 的了解

面向切面编程,可以说是面向对象编程的补充和完善。OOP 引入封装、继承、多态等概念来建立一种对象层次结构。不过 OOP 允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。 AOP 技术恰恰相反,它利用一种称为 “横切” 的技术,剖解开封装的对象内部,并将那些影响了多个类的 公共行为封装到一个可重用模块,并将其命名为切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的 耦合度,并有利于未来的可操作性和可维护性。

Spring 中 AOP 代理由 Spring 的 IOC 容器负责生成、管理,其依赖关系也由 IOC 容器负责管理。因此,AOP 代理可以直接使用容器中的其它 bean 实例作为目标,这种关系可由 IOC 容器的依赖注入提供。Spring 创建代理的规则为:

  • 默认使用 JDK 动态代理来创建 AOP 代理,这样就可以为任何接口实例创建代理了。
  • 当需要代理类,而不是代理接口的时候,Spring 会切换为使用 CGLIB 代理 ,也可强制使用 CGLIB。

AOP 编程其实是很简单的事情,纵观 AOP 编程,程序员只需要参与三个部分:

  • 定义普通业务组件
  • 定义切入点,一个切入点可能横切多个业务组件
  • 定义增强处理,增强处理就是在 AOP 框架为普通业务组件织入的处理动作

所以进行 AOP 编程的关键就是定义切入点和定义增强处理,一旦定义了合适的切入点和增强处理,AOP 框架将自动生成 AOP 代理,即:代理对象的方法=增强处理+被代理对象的方法。

说一说对 Spring IOC 的理解

  1. 什么是 IOC

Spring 的 IOC,也就是控制反转,它的核心思想是让对象的创建和依赖关系由容器来控制,不是我们自己 new 出来的,这样各个组件之间就能保持松散的耦合。

这里的容器实际上就是个 Map,Map 中存放的是各种对象。通过 DI 依赖注入,Spring 容器可以在运行时动态地将依赖注入到需要它们的对象中,而不是对象自己去寻找或创建依赖。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。举例来说,在实际项目中一个 Service 类可能有几百甚至上千个类作为它的底层,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,如果利用 IoC 的话,你只需要配置好,然后在需要的地方引用就行了。

  1. 如何配置

Spring 时代我们一般通过 XML 文件来配置,后来开发人员觉得 XML 文件来配置不太好,于是 SpringBoot 注解配置就慢慢开始流行起来。使用配置可以告诉 Spring 容器如何创建对象、如何管理对象的生命周期。

总结来说,Spring 的 IOC 容器是一个中央化的、负责管理应用中所有对象生命周期的强大工具。

Bean 的作用域

在 Spring 中,那些组成应用程序的主体及由 Spring IOC 容器所管理的对象,被称之为 Bean。而 Bean 的作用域定义了在应用程序中创建的 Bean 实例的生命周期和可见范围,主要有以下几种。

  1. 单例:这是默认的作用域,当一个 Bean 的作用域为 Singleton,那么 Spring IoC 容器中只会存在一个共享的 Bean 实例,并且所有对 Bean 的请求,只要 id 与该 bean 定义相匹配,则只会返回 bean 的同一实例。
  2. 原型:当一个 bean 的作用域为 prototype,表示一个 bean 定义对应多个对象实例。prototype 作用域的 bean 会导致在每次对该 bean 请求时都会创建一个新的 bean 实例。因此,每次请求都会得到一个新的 Bean 实例。
  3. 请求:一个HTTP请求对应一个 Bean 实例,每个请求都有自己的 Bean 实例,且该 Bean 仅在请求期间有效。
  4. 会话:一个 HTTP 会话对应一个 Bean 实例,Bean 的生命周期与用户的会话周期相同。
  5. 应用程序:对于定义在 ServletContext 中的 Bean,整个 Web 应用程序共享一个 Bean 实例。
  6. Websocket:WebSocket生命周期内,每个 WebSocket 会话拥有一个 Bean 实例。

Bean 的生命周期

Spring Bean 的生命周期,其实就是 Spring 容器从创建 Bean 到销毁 Bean 的整个过程。这里面有几个关键步骤:

  1. 实例化 Bean:Spring 容器通过构造器或工厂方法创建 Bean 实例。
  2. 设置属性:容器会注入 Bean 的属性,这些属性可能是其他 Bean 的引用,也可能是简单的配置值。
  3. 检查 Aware 接口并设置相关依赖:如果 Bean 实现了 BeanNameAware 或 BeanFactoryAware 接口,容器会调用相应的 setBeanName 或 setBeanFactory 方法。
  4. BeanPostProcessor:在 Bean 的属性设置之后,Spring 会调用所有注册的 BeanPostProcessor 的 postProcessBeforeInitialization 方法。
  5. 初始化 Bean:如果 Bean 实现了 InitializingBean 接口,容器会调用其 afterPropertiesSet 方法。同时,如果 Bean 定义了 init-method,容器也会调用这个方法。
  6. BeanPostProcessor 的第二次调用:容器会再次调用所有注册的 BeanPostProcessor 的 postProcessAfterInitialization 方法,这次是在 Bean 初始化之后。
  7. 使用 Bean:此时,Bean 已经准备好了,可以被应用程序使用了。
  8. 处理 DisposableBean 和 destroy-method:当容器关闭时,如果 Bean 实现了 DisposableBean 接口,容器会调用其 destroy 方法。如果 Bean 定义了 destroy-method,容器也会调用这个方法。
  9. Bean 销毁:最后,Bean 被 Spring 容器销毁,结束了它的生命周期。

Bean 循环依赖是怎么解决的

  1. 什么是循环依赖

两个或者两个以上的 bean 互相持有对方,最终形成闭环。比如 Bean A 依赖于 Bean B,而 Bean B 又依赖于 Bean A,形成了一个循环依赖关系。这种情况下,如果不处理,会导致 Spring 容器无法完成 Bean 的初始化,从而抛出循环依赖异常。

  1. 怎么检测是否存在循环依赖

检测循环依赖相对比较容易,Bean 在创建的时候可以给该 Bean 打标,如果递归调用回来发现正在创建中的话,即说明了循环依赖了。

  1. 如何解决
  • 构造器循环依赖:Spring 容器在创建 Bean 时,如果遇到循环依赖,通常是无法处理的,因为这会导致无限递归创建 Bean 实例。所以,构造器注入是不支持循环依赖的。
  • 字段注入或 Setter 注入:使用了三级缓存来解决循环依赖问题。
    1. 首先,Spring 容器会创建一个 Bean 的原始实例,但此时Bean的属性尚未设置,这个实例被存放在一级缓存中。
    2. 当 Bean 的属性被设置时,如果属性值是其他 Bean 的引用,Spring 会去检查二级缓存,看是否已经有该 Bean 的引用存在。
    3. 如果二级缓存中没有,Spring 会尝试创建这个被引用的 Bean,并将其放入三级缓存。
    4. 最后,当 Bean 的属性设置完成后,原始的 Bean 实例会被放入二级缓存,供其他 Bean 引用
  • 使用 @Lazy 注解:通过 @Lazy 注解,可以延迟 Bean 的加载,直到它被实际使用时才创建,这可以避免一些循环依赖的问题。

Spring 中用到了哪些设计模式

  • 工厂设计模式:Spring 使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象。
  • 代理设计模式:Spring AOP 功能的实现。
  • 单例设计模式:Spring 中的 Bean 默认都是单例的。
  • 模板方法模式:Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。
  • 包装器设计模式:我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访 问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
  • 观察者模式:Spring 事件驱动模型就是观察者模式很经典的一个应用。
  • 适配器模式:Spring AOP 的增强或通知(Advice)使用到了适配器模式、Spring MVC 中也是用到了适配器模式适配 Controller。

Spring Boot Starter 有什么用

Spring Boot Starter 的作用是简化和加速项目的配置和依赖管理。

  • Spring Boot Starter 可以理解为一种预配置的模块,它封装了特定功能的依赖项和配置,开发者只需引入相关的 Starter 依赖,无需手动配置大量的参数和依赖项。常用的启动器包括 spring-boot-starter-web(用于 Web 应用)、spring-boot-starter-data-jpa(用于数据库访问)等。 引入这些启动器后,Spring Boot 会自动配置所需的组件和 Bean,无需开发者手动添加大量配置。
  • Starter 还管理了相关功能的依赖项,包括其他 Starter 和第三方库,确保它们能够良好地协同工作,避免版本冲突和依赖问题。
  • Spring Boot Starter 的设计使得应用可以通过引入不同的 Starter 来实现模块化的开发。每个 Starter 都关注一个特定的功能领域,如数据库访问、消息队列、Web 开发等。
  • 开发者可以创建自定义的 Starter,以便在项目中共享和重用特定功能的配置和依赖项。

Spring Boot 的常用注解

  • @SpringBootApplication:用于标识主应用程序类,通常位于项目的顶级包中。这个注解包含了 @Configuration、@EnableAutoConfiguration 和 @ComponentScan。
  • @Controller:用于标识类作为 Spring MVC 的 Controller。
  • @RestController:类似于 @Controller,但它是专门用于 RESTful Web 服务的。它包含了 @Controller 和 @ResponseBody。
  • @RequestMapping:用于将 HTTP 请求映射到 Controller 的处理方法。可以用在类级别和方法级别。
  • @Autowired:用于自动注入 Spring 容器中的 Bean,可以用在构造方法、字段、Setter 方法上。
  • @Service:用于标识类作为服务层的 Bean。
  • @Repository:用于标识类作为数据访问层的 Bean,通常用于与数据库交互。
  • @Component:通用的组件注解,用于标识任何 Spring 托管的 Bean。
  • @Configuration:用于定义配置类,类中可能包含一些 @Bean 注解用于定义 Bean。
  • @EnableAutoConfiguration:用于启用 Spring Boot 的自动配置机制,根据项目的依赖和配置自动配置 Spring 应用程序。
  • @Value:用于从属性文件或配置中读取值,将值注入到成员变量中。
  • @Qualifier: ​与 @Autowired 一起使用,指定注入时使用的 Bean 名称。
  • @ConfigurationProperties:用于将配置文件中的属性映射到 Java Bean。
  • @Profile:用于定义不同环境下的配置,可以标识在类或方法上。
  • @Async:用于将方法标记为异步执行。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注