Spring Bean生命周期全流程:从BeanDefinition到销毁

Spring Bean生命周期全流程:从BeanDefinition到销毁

1. 从一个让你困惑的现象说起

假设你刚接手一个Spring Boot项目,代码里有一个配置类:

java 复制代码
@Configuration
public class MyConfig {
    @Bean(initMethod = "init", destroyMethod = "cleanup")
    public MyService myService() {
        return new MyService();
    }
}

你直觉认为当Spring容器启动后,MyService对象就会被创建。但是,当你写了一个单元测试,只加载这个配置类,却发现MyService的构造器根本没有执行,除非你去调用它。

再看另一个常见场景:你写了两个Bean互相依赖,比如A需要B,B也需要A,你觉得Spring一定会报错,可实际Spring容器却成功启动,只是打印了一行奇怪的日志"正在创建Bean 'a'"。

这些现象背后,隐藏的就是Spring Bean的生命周期管理。如果你不理解它,就会在排查"为什么我的Bean没初始化""为什么销毁方法没被调用"等问题上浪费大量时间。本文将以实例为引导,带你一步步搞清楚从BeanDefinition到销毁的完整流程。

但在深入之前,先记住一个最简模型:Spring容器的核心工作就是把一个"配方"(BeanDefinition)变成一个"成品"(Bean实例),并在容器关闭时负责"回收"(销毁回调)

2. 整体框架:一次请求"走过"的完整路径

Spring容器内部有两大部分:BeanFactory (工厂)和ApplicationContext(应用上下文,它是BeanFactory的增强版,增加了国际化、事件发布等能力)。我们通常用的是ApplicationContext,但底层对Bean的管理都委托给BeanFactory。

管理一个Bean分三个阶段:注册阶段创建阶段销毁阶段。先看下面的ASCII图,它展示了整个流程的骨架和关键角色:

text 复制代码
+----------------+   1. 解析配置   +----------------+   2. 封装成     +----------------+
|                | --------------> |                | -------------> |                |
| XML/注解/Java类 |                | BeanDefinition |                | BeanFactory    |
|                |                |   注册表        |                |   容器中        |
+----------------+                +----------------+                +----------------+
                                                                         |
                                                                         | 3. 创建Bean时
                                                                         v
+----------------+   6. 初始化     +----------------+   5. 属性填充   +----------------+
|                对象可用          |  完成初始化      |                |  实例化         |
+----------------+                +----------------+                +----------------+
                                                                         |
                                                                         | 4. 解析依赖
                                                                         v
                                                                    +----------------+
                                                                    |   依赖的Bean    |
                                                                    |  (可能递归)     |
                                                                    +----------------+

销毁阶段:容器关闭 -> 调用@PreDestroy或destroyMethod

这个流程中,BeanFactory是核心引擎,它驱动着一系列BeanPostProcessor(后置处理器)在Bean创建的各个时机插入自定义逻辑。先记住几个重要时机:

  • 实例化前 (beforeInstantiation)和实例化后(afterInstantiation)。
  • 初始化前 (postProcessBeforeInitialization)和初始化后(postProcessAfterInitialization)。

这些后处理器是Spring实现AOP、自动注入、@Autowired等功能的钩子。理解它们,你就能在合适的阶段扩展自定义逻辑。

3. 注册阶段:BeanDefinition从哪里来

3.1 什么是BeanDefinition?

BeanDefinition是Bean的"元信息",它不包含对象本身,而是描述如何创建这个Bean:类名、作用域、构造参数、属性值、初始化方法名、销毁方法名等。可以把BeanDefinition想象成烹饪食谱,而Bean是最终做出来的菜。

3.2 配置解析示例

我们来运行一个最简例子。先看一个XML配置:

xml 复制代码
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="userService" class="com.example.UserService">
        <property name="name" value="张三"/>
    </bean>
</beans>

示例1:从XML加载Bean并查看BeanDefinition

下面是一个完整的可运行类,用于加载上面的XML,并在创建Bean前打印BeanDefinition的内容。

java 复制代码
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.core.io.ClassPathResource;

public class BeanDefinitionDemo {
    public static void main(String[] args) {
        // 创建一个BeanFactory(Spring容器最基础的实现)
        DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
        // 用XML读取器解析XML配置
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
        reader.loadBeanDefinitions(new ClassPathResource("beans.xml"));

        // 注意:此时只是注册了BeanDefinition,还没创建UserService实例
        BeanDefinition bd = factory.getBeanDefinition("userService");
        System.out.println("Bean类: " + bd.getBeanClassName());
        System.out.println("作用域: " + bd.getScope());
        System.out.println("属性值: " + bd.getPropertyValues());
        // 输出初始化方法名,默认null
        System.out.println("初始化方法: " + bd.getInitMethodName());

        // 现在真正触发创建(如果调用getBean)
        UserService service = factory.getBean(UserService.class);
        System.out.println(service.getName());
    }
}

// UserService类
public class UserService {
    private String name;

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

前置环境:将上述XML放在resources目录,并将UserService和示例类放在任意包下,编译运行即可。

关键步骤与结果 :运行后你会看到控制台先打印Bean的类名和属性,最后打印"张三"。注意getBean之前,Spring并没有执行构造器。这证明了BeanDefinition是惰性加载的基础 。什么时候才真正创建?通常是在首次getBean()时,除非使用lazy-init="true",否则Spring在启动时就会预先创建所有单例Bean。

适用场景:排查"为什么我的Bean没有被创建"时,先检查BeanDefinition是否被注册,再检查触发创建的时机。

常见错误 :很多人以为@Configuration类中的@Bean方法在Spring启动时就会被调用,实际上Spring会解析配置类,将@Bean方法的信息转为BeanDefinition,但Bean的实例还是在容器启动阶段(除非是@Lazy)一次性创建。

3.3 注册方式对比

下表总结了三种常见的BeanDefinition注册方式:

方式 典型配置 特点
XML <bean> 早年间标准,配置冗长
注解 @Component, @Service, @Bean 基于类路径扫描或@Configuration,是当前主流
编程式 BeanDefinitionRegistry.registerBeanDefinition() 代码动态注册,适合框架集成、条件判断场景

4. 创建阶段:实例化前后的处理

4.1 实例化从哪里开始?

当容器需要某个Bean时(通常是启动阶段所有单例Bean),会进入AbstractAutowireCapableBeanFactory.createBean()。在这里,Spring先会询问每个BeanPostProcessor:"你是否要在这个Bean创建前返回一个代理对象?"这就是实例化前的时机。

请记住:实例化在这里不是调用构造器,而是通过createBeanInstance选择构造器并创建对象

4.2 实例化前后的扩展点示例

示例2:实例化前返回代理对象

我们自定义一个BeanPostProcessor,干扰Bean创建。

java 复制代码
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;

public class ProxyPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
        // 只对MyService应用代理
        if ("myService".equals(beanName)) {
            System.out.println("实例化前:为myService创建代理");
            // 返回一个代理对象替代真实实例,后面真正的实例化过程将会被跳过
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(beanClass);
            enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
                System.out.println("代理拦截调用:" + method.getName());
                return proxy.invokeSuper(obj, args);
            });
            return enhancer.create();
        }
        return null;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }
}

注册这个BeanPostProcessor(通过@Configuration或XML),然后加载一个包含myService的配置,看控制台输出。

前置环境:需要一个MyService类(可以是空类),并注册上面的PostProcessor。

运行效果 :控制台会打印"实例化前:为myService创建代理"。当调用myService的方法时,会先打印代理拦截调用。注意:由于在postProcessBeforeInstantiation返回了非null对象,Spring会完全跳过后续的实例化和属性填充等步骤,直接进入postProcessAfterInitialization(实际上它会在返回代理后直接返回)。

关键解析 :这个机制被Spring AOP使用。当开启@EnableAspectJAutoProxy时,AspectJAutoProxyCreator就是这个后处理器。它会在实例化前判断是否有切面适用,如果适用,就返回代理。

边界与常见错误 :如果后处理器在postProcessBeforeInstantiation返回了非null代理,那么Bean还是会被创建登记到容器里,但它的生命周期其余回调(比如InitializingBean)将不会执行。因此如果你在代理类中依赖初始化逻辑,可能失效。

4.3 完整创建过程的时间线

我们再看看一个Bean从实例化到初始化完成的详细时间线。postProcessAfterInstantiation只是属性填充后,postProcessBeforeInitialization之前的一个时机,通常很少使用。下面用ASCII时间线展示:

text 复制代码
时间轴 →
步骤1: 实例化 (调用构造器)           -> new Object()
步骤2: postProcessAfterInstantiation (如果返回false则跳过属性填充,默认返回true)
步骤3: 属性填充 (populateBean)       -> @Autowired,@Resource,@Value 在这里注入
步骤4: postProcessBeforeInitialization <- 在这里可以做before初始化的逻辑
步骤5: 初始化 (调用afterPropertiesSet或init-method)
步骤6: postProcessAfterInitialization  <- AOP代理通常在这里
步骤7: 生成了现成的Bean,放入单例池

5. 初始化过程:Bean准备好被使用

5.1 初始化方法有几种?

Spring提供了三种指定初始化回调的方式,各自的优先级在注释中体现。下表比较:

方式 机制 执行顺序 典型应用
@PostConstruct JSR-250注解,由CommonAnnotationBeanPostProcessor处理 最先执行 需要依赖注入后才做初始化,且不想和Spring耦合
InitializingBean接口 Spring直接调用afterPropertiesSet() 在@PostConstruct之后 需要明确实现Spring接口
@Bean(initMethod) + init-method XML 反射调用指定方法名 最后执行 主要用于配置类中的@Bean方法,或第三方Bean

5.2 一个综合初始化的例子

我们用三种方式混用,观察执行顺序。你可能会遇到很多源码提问"顺序是什么?",这个例子给出了实证。

示例3:三种初始化方式执行顺序验证

定义一个包含三种初始化的Bean。

java 复制代码
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component(initMethod = "customInit") // 注意@Bean的initMethod不能和@Component并存,这里为演示改用@Bean方式
public class LifecycleBean implements InitializingBean {
    @Value("${app.name}")
    private String name;

    public LifecycleBean() {
        System.out.println("构造器执行");
    }

    @PostConstruct
    public void postConstruct() {
        System.out.println("1 @PostConstruct");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("2 InitializingBean#afterPropertiesSet");
    }

    public void customInit() {
        System.out.println("3 @Bean(initMethod)");
    }
}

然后编写一个@Configuration类注册它:

java 复制代码
@Configuration
public class LifecycleConfig {
    @Bean(initMethod = "customInit")
    public LifecycleBean lifecycleBean() {
        return new LifecycleBean();
    }
}

启动一个简单Spring应用:

java 复制代码
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class LifecycleDemo {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(LifecycleConfig.class);
        context.close();
    }
}

运行结果

text 复制代码
构造器执行
1 @PostConstruct
2 InitializingBean#afterPropertiesSet
3 @Bean(initMethod)

关键解析 :顺序完全符合预期。@PostConstruct在执行时,属性已经被填充。如果你依赖属性注入,这些属性已经可用。

边界提醒@Component不能直接设置initMethod,所以必须使用@Bean(initMethod)。如果你的Bean是由@Component注册的,里面只有@PostConstruct和实现InitializingBean这两种方式可用。

5.3 为什么需要这么多初始化方式?

这些方式的历史背景不同:InitializingBean是Spring老接口;@PostConstruct是JDK的注解,只是Spring借用它,好处是不依赖Spring;init-method非常适合第三方Bean,因为你无法给那个类增加注解。

最容易被误解的是,@PostConstruct实际上在postProcessBeforeInitialization阶段被调用,而InitializingBeanafterPropertiesSet中被调用,后者发生在postProcessAfterInitialization里。不要混淆这个先后顺序:@PostConstruct一定会优先于InitializingBean,因为CommonAnnotationBeanPostProcessorpostProcessBeforeInitialization先注册了。

6. 销毁回调:优雅地清理资源

6.1 销毁的时机

当容器关闭时(比如context.close()),Spring会调用每个单例Bean的销毁回调。但要注意,只有单例Bean在容器关闭时才会被销毁;原型作用域的Bean由调用方负责清理,容器不管理。

6.2 销毁方法的三种方式

和初始化类似,有三种对应方式。记住一个关键点:销毁方法只会调用一次,且如果同时定义了@PreDestroy和InitializingBean(这里用错误示例),实际顺序会符合预期。

下表对比销毁方式:

方式 调用时机 示例
@PreDestroy 先于其他销毁回调执行 资源关闭、线程池关闭
DisposableBean#destroy() 在@PostDestroy之后,但先于destroy-method 需要实现Spring接口
destroyMethod / @Bean(destroyMethod) 最后执行 第三方Bean,如连接池

6.3 一个带清理操作的完整示例

我们模拟一个占用资源的Bean。

示例4:带有三种销毁方法的Bean

java 复制代码
import javax.annotation.PreDestroy;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.stereotype.Component;

@Component(destroyMethod = "cleanup")
public class ResourceBean implements DisposableBean {
    public ResourceBean() {
        System.out.println("ResourceBean构造");
    }

    @PreDestroy
    public void preDestroy() {
        System.out.println("1 @PreDestroy: 释放资源");
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("2 DisposableBean#destroy");
    }

    public void cleanup() {
        System.out.println("3 @Bean(destroyMethod)");
    }
}

启动上下文后关闭:

java 复制代码
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class DestroyDemo {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("com.example");
        context.close();
    }
}

输出:

复制代码
1 @PreDestroy: 释放资源
2 DisposableBean#destroy
3 @Bean(destroyMethod)

关键解析 :注意@Component(destroyMethod = "cleanup")的写法可行,通过属性指定销毁方法。

适用场景:数据库连接、网络连接、文件句柄、线程池等需要释放的资源。不释放可能导致内存泄漏或连接泄漏。

常见错误 :很多人以为在@Bean上指定destroyMethod,Bean被getBean获取后手动close时会触发,其实不一定。@Bean(destroyMethod)只有在容器关闭时才回调。如果希望手动关闭这个Bean,要调用context.close()

7. 循环依赖处理:一个巧妙的设计

7.1 什么是循环依赖?

循环依赖指Bean A依赖Bean B,而B又依赖A。最常见的形式是构造器注入(无法解决)和setter注入(可以解决)。

7.2 三级缓存机制

Spring解决setter注入的循环依赖依靠的是"提前暴露半成品"的三级缓存。看下面的ASCII图:

text 复制代码
一级缓存: singletonObjects (完成品)
二级缓存: earlySingletonObjects (提前暴露的半成品,尚未属性填充)
三级缓存: singletonFactories (ObjectFactory,能生成代理或完整对象)

创建A时:
  1. new A() 还没属填充
  2. A放入三级缓存 (singletonFactories) 作为ObjectFactory
  3. 填充A属性,发现需要B
  4. 创建B,填充B属性,发现需要A
  5. 从三级缓存得到A的ObjectFactory,并提前暴露给B (移到二级缓存)
  6. B完成创建,放入一级缓存
  7. A继续完成初始化,放入一级缓存

7.3 什么情况下会报错?

构造器注入无法解决循环依赖,因为new A时就需要B的实例,而此时B还没创建。还有一种情况是@Async等代理未提前暴露,也会报错。

常见误区是:只要出现循环就一定会报错。实际上如果你的代码使用setter注入(包括@Autowired字段注入,这是setter注入的变体),Spring能自动处理。

7.4 使用@Lazy解决顽固循环

当无法避免构造器循环时,可以在其中一个依赖上加@Lazy,Spring会创建一个代理注入,真正的依赖在第一次使用时才创建。

示例5:演示三级缓存能解决的问题

我们写一个简单的循环依赖例子。

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class AService {

    private BService bService;

    @Autowired
    public void setBService(BService bService) {
        System.out.println("注入BService到A");
        this.bService = bService;
    }
}

@Component
public class BService {
    private AService aService;

    @Autowired
    public void setAService(AService aService) {
        System.out.println("注入AService到B");
        this.aService = aService;
    }
}

启动上下文,观察日志顺序:

java 复制代码
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class CircularDemo {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("com.example");
        context.close();
    }
}

输出可能类似:

复制代码
注入BService到A
注入AService到B

关键解析 :这个顺序表明,在填充A的bService属性时,Spring开始创建B,然后在B的属性填充中获得A的"早期引用"。

边界与常见错误 :如果你将注入改成构造器注入,容器启动会抛出BeanCurrentlyInCreationException。下面的示例展示了如何用@Lazy来解决。

示例6:使用@Lazy打破构造器循环

java 复制代码
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

@Component
public class ServiceA {
    private ServiceB serviceB;

    public ServiceA(@Lazy ServiceB serviceB) {
        this.serviceB = serviceB;
    }
}

@Component
public class ServiceB {
    private ServiceA serviceA;

    public ServiceB(ServiceA serviceA) {
        this.serviceA = serviceA;
    }
}

如果去掉@Lazy启动会报错。加上后正常。但如果运行时真的调用了serviceB的方法,代理会去获取真正的Bean,到时候可能再次产生创建问题?实际上因为有缓存,代理会触发创建,所以能成功。

关键解析@Lazy注入的是代理对象,Spring会为它生成一个代理,该代理在首次访问目标对象时才触发创建。这种方式能解决构造器循环,但代价是延迟了依赖的初始化,可能影响启动时检查。

8. 可见误区:这些情况你以为Bean会被创建和销毁

误区1:给prototype作用域的Bean写销毁方法,以为容器关闭时会调用。 原型Bean容器不管理其生命周期,除非你手动获得Bean并调用销毁方法。

误区2:使用了@PostConstruct就一定会在依赖注入后执行。 是的,但如果你在一个修改的Bean上(如从@PostConstruct返回了代理),后处理器可能跳过初始化,导致@PostConstruct不执行。

误区3:Bean的构造器会先于某些静态字段的初始化。 无关。Java类加载时静态块先于任何对象创建,但Bean的依赖注入发生在构造器之后。

9. 生产实践建议

经验1:尽量使用构造器注入,它能使依赖不可变,且能避免循环依赖,更容易测试。如果必须用字段或setter注入,确保没有构造器循环。

经验2:合理利用@PostConstruct和@PreDestroy,因为它们不依赖Spring接口,在其他容器(比如CDI)中也可复用。

经验3:在分布式场景或微服务中,谨慎使用通过Bean初始化的资源连接。 如果连接不可用,会导致Bean创建失败,容器无法启动。建议使用懒初始化或重试机制。

经验4:调试循环依赖时,开启DEBUG日志 ,可以看到Creating shared instance of singleton bean等信息。

10. 排障清单:常见的Bean生命周期异常

下表列出几个典型异常及其原因:

异常信息 可能原因 排查方向
BeanCreationException: Error creating bean with name 'xxx' 构造器异常、依赖注入失败、初始化方法抛错 查看堆栈定位具体Bean和行
BeanCurrentlyInCreationException 构造器循环依赖或不可解决的循环 检查是否用setter注入,或使用@Lazy
DefaultListableBeanFactory 找不到Bean 没有被扫描或没有注册 检查@ComponentScan路径和BeanDefinition是否注册
销毁方法没有被调用 作用域不是singleton;或Bean代理导致没走销毁回调 检查作用域和代理方式

11. 面试/复盘问题

  • 请描述Spring Bean从定义到初始化的完整流程,包括各个扩展点。
  • @PostConstructInitializingBeaninit-method三种方式的执行顺序是什么?为什么?
  • 为什么构造器注入无法解决循环依赖?Spring的三级缓存是如何工作的?
  • 单例Bean的销毁回调一定会在容器关闭时执行吗?原型Bean呢?
  • 如果我想在Bean实例化前直接返回代理对象,应该实现哪个接口?

12. 总结与全局框架图

现在,我们用一个简化的时序图来总结整个生命周期,它同时也是我们上面所有细节的浓缩:

text 复制代码
BeanDefinition注册
   |
   v
实例化 (构造器)
   |
   v
属性填充 (依赖注入)
   |
   v
初始化前 *扩展点 (postProcessBeforeInitialization)
   |
   v
初始化 (@PostConstruct -> InitializingBean -> init-method)
   |
   v
初始化后 *扩展点 (postProcessAfterInitialization)  AOP在此
   |
   v
Bean就绪 (放入单例池)
   |
   v
容器关闭 -> 销毁前 @PreDestroy -> DisposableBean.destroy -> destroy-method

把握这个主线,遇到任何Spring Bean初始化问题,你都可以按这个顺序去推断:配置文件是否正确?是否到了初始化时机?有没有后处理器干扰?销毁是否被容器管理?

13. 参考资料

(本文示例基于Spring Framework 5.x,文中未特别说明的源码位置可查阅上述文档。)

附录:完整示例清单

本文包含可运行的完整示例6个:

  1. BeanDefinitionDemo.java (示例1)
  2. ProxyPostProcessor.java (示例2,需配合配置注册)
  3. LifecycleDemo.java (示例3,初始化顺序)
  4. DestroyDemo.java (示例4,销毁顺序)
  5. CircularDemo.java (示例5,setter循环)
  6. 示例6 (构造器循环+@Lazy,可独立运行)

运行方法:创建Maven项目,引入spring-context依赖,将代码复制至对应包,确保类路径正确,即可运行main方法。

注意:如果使用JDK9+,需要增加模块开放;使用Spring Boot则直接添加spring-boot-starter。

至此,从BeanDefinition到销毁的旅程全部结束,希望你能在遇到Bean相关问题时,回想起这篇文章的顺序。

相关推荐
leeyi1 小时前
在 Go 里跑不信任的代码:WASM 沙箱的四道墙(第107篇)
后端
摇滚侠1 小时前
《Spring Boot 3:高级与架构设计》第 1 章 BeanDefinitionRegistry 阅读笔记 3
spring boot·笔记·后端
君顾12 小时前
智慧场馆解决方案小程序系统实战:从架构设计到上线指南
java·开发语言·智慧场馆
zhougl9962 小时前
Dockerfile实战教程
java·开发语言·spring boot
新时代牛马2 小时前
Linux 驱动调试完整篇:从printk/dev_dbg、动态调试到 debugfs/ftrace 排障
java·linux·服务器
烂蜻蜓2 小时前
Flask入门教程(三十一):实用函数与类API——全局工具函数速查
后端·python·flask
计算机毕设定制辅导-无忧学长2 小时前
《基于Spring Boot传承之光非遗陶瓷烧造产品交易平台的设计与实现》
java·vue.js·spring boot·后端·毕业设计
Zane19942 小时前
内存都要回收,为什么JVM偏要把堆分成新生代和老年代
java·后端